Compare commits

...

233 Commits

Author SHA1 Message Date
Daniel Hougaard
be2fc4fec4 Update Chart.yaml 2024-05-08 00:42:38 +02:00
Daniel Hougaard
829dbb9970 Update values.yaml 2024-05-08 00:41:53 +02:00
Daniel Hougaard
0b012c5dfb Chore: Helm 2024-05-08 00:23:50 +02:00
Daniel Hougaard
b0421ccad0 Docs: Add recursive to example 2024-05-08 00:21:08 +02:00
Daniel Hougaard
6b83326d00 Feat: Recursive mode support 2024-05-08 00:18:53 +02:00
Daniel Hougaard
1f6abc7f27 Feat: Recursive mode and fix error formatting 2024-05-08 00:18:40 +02:00
Daniel Hougaard
4a02520147 Update sample 2024-05-08 00:18:26 +02:00
Daniel Hougaard
14f38eb961 Feat: Recursive mode types 2024-05-08 00:16:51 +02:00
Maidul Islam
3a1cdc4f44 Delete backend/src/db/migrations/20240507162149_test.ts 2024-05-07 15:41:09 -04:00
Maidul Islam
2929d94f0a Merge pull request #1797 from Infisical/maidul98-patch-10
test
2024-05-07 14:28:03 -04:00
Maidul Islam
0383ae9e8b Create 20240507162149_test.ts 2024-05-07 14:27:44 -04:00
Maidul Islam
00faa6257f Delete backend/src/db/migrations/20240507162149_test.ts 2024-05-07 14:27:33 -04:00
Maidul Islam
183bde55ca correctly fetch merged by user login 2024-05-07 14:26:56 -04:00
Maidul Islam
c96fc1f798 Merge pull request #1795 from Infisical/maidul98-patch-9
test
2024-05-07 14:09:49 -04:00
Maidul Islam
80f7ff1ea8 Create 20240507162149_test.ts 2024-05-07 14:09:38 -04:00
Maidul Islam
c87620109b Rename 20240507162141_access to 20240507162141_access.ts 2024-05-07 13:58:10 -04:00
Maidul Islam
02c158b4ed Delete backend/src/db/migrations/20240507162180_test 2024-05-07 13:47:25 -04:00
Maidul Islam
ddfa64eb33 Merge pull request #1793 from Infisical/maidul98-patch-8
testing-ignore
2024-05-07 13:27:19 -04:00
Maidul Islam
7fdaa1543a Create 20240507162180_test 2024-05-07 13:26:52 -04:00
Maidul Islam
c8433f39ed Delete backend/src/db/migrations/20240507162180_test 2024-05-07 13:26:42 -04:00
Maidul Islam
ba238a8f3b get pr details by pr number 2024-05-07 13:25:35 -04:00
Sheen Capadngan
dd89a80449 Merge pull request #1788 from Infisical/feature/add-multi-select-deletion-overview
Feature: Add support for deleting secrets and folders in the Overview page
2024-05-08 01:25:21 +08:00
Maidul Islam
a1585db76a Merge pull request #1791 from Infisical/maidul98-patch-7
Create 20240507162180_test
2024-05-07 13:16:59 -04:00
Maidul Islam
f5f0bf3c83 Create 20240507162180_test 2024-05-07 13:16:42 -04:00
Maidul Islam
3638645b8a get closed by user 2024-05-07 13:15:15 -04:00
Sheen Capadngan
f957b9d970 misc: migrated to react-state 2024-05-08 01:03:41 +08:00
Maidul Islam
b461697fbf Merge pull request #1790 from Infisical/fix/api-doc-typo
doc: fixed typo in api privilege documentation
2024-05-07 12:56:34 -04:00
Akhil Mohan
3ce91b8a20 doc: fixed typo in api privilege documentation 2024-05-07 22:25:36 +05:30
Maidul Islam
78922a80e2 Merge pull request #1716 from Infisical/snyk-fix-0eecde4245cc6ed2d19ec9aa18a14703
[Snyk] Security upgrade mysql2 from 3.9.4 to 3.9.7
2024-05-07 12:23:13 -04:00
Maidul Islam
0181007c66 Merge pull request #1789 from Infisical/create-pull-request/patch-1715098901
GH Action: rename new migration file timestamp
2024-05-07 12:22:42 -04:00
github-actions
306cf8733e chore: renamed new migration files to latest timestamp (gh-action) 2024-05-07 16:21:40 +00:00
Maidul Islam
6e829516db Merge pull request #1652 from Infisical/daniel/request-access
Feat: Request Access
2024-05-07 12:21:17 -04:00
Sheen Capadngan
c08fcc6f5e adjustment: finalized notification text 2024-05-08 00:12:55 +08:00
Daniel Hougaard
9a585ad930 Fix: Rebase error 2024-05-07 17:30:36 +02:00
Daniel Hougaard
95c1fff7d3 Chore: Remove unused files 2024-05-07 17:30:36 +02:00
Daniel Hougaard
9c2591f3a6 Fix: Moved Divider to v2 2024-05-07 17:30:36 +02:00
Daniel Hougaard
a579598b6d Chore: Moved verifyApprovers 2024-05-07 17:30:36 +02:00
Daniel Hougaard
af0d31db2c Fix: Improved migrations 2024-05-07 17:30:36 +02:00
Daniel Hougaard
fb6c4acf31 Delete access-approval-request-secret-dal.ts 2024-05-07 17:30:36 +02:00
Daniel Hougaard
551ca0fa8c Migration improvements 2024-05-07 17:30:36 +02:00
Daniel Hougaard
4a0ccbe69e Fixed bugs 2024-05-07 17:30:36 +02:00
Daniel Hougaard
f5a463ddea Update SecretApprovalPage.tsx 2024-05-07 17:30:36 +02:00
Daniel Hougaard
ce1ad6f32e Fix: Rebase errors 2024-05-07 17:30:36 +02:00
Daniel Hougaard
56c8b4f5e5 Removed unnessecary types 2024-05-07 17:30:36 +02:00
Daniel Hougaard
29b26e3158 Update AccessApprovalRequest.tsx 2024-05-07 17:30:36 +02:00
Daniel Hougaard
6e209bf099 Update AccessApprovalRequest.tsx 2024-05-07 17:30:36 +02:00
Daniel Hougaard
949d210263 Update AccessApprovalRequest.tsx 2024-05-07 17:30:36 +02:00
Vladyslav Matsiiako
1a2d8e96f3 style changes 2024-05-07 17:30:36 +02:00
Daniel Hougaard
9198eb5fba Update licence-fns.ts 2024-05-07 17:30:36 +02:00
Daniel Hougaard
0580f37c5e Update generate-schema-types.ts 2024-05-07 17:30:36 +02:00
Daniel Hougaard
e53d40f0e5 Update SecretApprovalPage.tsx 2024-05-07 17:30:36 +02:00
Daniel Hougaard
801c0c5ada Fix: Remove redundant code 2024-05-07 17:30:36 +02:00
Daniel Hougaard
7b8af89bee Fix: Validate approvers access 2024-05-07 17:30:36 +02:00
Daniel Hougaard
ef7f5c9eac Feat: Request access (new routes) 2024-05-07 17:30:36 +02:00
Daniel Hougaard
db0b4a5ad1 Feat: Request access 2024-05-07 17:30:36 +02:00
Daniel Hougaard
cb505d1525 Draft 2024-05-07 17:30:36 +02:00
Daniel Hougaard
c66476e2b4 Fix: Multiple approvers acceptance bug 2024-05-07 17:30:36 +02:00
Daniel Hougaard
60a06edd9b Style: Fix styling 2024-05-07 17:30:36 +02:00
Daniel Hougaard
e8e1d46f0e Capitalization 2024-05-07 17:30:36 +02:00
Daniel Hougaard
038fe3508c Removed unnessecary types 2024-05-07 17:30:36 +02:00
Daniel Hougaard
7d1dff9e5a Fix: Security vulnurbility making it possible to spoof env & secret path requested. 2024-05-07 17:30:36 +02:00
Daniel Hougaard
5117f5d3c1 Update AccessApprovalRequest.tsx 2024-05-07 17:30:36 +02:00
Daniel Hougaard
350dd97b98 Update AccessApprovalRequest.tsx 2024-05-07 17:30:36 +02:00
Daniel Hougaard
121902e51f Update AccessApprovalRequest.tsx 2024-05-07 17:30:36 +02:00
Vladyslav Matsiiako
923bf02046 style changes 2024-05-07 17:30:36 +02:00
Daniel Hougaard
27447ddc88 Update licence-fns.ts 2024-05-07 17:30:36 +02:00
Daniel Hougaard
a3b4b650d1 Removed unused parameter 2024-05-07 17:30:36 +02:00
Daniel Hougaard
3f0f45e853 Update SpecificPrivilegeSection.tsx 2024-05-07 17:30:36 +02:00
Daniel Hougaard
3bb50b235d Update generate-schema-types.ts 2024-05-07 17:30:36 +02:00
Daniel Hougaard
1afd120e8e Feat: Request access 2024-05-07 17:30:36 +02:00
Daniel Hougaard
ab3593af37 Feat: Request access 2024-05-07 17:30:36 +02:00
Daniel Hougaard
2c2afbea7a Fix: Move to project slug 2024-05-07 17:30:36 +02:00
Daniel Hougaard
4eabbb3ac5 Fix: Added support for request access 2024-05-07 17:30:36 +02:00
Daniel Hougaard
1ccd74e1a5 Fix: Remove redundant code 2024-05-07 17:30:35 +02:00
Daniel Hougaard
812cced9d5 Feat: Request access 2024-05-07 17:30:35 +02:00
Daniel Hougaard
cd6be68461 Fix: Validate approvers access 2024-05-07 17:30:35 +02:00
Daniel Hougaard
5c69bbf515 Feat: Request access (new routes) 2024-05-07 17:30:35 +02:00
Daniel Hougaard
448f89fd1c Feat: Request Access (migrations) 2024-05-07 17:30:35 +02:00
Daniel Hougaard
3331699f56 Feat: Request access 2024-05-07 17:30:35 +02:00
Daniel Hougaard
810f670e64 Feat: Request Access 2024-05-07 17:30:35 +02:00
Daniel Hougaard
5894df4370 Draft 2024-05-07 17:30:35 +02:00
Daniel Hougaard
2aacd54116 Update SpecificPrivilegeSection.tsx 2024-05-07 17:30:35 +02:00
Daniel Hougaard
73d9fcc0de Draft 2024-05-07 17:30:35 +02:00
Tuan Dang
7ac3bb20df Update instance recognition of offline license 2024-05-07 17:30:35 +02:00
Daniel Hougaard
d659b5a624 Fix: Duplicate access request check 2024-05-07 17:30:35 +02:00
Daniel Hougaard
0bbdf2a8f4 Update SecretApprovalPage.tsx 2024-05-07 17:30:35 +02:00
Daniel Hougaard
a8eba9cfbf Fix: Moved from email to username 2024-05-07 17:30:35 +02:00
Daniel Hougaard
a3d7c5f599 Cleanup 2024-05-07 17:30:35 +02:00
Daniel Hougaard
c325674da0 Fix: Move standalone components to individual files 2024-05-07 17:30:35 +02:00
Daniel Hougaard
3637152a6b Chore: Remove unused files 2024-05-07 17:30:35 +02:00
Daniel Hougaard
8ed3c0cd68 Fix: Use username instead of email 2024-05-07 17:30:35 +02:00
Daniel Hougaard
cdd836d58f Fix: Columns 2024-05-07 17:30:35 +02:00
Daniel Hougaard
3d3b1eb21a Fix: Use username instead of email 2024-05-07 17:30:35 +02:00
Daniel Hougaard
6aab28c4c7 Feat: Badge component 2024-05-07 17:30:35 +02:00
Daniel Hougaard
f038b28c1c Fix: Moved Divider to v2 2024-05-07 17:30:35 +02:00
Daniel Hougaard
24a286e898 Update index.ts 2024-05-07 17:30:35 +02:00
Daniel Hougaard
0c1103e778 Fix: Pick 2024-05-07 17:30:35 +02:00
Daniel Hougaard
2c1eecaf85 Chore: Moved verifyApprovers 2024-05-07 17:30:35 +02:00
Daniel Hougaard
5884565de7 Fix: Make verifyApprovers independent on memberships 2024-05-07 17:30:35 +02:00
Daniel Hougaard
dd43268506 Fix: Made API endpoints more REST compliant 2024-05-07 17:30:35 +02:00
Daniel Hougaard
9d362b8597 Chore: Cleaned up models 2024-05-07 17:30:35 +02:00
Daniel Hougaard
972ecc3e92 Fix: Improved migrations 2024-05-07 17:30:35 +02:00
Daniel Hougaard
dc3014409f Delete access-approval-request-secret-dal.ts 2024-05-07 17:30:35 +02:00
Daniel Hougaard
4e449f62c0 Fix: Don't display requested by when user has no access to read workspace members 2024-05-07 17:30:35 +02:00
Daniel Hougaard
c911a7cd81 Fix: Don't display requested by when user has no access to read workspace members 2024-05-07 17:30:35 +02:00
Daniel Hougaard
44370d49e3 Fix: Add tooltip for clarity and fix wording 2024-05-07 17:30:35 +02:00
Daniel Hougaard
c7d2dfd351 Fix: Requesting approvals on previously rejected resources 2024-05-07 17:30:35 +02:00
Daniel Hougaard
1785548a40 Fix: Sort by createdAt 2024-05-07 17:30:35 +02:00
Daniel Hougaard
2baf9e0739 Migration improvements 2024-05-07 17:30:35 +02:00
Daniel Hougaard
01e7ed23ba Fixed bugs 2024-05-07 17:30:35 +02:00
Daniel Hougaard
1f789110e3 Update SecretApprovalPage.tsx 2024-05-07 17:30:35 +02:00
Daniel Hougaard
c874c943c1 Fix: Rebase errors 2024-05-07 17:30:35 +02:00
Daniel Hougaard
dab69dcb51 Removed unnessecary types 2024-05-07 17:30:35 +02:00
Daniel Hougaard
8e82bfae86 Update AccessApprovalRequest.tsx 2024-05-07 17:30:35 +02:00
Daniel Hougaard
bc810ea567 Update AccessApprovalRequest.tsx 2024-05-07 17:30:35 +02:00
Daniel Hougaard
22470376d9 Update AccessApprovalRequest.tsx 2024-05-07 17:30:35 +02:00
Vladyslav Matsiiako
bb9503471f style changes 2024-05-07 17:30:35 +02:00
Daniel Hougaard
a687b1d0db Update licence-fns.ts 2024-05-07 17:30:35 +02:00
Daniel Hougaard
0aa77f90c8 Update SpecificPrivilegeSection.tsx 2024-05-07 17:30:35 +02:00
Daniel Hougaard
5a04371fb0 Update generate-schema-types.ts 2024-05-07 17:30:35 +02:00
Daniel Hougaard
70c06c91c8 Update SecretApprovalPage.tsx 2024-05-07 17:30:35 +02:00
Daniel Hougaard
926d324ae3 Fix: Added support for request access 2024-05-07 17:30:35 +02:00
Daniel Hougaard
e48377dea9 Fix: Remove redundant code 2024-05-07 17:30:35 +02:00
Daniel Hougaard
5e1484bd05 Fix: Validate approvers access 2024-05-07 17:30:35 +02:00
Daniel Hougaard
6d9de752d7 Feat: Request access (new routes) 2024-05-07 17:30:35 +02:00
Daniel Hougaard
f9a9b1222e Feat: Request Access (migrations) 2024-05-07 17:30:35 +02:00
Daniel Hougaard
4326ce970a Feat: Request access 2024-05-07 17:30:35 +02:00
Daniel Hougaard
7a3a9ca9ea Draft 2024-05-07 17:30:35 +02:00
Daniel Hougaard
32a110e0ca Fix: Multiple approvers acceptance bug 2024-05-07 17:30:35 +02:00
Daniel Hougaard
da5278f6bf Fix: Rename change -> secret 2024-05-07 17:30:35 +02:00
Daniel Hougaard
7e765681cb Style: Fix styling 2024-05-07 17:30:35 +02:00
Daniel Hougaard
0990ce1f92 Capitalization 2024-05-07 17:30:35 +02:00
Daniel Hougaard
2369ff6813 Removed unnessecary types 2024-05-07 17:30:35 +02:00
Daniel Hougaard
478520f090 Remove unnessecary types and projectMembershipid 2024-05-07 17:30:35 +02:00
Daniel Hougaard
54313f9c08 Renaming 2024-05-07 17:30:35 +02:00
Daniel Hougaard
cb8763bc9c Update smtp-service.ts 2024-05-07 17:30:35 +02:00
Daniel Hougaard
c5d11eee7f Feat: Find users by project membership ID's 2024-05-07 17:30:35 +02:00
Daniel Hougaard
8e1d19c041 Feat: access request emails 2024-05-07 17:30:35 +02:00
Daniel Hougaard
608c7a4dee Update index.ts 2024-05-07 17:30:35 +02:00
Daniel Hougaard
c7b60bcf0e Update access-approval-request-types.ts 2024-05-07 17:30:35 +02:00
Daniel Hougaard
6ae62675be Feat: Send emails for access requests 2024-05-07 17:30:35 +02:00
Daniel Hougaard
fb2ab200b9 Feat: Request access, extract permission details 2024-05-07 17:30:35 +02:00
Daniel Hougaard
f1428d72c2 Fix: Security vulnurbility making it possible to spoof env & secret path requested. 2024-05-07 17:30:35 +02:00
Daniel Hougaard
4cb51805f0 Update AccessApprovalRequest.tsx 2024-05-07 17:30:35 +02:00
Daniel Hougaard
8c40918cef Update AccessApprovalRequest.tsx 2024-05-07 17:30:35 +02:00
Daniel Hougaard
3a002b921a Update AccessApprovalRequest.tsx 2024-05-07 17:30:35 +02:00
Vladyslav Matsiiako
299653528c style changes 2024-05-07 17:30:35 +02:00
Daniel Hougaard
8c256bd9c8 Fix: Status filtering & query invalidation 2024-05-07 17:30:35 +02:00
Daniel Hougaard
f8e0e01bb8 Fix: Access request query invalidation 2024-05-07 17:30:35 +02:00
Vladyslav Matsiiako
b59413ded0 fix privilegeId issue 2024-05-07 17:30:35 +02:00
Daniel Hougaard
15c747e8e8 Fix: Request access permissions 2024-05-07 17:30:35 +02:00
Daniel Hougaard
073a9ee6a4 Update licence-fns.ts 2024-05-07 17:30:35 +02:00
Daniel Hougaard
d371c568f1 Add count 2024-05-07 17:30:35 +02:00
Daniel Hougaard
e6c086ab09 Fix: Don't allow users to request access to the same resource with same permissions multiple times 2024-05-07 17:30:35 +02:00
Daniel Hougaard
890c8b89be Removed unused parameter 2024-05-07 17:30:35 +02:00
Daniel Hougaard
6f4b62cfbb Removed logs 2024-05-07 17:30:35 +02:00
Daniel Hougaard
076c70f6ff Removed logs 2024-05-07 17:30:35 +02:00
Daniel Hougaard
aedc1f2441 Update SpecificPrivilegeSection.tsx 2024-05-07 17:30:35 +02:00
Daniel Hougaard
352d363bd4 Update generate-schema-types.ts 2024-05-07 17:30:35 +02:00
Daniel Hougaard
ac92a916b4 Update SecretApprovalPage.tsx 2024-05-07 17:30:35 +02:00
Daniel Hougaard
17587ff1b8 Fix: Minor fixes 2024-05-07 17:30:35 +02:00
Daniel Hougaard
7f1c8d9ff6 Create index.tsx 2024-05-07 17:30:35 +02:00
Daniel Hougaard
ac24c0f760 Feat: Request access 2024-05-07 17:30:35 +02:00
Daniel Hougaard
0e95c1bcee Feat: Request access 2024-05-07 17:30:35 +02:00
Daniel Hougaard
447630135b Feat: Request access 2024-05-07 17:30:35 +02:00
Daniel Hougaard
ddd6adf804 Fix: Move to project slug 2024-05-07 17:30:35 +02:00
Daniel Hougaard
a4b6d2650a Fix: Move to project slug 2024-05-07 17:30:35 +02:00
Daniel Hougaard
2f5d6b11da Fix: Move to project slug 2024-05-07 17:30:35 +02:00
Daniel Hougaard
d380b7f788 Fix: Added support for request access 2024-05-07 17:30:35 +02:00
Daniel Hougaard
7aee4fdfcd Feat: Request access 2024-05-07 17:30:27 +02:00
Daniel Hougaard
83bd3a0bf4 Update index.tsx 2024-05-07 17:30:27 +02:00
Daniel Hougaard
1f68730aa3 Fix: Improve disabled Select 2024-05-07 17:30:27 +02:00
Daniel Hougaard
7fd1d72985 Fix: Access Request setup 2024-05-07 17:30:27 +02:00
Daniel Hougaard
b298eec9db Fix: Danger color not working on disabled buttons 2024-05-07 17:30:27 +02:00
Daniel Hougaard
696479a2ef Fix: Remove redundant code 2024-05-07 17:30:27 +02:00
Daniel Hougaard
ad6e2aeb9e Feat: Request Access 2024-05-07 17:30:27 +02:00
Daniel Hougaard
ad405109a0 Feat: Request access 2024-05-07 17:30:27 +02:00
Daniel Hougaard
992a82015a Feat: Request access 2024-05-07 17:30:27 +02:00
Daniel Hougaard
317956a038 Fix: Types mismatch 2024-05-07 17:30:27 +02:00
Daniel Hougaard
5255c4075a Fix: Validate approvers access 2024-05-07 17:30:27 +02:00
Daniel Hougaard
eca36f1993 Feat: Request access 2024-05-07 17:30:27 +02:00
Daniel Hougaard
7e29a6a656 Fix: Access Approval Policy DAL bugs 2024-05-07 17:30:27 +02:00
Daniel Hougaard
f458e34c37 Feat: Request access (new routes) 2024-05-07 17:30:27 +02:00
Daniel Hougaard
99f5ed1f4b Fix: Move to project slug 2024-05-07 17:30:27 +02:00
Daniel Hougaard
f981c59b5c Feat: Request access (models) 2024-05-07 17:30:27 +02:00
Daniel Hougaard
a528d011c0 Feat: Request Access (migrations) 2024-05-07 17:30:27 +02:00
Daniel Hougaard
d337118803 Feat: Request access 2024-05-07 17:30:27 +02:00
Daniel Hougaard
68a11db1c6 Feat: Request access 2024-05-07 17:30:27 +02:00
Daniel Hougaard
91bf6a6dad Fix: Remove logs 2024-05-07 17:30:13 +02:00
Daniel Hougaard
12c655a152 Feat: Request Access 2024-05-07 17:30:13 +02:00
Daniel Hougaard
1d2f10178f Draft 2024-05-07 17:30:13 +02:00
Tuan Dang
c5cd5047d7 Update trusted email migration file with backfill 2024-05-07 07:59:37 -07:00
Sheen Capadngan
06c103c10a misc: added handling for no changes made 2024-05-07 22:19:20 +08:00
Sheen Capadngan
b6a73459a8 misc: addressed rbac for bulk delete in overview 2024-05-07 16:37:10 +08:00
Sheen Capadngan
536f51f6ba misc: added descriptive error message 2024-05-07 15:21:17 +08:00
Sheen Capadngan
a9b72b2da3 feat: added handling of folder/secret deletion 2024-05-07 15:16:37 +08:00
Sheen Capadngan
a3552d00d1 feat: add multi-select in secret overview 2024-05-07 13:52:42 +08:00
Maidul Islam
c9f0ba08e1 Merge pull request #1787 from Infisical/create-pull-request/patch-1715052491
GH Action: rename new migration file timestamp
2024-05-07 01:17:35 -04:00
github-actions
308e605b6c chore: renamed new migration files to latest timestamp (gh-action) 2024-05-07 03:28:10 +00:00
Maidul Islam
4d8965eb82 Merge pull request #1762 from Infisical/groups-phase-2c
Groups Phase 2B (Trust external SAML/LDAP email option, email verification, SCIM user ID ref update)
2024-05-06 23:27:50 -04:00
Tuan Dang
0357e7c80e Put email-confirmation migration into trusted-saml-ldap-emails file 2024-05-06 19:58:58 -07:00
Tuan Dang
ba1b223655 Patch migration file hasTable ref 2024-05-06 19:44:43 -07:00
Tuan Dang
3b88a2759b Patch unsynchronized username/email for saml/scim 2024-05-06 18:27:36 -07:00
Maidul Islam
42383d5643 Merge pull request #1782 from akhilmhdh/feat/privilege-identity-api-change
Privilege identity api change
2024-05-06 15:01:02 -04:00
Akhil Mohan
d198ba1a79 feat: refactored the map unpack to a function 2024-05-06 23:27:51 +05:30
Maidul Islam
b3579cb271 rephrase text for permission schema zod 2024-05-06 13:44:39 -04:00
Tuan Dang
30ccb78c81 Merge remote-tracking branch 'origin' into groups-phase-2c 2024-05-06 09:33:36 -07:00
Maidul Islam
fdd67c89b3 Merge pull request #1783 from akhilmhdh:feat/dashboard-slug-fix
feat: debounced main page search and rolled back to old input component
2024-05-06 12:31:57 -04:00
Akhil Mohan
79e9b1b2ae feat: debounced main page search and rolled back to old input component 2024-05-06 20:43:23 +05:30
Akhil Mohan
86fd4d5fba feat: added a fixed sorted order to avoid jumps 2024-05-06 14:26:46 +05:30
Akhil Mohan
4692aa12bd feat: updated identity additional privilege permission object in api to have a proper body and explanation 2024-05-06 14:01:30 +05:30
Akhil Mohan
61a0997adc fix(ui): secret path input showing / for a valid value that comes delayed 2024-05-06 14:00:32 +05:30
Maidul Islam
b4f1bec1a9 Merge pull request #1781 from Infisical/feature/added-secret-expand-in-raw-secret-get
feat: added secret expand option in secrets get API
2024-05-04 22:09:12 -04:00
Maidul Islam
ab79342743 rename to expandSecretReferences 2024-05-04 22:05:57 -04:00
Maidul Islam
1957531ac4 Update docker-compose.mdx 2024-05-04 21:01:19 -04:00
Sheen Capadngan
61ae0e2fc7 feat: added secret expand option in secrets get API 2024-05-04 14:42:22 +08:00
Tuan Dang
ea4ef7f7ef Merge remote-tracking branch 'origin' into groups-phase-2c 2024-04-30 21:37:48 -07:00
Tuan Dang
0482424a1c Make merge user step automatic after email verification 2024-04-30 21:33:27 -07:00
Tuan Dang
ce2a9c8640 Rename migration file 2024-04-29 11:57:30 -07:00
Tuan Dang
ac97f273e3 Merge remote-tracking branch 'origin' into groups-phase-2c 2024-04-29 11:55:53 -07:00
Tuan Dang
69c50af14e Move trust saml/ldap emails to server config 2024-04-29 11:53:28 -07:00
Tuan Dang
519403023a Pick 2024-04-28 22:04:22 -07:00
Tuan Dang
b2a976f3d4 Update groups CRUD SCIM to use orgMembershipId 2024-04-28 21:58:24 -07:00
Tuan Dang
a7af3a48d9 Continue moving SCIM userId refs to orgMembershipId 2024-04-28 19:09:12 -07:00
Tuan Dang
80da2a19aa Add TRUST_SAML_EMAILS and TRUST_LDAP_EMAILS opts 2024-04-26 22:30:07 -07:00
Tuan Dang
858a35812a Finish preliminary email validation, merge user flow w saml/ldap 2024-04-26 20:19:43 -07:00
Tuan Dang
d0cb06d875 Merge remote-tracking branch 'origin' into groups-phase-2c 2024-04-26 09:08:30 -07:00
Tuan Dang
d42f620e1b Continue user aliases 2024-04-26 09:02:10 -07:00
Tuan Dang
71e309bbcb Merge remote-tracking branch 'origin' into groups-phase-2c 2024-04-25 17:03:23 -07:00
Tuan Dang
8ff407927c Continue merge user 2024-04-25 17:02:55 -07:00
Tuan Dang
d9005e8665 Merge remote-tracking branch 'origin' into groups-phase-2c 2024-04-25 06:50:02 -07:00
snyk-bot
c88923e0c6 fix: backend/package.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-MYSQL2-6670046
2024-04-22 17:59:21 +00:00
Tuan Dang
54fcc23a6c Begin groups phase 2b 2024-04-19 16:16:16 -07:00
160 changed files with 5991 additions and 808 deletions

View File

@@ -38,6 +38,16 @@ jobs:
rm added_files.txt
git commit -m "chore: renamed new migration files to latest timestamp (gh-action)"
- name: Get PR details
id: pr_details
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
PR_MERGER=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER" | jq -r '.merged_by.login')
echo "PR Number: $PR_NUMBER"
echo "PR Merger: $PR_MERGER"
echo "pr_merger=$PR_MERGER" >> $GITHUB_OUTPUT
- name: Create Pull Request
if: env.SKIP_RENAME != 'true'
uses: peter-evans/create-pull-request@v6
@@ -46,3 +56,4 @@ jobs:
commit-message: 'chore: renamed new migration files to latest UTC (gh-action)'
title: 'GH Action: rename new migration file timestamp'
branch-suffix: timestamp
reviewers: ${{ steps.pr_details.outputs.pr_merger }}

View File

@@ -2,4 +2,6 @@
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx:generic-api-key:206
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:304
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx:generic-api-key:206
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292
docs/self-hosting/configuration/envars.mdx:generic-api-key:106
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451

View File

@@ -110,7 +110,7 @@
"libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0",
"ms": "^2.1.3",
"mysql2": "^3.9.4",
"mysql2": "^3.9.7",
"nanoid": "^5.0.4",
"nodemailer": "^6.9.9",
"ora": "^7.0.1",

View File

@@ -1,6 +1,8 @@
import "fastify";
import { TUsers } from "@app/db/schemas";
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
@@ -113,6 +115,8 @@ declare module "fastify" {
identityAccessToken: TIdentityAccessTokenServiceFactory;
identityProject: TIdentityProjectServiceFactory;
identityUa: TIdentityUaServiceFactory;
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
secretApprovalRequest: TSecretApprovalRequestServiceFactory;
secretRotation: TSecretRotationServiceFactory;

View File

@@ -2,6 +2,18 @@ import { Knex } from "knex";
import {
TableName,
TAccessApprovalPolicies,
TAccessApprovalPoliciesApprovers,
TAccessApprovalPoliciesApproversInsert,
TAccessApprovalPoliciesApproversUpdate,
TAccessApprovalPoliciesInsert,
TAccessApprovalPoliciesUpdate,
TAccessApprovalRequests,
TAccessApprovalRequestsInsert,
TAccessApprovalRequestsReviewers,
TAccessApprovalRequestsReviewersInsert,
TAccessApprovalRequestsReviewersUpdate,
TAccessApprovalRequestsUpdate,
TApiKeys,
TApiKeysInsert,
TApiKeysUpdate,
@@ -344,6 +356,31 @@ declare module "knex/types/tables" {
TIdentityProjectAdditionalPrivilegeInsert,
TIdentityProjectAdditionalPrivilegeUpdate
>;
[TableName.AccessApprovalPolicy]: Knex.CompositeTableType<
TAccessApprovalPolicies,
TAccessApprovalPoliciesInsert,
TAccessApprovalPoliciesUpdate
>;
[TableName.AccessApprovalPolicyApprover]: Knex.CompositeTableType<
TAccessApprovalPoliciesApprovers,
TAccessApprovalPoliciesApproversInsert,
TAccessApprovalPoliciesApproversUpdate
>;
[TableName.AccessApprovalRequest]: Knex.CompositeTableType<
TAccessApprovalRequests,
TAccessApprovalRequestsInsert,
TAccessApprovalRequestsUpdate
>;
[TableName.AccessApprovalRequestReviewer]: Knex.CompositeTableType<
TAccessApprovalRequestsReviewers,
TAccessApprovalRequestsReviewersInsert,
TAccessApprovalRequestsReviewersUpdate
>;
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
TSecretApprovalPolicies,

View File

@@ -0,0 +1,54 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const isUsersTablePresent = await knex.schema.hasTable(TableName.Users);
if (isUsersTablePresent) {
const hasIsEmailVerifiedColumn = await knex.schema.hasColumn(TableName.Users, "isEmailVerified");
if (!hasIsEmailVerifiedColumn) {
await knex.schema.alterTable(TableName.Users, (t) => {
t.boolean("isEmailVerified").defaultTo(false);
});
}
// Backfilling the isEmailVerified to true where isAccepted is true
await knex(TableName.Users).update({ isEmailVerified: true }).where("isAccepted", true);
}
const isUserAliasTablePresent = await knex.schema.hasTable(TableName.UserAliases);
if (isUserAliasTablePresent) {
await knex.schema.alterTable(TableName.UserAliases, (t) => {
t.string("username").nullable().alter();
});
}
const isSuperAdminTablePresent = await knex.schema.hasTable(TableName.SuperAdmin);
if (isSuperAdminTablePresent) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.boolean("trustSamlEmails").defaultTo(false);
t.boolean("trustLdapEmails").defaultTo(false);
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.Users, "isEmailVerified")) {
await knex.schema.alterTable(TableName.Users, (t) => {
t.dropColumn("isEmailVerified");
});
}
if (await knex.schema.hasColumn(TableName.SuperAdmin, "trustSamlEmails")) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.dropColumn("trustSamlEmails");
});
}
if (await knex.schema.hasColumn(TableName.SuperAdmin, "trustLdapEmails")) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.dropColumn("trustLdapEmails");
});
}
}

View File

@@ -0,0 +1,41 @@
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.AccessApprovalPolicy))) {
await knex.schema.createTable(TableName.AccessApprovalPolicy, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name").notNullable();
t.integer("approvals").defaultTo(1).notNullable();
t.string("secretPath");
t.uuid("envId").notNullable();
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicy);
}
if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover))) {
await knex.schema.createTable(TableName.AccessApprovalPolicyApprover, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("approverId").notNullable();
t.foreign("approverId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
t.uuid("policyId").notNullable();
t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover);
await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy);
await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover);
await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy);
}

View File

@@ -0,0 +1,51 @@
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.AccessApprovalRequest))) {
await knex.schema.createTable(TableName.AccessApprovalRequest, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("policyId").notNullable();
t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE");
t.uuid("privilegeId").nullable();
t.foreign("privilegeId").references("id").inTable(TableName.ProjectUserAdditionalPrivilege).onDelete("CASCADE");
t.uuid("requestedBy").notNullable();
t.foreign("requestedBy").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
// We use these values to create the actual privilege at a later point in time.
t.boolean("isTemporary").notNullable();
t.string("temporaryRange").nullable();
t.jsonb("permissions").notNullable();
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.AccessApprovalRequest);
if (!(await knex.schema.hasTable(TableName.AccessApprovalRequestReviewer))) {
await knex.schema.createTable(TableName.AccessApprovalRequestReviewer, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("member").notNullable();
t.foreign("member").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
t.string("status").notNullable();
t.uuid("requestId").notNullable();
t.foreign("requestId").references("id").inTable(TableName.AccessApprovalRequest).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.AccessApprovalRequestReviewer);
await knex.schema.dropTableIfExists(TableName.AccessApprovalRequest);
await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer);
await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest);
}

View File

@@ -0,0 +1,25 @@
// 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 AccessApprovalPoliciesApproversSchema = z.object({
id: z.string().uuid(),
approverId: z.string().uuid(),
policyId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
export type TAccessApprovalPoliciesApproversInsert = Omit<
z.input<typeof AccessApprovalPoliciesApproversSchema>,
TImmutableDBKeys
>;
export type TAccessApprovalPoliciesApproversUpdate = Partial<
Omit<z.input<typeof AccessApprovalPoliciesApproversSchema>, TImmutableDBKeys>
>;

View File

@@ -0,0 +1,24 @@
// 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 AccessApprovalPoliciesSchema = z.object({
id: z.string().uuid(),
name: z.string(),
approvals: z.number().default(1),
envId: z.string().uuid(),
secretPath: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
export type TAccessApprovalPoliciesInsert = Omit<z.input<typeof AccessApprovalPoliciesSchema>, TImmutableDBKeys>;
export type TAccessApprovalPoliciesUpdate = Partial<
Omit<z.input<typeof AccessApprovalPoliciesSchema>, TImmutableDBKeys>
>;

View File

@@ -0,0 +1,26 @@
// 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 AccessApprovalRequestsReviewersSchema = z.object({
id: z.string().uuid(),
member: z.string().uuid(),
status: z.string(),
requestId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TAccessApprovalRequestsReviewers = z.infer<typeof AccessApprovalRequestsReviewersSchema>;
export type TAccessApprovalRequestsReviewersInsert = Omit<
z.input<typeof AccessApprovalRequestsReviewersSchema>,
TImmutableDBKeys
>;
export type TAccessApprovalRequestsReviewersUpdate = Partial<
Omit<z.input<typeof AccessApprovalRequestsReviewersSchema>, TImmutableDBKeys>
>;

View File

@@ -0,0 +1,26 @@
// 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 AccessApprovalRequestsSchema = z.object({
id: z.string().uuid(),
policyId: z.string().uuid(),
privilegeId: z.string().uuid().nullable().optional(),
requestedBy: z.string().uuid(),
isTemporary: z.boolean(),
temporaryRange: z.string().nullable().optional(),
permissions: z.unknown(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;
export type TAccessApprovalRequestsInsert = Omit<z.input<typeof AccessApprovalRequestsSchema>, TImmutableDBKeys>;
export type TAccessApprovalRequestsUpdate = Partial<
Omit<z.input<typeof AccessApprovalRequestsSchema>, TImmutableDBKeys>
>;

View File

@@ -1,3 +1,7 @@
export * from "./access-approval-policies";
export * from "./access-approval-policies-approvers";
export * from "./access-approval-requests";
export * from "./access-approval-requests-reviewers";
export * from "./api-keys";
export * from "./audit-log-streams";
export * from "./audit-logs";

View File

@@ -50,6 +50,10 @@ export enum TableName {
IdentityProjectMembershipRole = "identity_project_membership_role",
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
ScimToken = "scim_tokens",
AccessApprovalPolicy = "access_approval_policies",
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
AccessApprovalRequest = "access_approval_requests",
AccessApprovalRequestReviewer = "access_approval_requests_reviewers",
SecretApprovalPolicy = "secret_approval_policies",
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
SecretApprovalRequest = "secret_approval_requests",

View File

@@ -14,7 +14,9 @@ export const SuperAdminSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
allowedSignUpDomain: z.string().nullable().optional(),
instanceId: z.string().uuid().default("00000000-0000-0000-0000-000000000000")
instanceId: z.string().uuid().default("00000000-0000-0000-0000-000000000000"),
trustSamlEmails: z.boolean().default(false).nullable().optional(),
trustLdapEmails: z.boolean().default(false).nullable().optional()
});
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

View File

@@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
export const UserAliasesSchema = z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
username: z.string(),
username: z.string().nullable().optional(),
aliasType: z.string(),
externalId: z.string(),
emails: z.string().array().nullable().optional(),

View File

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

View File

@@ -0,0 +1,168 @@
import { nanoid } from "nanoid";
import { z } from "zod";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
method: "POST",
schema: {
body: z
.object({
projectSlug: z.string().trim(),
name: z.string().optional(),
secretPath: z.string().trim().default("/"),
environment: z.string(),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
response: {
200: z.object({
approval: sapPubSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
projectSlug: req.body.projectSlug,
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`
});
return { approval };
}
});
server.route({
url: "/",
method: "GET",
schema: {
querystring: z.object({
projectSlug: z.string().trim()
}),
response: {
200: z.object({
approvals: sapPubSchema.extend({ approvers: z.string().array(), secretPath: z.string().optional() }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const approvals = await server.services.accessApprovalPolicy.getAccessApprovalPolicyByProjectSlug({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectSlug: req.query.projectSlug
});
return { approvals };
}
});
server.route({
url: "/count",
method: "GET",
schema: {
querystring: z.object({
projectSlug: z.string(),
envSlug: z.string()
}),
response: {
200: z.object({
count: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { count } = await server.services.accessApprovalPolicy.getAccessPolicyCountByEnvSlug({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
projectSlug: req.query.projectSlug,
actorOrgId: req.permission.orgId,
envSlug: req.query.envSlug
});
return { count };
}
});
server.route({
url: "/:policyId",
method: "PATCH",
schema: {
params: z.object({
policyId: z.string()
}),
body: z
.object({
name: z.string().optional(),
secretPath: z
.string()
.trim()
.optional()
.transform((val) => (val === "" ? "/" : val)),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
response: {
200: z.object({
approval: sapPubSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({
policyId: req.params.policyId,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
...req.body
});
}
});
server.route({
url: "/:policyId",
method: "DELETE",
schema: {
params: z.object({
policyId: z.string()
}),
response: {
200: z.object({
approval: sapPubSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
policyId: req.params.policyId
});
return { approval };
}
});
};

View File

@@ -0,0 +1,160 @@
import { z } from "zod";
import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas";
import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
method: "POST",
schema: {
body: z.object({
permissions: z.any().array(),
isTemporary: z.boolean(),
temporaryRange: z.string().optional()
}),
querystring: z.object({
projectSlug: z.string().trim()
}),
response: {
200: z.object({
approval: AccessApprovalRequestsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { request } = await server.services.accessApprovalRequest.createAccessApprovalRequest({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
permissions: req.body.permissions,
actorOrgId: req.permission.orgId,
projectSlug: req.query.projectSlug,
temporaryRange: req.body.temporaryRange,
isTemporary: req.body.isTemporary
});
return { approval: request };
}
});
server.route({
url: "/count",
method: "GET",
schema: {
querystring: z.object({
projectSlug: z.string().trim()
}),
response: {
200: z.object({
pendingCount: z.number(),
finalizedCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { count } = await server.services.accessApprovalRequest.getCount({
projectSlug: req.query.projectSlug,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
return { ...count };
}
});
server.route({
url: "/",
method: "GET",
schema: {
querystring: z.object({
projectSlug: z.string().trim(),
authorProjectMembershipId: z.string().trim().optional(),
envSlug: z.string().trim().optional()
}),
response: {
200: z.object({
requests: AccessApprovalRequestsSchema.extend({
environmentName: z.string(),
isApproved: z.boolean(),
privilege: z
.object({
membershipId: z.string(),
isTemporary: z.boolean(),
temporaryMode: z.string().nullish(),
temporaryRange: z.string().nullish(),
temporaryAccessStartTime: z.date().nullish(),
temporaryAccessEndTime: z.date().nullish(),
permissions: z.unknown()
})
.nullable(),
policy: z.object({
id: z.string(),
name: z.string(),
approvals: z.number(),
approvers: z.string().array(),
secretPath: z.string().nullish(),
envId: z.string()
}),
reviewers: z
.object({
member: z.string(),
status: z.string()
})
.array()
}).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({
projectSlug: req.query.projectSlug,
authorProjectMembershipId: req.query.authorProjectMembershipId,
envSlug: req.query.envSlug,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
return { requests };
}
});
server.route({
url: "/:requestId/review",
method: "POST",
schema: {
params: z.object({
requestId: z.string().trim()
}),
body: z.object({
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
}),
response: {
200: z.object({
review: AccessApprovalRequestsReviewersSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const review = await server.services.accessApprovalRequest.reviewAccessRequest({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
requestId: req.params.requestId,
status: req.body.status
});
return { review };
}
});
};

View File

@@ -1,16 +1,14 @@
import { MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { packRules } from "@casl/ability/extra";
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
import { ProjectPermissionSet } from "@app/ee/services/permission/project-permission";
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { PermissionSchema, SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
@@ -41,11 +39,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
}),
response: {
200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema
privilege: SanitizedIdentityPrivilegeSchema
})
}
},
@@ -92,7 +90,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions),
permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions),
temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
@@ -107,7 +105,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}),
response: {
200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema
privilege: SanitizedIdentityPrivilegeSchema
})
}
},
@@ -157,7 +155,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
message: "Slug must be a valid slug"
})
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
@@ -175,7 +173,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}),
response: {
200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema
privilege: SanitizedIdentityPrivilegeSchema
})
}
},
@@ -219,7 +217,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}),
response: {
200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema
privilege: SanitizedIdentityPrivilegeSchema
})
}
},
@@ -260,7 +258,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}),
response: {
200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema
privilege: SanitizedIdentityPrivilegeSchema
})
}
},
@@ -293,16 +291,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
],
querystring: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.projectSlug),
unpacked: z
.enum(["false", "true"])
.transform((el) => el === "true")
.default("true")
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.unpacked)
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.projectSlug)
}),
response: {
200: z.object({
privileges: IdentityProjectAdditionalPrivilegeSchema.array()
privileges: SanitizedIdentityPrivilegeSchema.array()
})
}
},
@@ -315,15 +308,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
actorOrgId: req.permission.orgId,
...req.query
});
if (req.query.unpacked) {
return {
privileges: privileges.map(({ permissions, ...el }) => ({
...el,
permissions: unpackRules(permissions as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
}))
};
}
return { privileges };
return {
privileges
};
}
});
};

View File

@@ -1,3 +1,5 @@
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
@@ -41,6 +43,9 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
prefix: "/secret-rotation-providers"
});
await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals/policies" });
await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approvals/requests" });
await server.register(
async (dynamicSecretRouter) => {
await dynamicSecretRouter.register(registerDynamicSecretRouter);

View File

@@ -18,6 +18,7 @@ import { LdapConfigsSchema, LdapGroupMapsSchema } from "@app/db/schemas";
import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types";
import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -52,6 +53,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
// eslint-disable-next-line
async (req: IncomingMessage, user, cb) => {
try {
if (!user.email) throw new BadRequestError({ message: "Invalid request. Missing email." });
const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig;
let groups: { dn: string; cn: string }[] | undefined;
@@ -74,7 +76,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
username: user.uid,
firstName: user.givenName ?? user.cn ?? "",
lastName: user.sn ?? "",
emails: user.mail ? [user.mail] : [],
email: user.mail,
groups,
relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState,
orgId: (req as unknown as FastifyRequest).ldapConfig.organization

View File

@@ -102,12 +102,12 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
if (!profile) throw new BadRequestError({ message: "Missing profile" });
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
if (!profile.email || !profile.firstName) {
if (!email || !profile.firstName) {
throw new BadRequestError({ message: "Invalid request. Missing email or first name" });
}
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
username: profile.nameID ?? email,
externalId: profile.nameID,
email,
firstName: profile.firstName as string,
lastName: profile.lastName as string,

View File

@@ -153,7 +153,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const users = await req.server.services.scim.listScimUsers({
offset: req.query.startIndex,
startIndex: req.query.startIndex,
limit: req.query.count,
filter: req.query.filter,
orgId: req.permission.orgId
@@ -163,11 +163,11 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
});
server.route({
url: "/Users/:userId",
url: "/Users/:orgMembershipId",
method: "GET",
schema: {
params: z.object({
userId: z.string().trim()
orgMembershipId: z.string().trim()
}),
response: {
201: z.object({
@@ -193,7 +193,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.getScimUser({
userId: req.params.userId,
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId
});
return user;
@@ -249,7 +249,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
const primaryEmail = req.body.emails?.find((email) => email.primary)?.value;
const user = await req.server.services.scim.createScimUser({
username: req.body.userName,
externalId: req.body.userName,
email: primaryEmail,
firstName: req.body.name.givenName,
lastName: req.body.name.familyName,
@@ -261,11 +261,11 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
});
server.route({
url: "/Users/:userId",
url: "/Users/:orgMembershipId",
method: "DELETE",
schema: {
params: z.object({
userId: z.string().trim()
orgMembershipId: z.string().trim()
}),
response: {
200: z.object({})
@@ -274,7 +274,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.deleteScimUser({
userId: req.params.userId,
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId
});
@@ -361,7 +361,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
const groups = await req.server.services.scim.listScimGroups({
orgId: req.permission.orgId,
offset: req.query.startIndex,
startIndex: req.query.startIndex,
limit: req.query.count
});
@@ -416,10 +416,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(), // infisical userId
value: z.string(), // infisical orgMembershipId
display: z.string()
})
) // note: is this where members are added to group?
)
}),
response: {
200: z.object({
@@ -534,11 +534,11 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
});
server.route({
url: "/Users/:userId",
url: "/Users/:orgMembershipId",
method: "PUT",
schema: {
params: z.object({
userId: z.string().trim()
orgMembershipId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
@@ -575,7 +575,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.replaceScimUser({
userId: req.params.userId,
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId,
active: req.body.active
});

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 TAccessApprovalPolicyApproverDALFactory = ReturnType<typeof accessApprovalPolicyApproverDALFactory>;
export const accessApprovalPolicyApproverDALFactory = (db: TDbClient) => {
const accessApprovalPolicyApproverOrm = ormify(db, TableName.AccessApprovalPolicyApprover);
return { ...accessApprovalPolicyApproverOrm };
};

View File

@@ -0,0 +1,76 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TAccessApprovalPolicies } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, mergeOneToManyRelation, ormify, selectAllTableCols, TFindFilter } from "@app/lib/knex";
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy);
const accessApprovalPolicyFindQuery = async (tx: Knex, filter: TFindFilter<TAccessApprovalPolicies>) => {
const result = await tx(TableName.AccessApprovalPolicy)
// eslint-disable-next-line
.where(buildFindFilter(filter))
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.join(
TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId`
)
.select(tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
.select(tx.ref("projectId").withSchema(TableName.Environment))
.select(selectAllTableCols(TableName.AccessApprovalPolicy));
return result;
};
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await accessApprovalPolicyFindQuery(tx || db, {
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id
});
const formatedDoc = mergeOneToManyRelation(
doc,
"id",
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
...el,
envId,
environment: { id: envId, name, slug }
}),
({ approverId }) => approverId,
"approvers"
);
return formatedDoc?.[0];
} catch (error) {
throw new DatabaseError({ error, name: "FindById" });
}
};
const find = async (filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, tx?: Knex) => {
try {
const docs = await accessApprovalPolicyFindQuery(tx || db, filter);
const formatedDoc = mergeOneToManyRelation(
docs,
"id",
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
...el,
envId,
environment: { id: envId, name, slug }
}),
({ approverId }) => approverId,
"approvers"
);
return formatedDoc.map((policy) => ({ ...policy, secretPath: policy.secretPath || undefined }));
} catch (error) {
throw new DatabaseError({ error, name: "Find" });
}
};
return { ...accessApprovalPolicyOrm, find, findById };
};

View File

@@ -0,0 +1,36 @@
import { ForbiddenError, subject } from "@casl/ability";
import { BadRequestError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TVerifyApprovers } from "./access-approval-policy-types";
export const verifyApprovers = async ({
userIds,
projectId,
orgId,
envSlug,
actorAuthMethod,
secretPath,
permissionService
}: TVerifyApprovers) => {
for await (const userId of userIds) {
try {
const { permission: approverPermission } = await permissionService.getProjectPermission(
ActorType.USER,
userId,
projectId,
actorAuthMethod,
orgId
);
ForbiddenError.from(approverPermission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath })
);
} catch (err) {
throw new BadRequestError({ message: "One or more approvers doesn't have access to be specified secret path" });
}
}
};

View File

@@ -0,0 +1,273 @@
import { ForbiddenError } from "@casl/ability";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
import { verifyApprovers } from "./access-approval-policy-fns";
import {
TCreateAccessApprovalPolicy,
TDeleteAccessApprovalPolicy,
TGetAccessPolicyCountByEnvironmentDTO,
TListAccessApprovalPoliciesDTO,
TUpdateAccessApprovalPolicy
} from "./access-approval-policy-types";
type TSecretApprovalPolicyServiceFactoryDep = {
projectDAL: TProjectDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
};
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
permissionService,
projectEnvDAL,
projectDAL,
projectMembershipDAL
}: TSecretApprovalPolicyServiceFactoryDep) => {
const createAccessApprovalPolicy = async ({
name,
actor,
actorId,
actorOrgId,
secretPath,
actorAuthMethod,
approvals,
approvers,
projectSlug,
environment
}: TCreateAccessApprovalPolicy) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
if (approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval
);
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
if (!env) throw new BadRequestError({ message: "Environment not found" });
const secretApprovers = await projectMembershipDAL.find({
projectId: project.id,
$in: { id: approvers }
});
if (secretApprovers.length !== approvers.length) {
throw new BadRequestError({ message: "Approver not found in project" });
}
await verifyApprovers({
projectId: project.id,
orgId: actorOrgId,
envSlug: environment,
secretPath,
actorAuthMethod,
permissionService,
userIds: secretApprovers.map((approver) => approver.userId)
});
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.create(
{
envId: env.id,
approvals,
secretPath,
name
},
tx
);
await accessApprovalPolicyApproverDAL.insertMany(
secretApprovers.map(({ id }) => ({
approverId: id,
policyId: doc.id
})),
tx
);
return doc;
});
return { ...accessApproval, environment: env, projectId: project.id };
};
const getAccessApprovalPolicyByProjectSlug = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectSlug
}: TListAccessApprovalPoliciesDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
// Anyone in the project should be able to get the policies.
/* const { permission } = */ await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
// ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id });
return accessApprovalPolicies;
};
const updateAccessApprovalPolicy = async ({
policyId,
approvers,
secretPath,
name,
actorId,
actor,
actorOrgId,
actorAuthMethod,
approvals
}: TUpdateAccessApprovalPolicy) => {
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
accessApprovalPolicy.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.updateById(
accessApprovalPolicy.id,
{
approvals,
secretPath,
name
},
tx
);
if (approvers) {
// Find the workspace project memberships of the users passed in the approvers array
const secretApprovers = await projectMembershipDAL.find(
{
projectId: accessApprovalPolicy.projectId,
$in: { id: approvers }
},
{ tx }
);
await verifyApprovers({
projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId,
envSlug: accessApprovalPolicy.environment.slug,
secretPath: doc.secretPath!,
actorAuthMethod,
permissionService,
userIds: secretApprovers.map((approver) => approver.userId)
});
if (secretApprovers.length !== approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await accessApprovalPolicyApproverDAL.insertMany(
secretApprovers.map(({ id }) => ({
approverId: id,
policyId: doc.id
})),
tx
);
}
return doc;
});
return {
...updatedPolicy,
environment: accessApprovalPolicy.environment,
projectId: accessApprovalPolicy.projectId
};
};
const deleteAccessApprovalPolicy = async ({
policyId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TDeleteAccessApprovalPolicy) => {
const policy = await accessApprovalPolicyDAL.findById(policyId);
if (!policy) throw new BadRequestError({ message: "Secret approval policy not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
policy.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretApproval
);
await accessApprovalPolicyDAL.deleteById(policyId);
return policy;
};
const getAccessPolicyCountByEnvSlug = async ({
actor,
actorOrgId,
actorAuthMethod,
projectSlug,
actorId,
envSlug
}: TGetAccessPolicyCountByEnvironmentDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const { membership } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
if (!membership) throw new BadRequestError({ message: "User not found in project" });
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
if (!environment) throw new BadRequestError({ message: "Environment not found" });
const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id });
if (!policies) throw new BadRequestError({ message: "No policies found" });
return { count: policies.length };
};
return {
getAccessPolicyCountByEnvSlug,
createAccessApprovalPolicy,
deleteAccessApprovalPolicy,
updateAccessApprovalPolicy,
getAccessApprovalPolicyByProjectSlug
};
};

View File

@@ -0,0 +1,44 @@
import { TProjectPermission } from "@app/lib/types";
import { ActorAuthMethod } from "@app/services/auth/auth-type";
import { TPermissionServiceFactory } from "../permission/permission-service";
export type TVerifyApprovers = {
userIds: string[];
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
envSlug: string;
actorAuthMethod: ActorAuthMethod;
secretPath: string;
projectId: string;
orgId: string;
};
export type TCreateAccessApprovalPolicy = {
approvals: number;
secretPath: string;
environment: string;
approvers: string[];
projectSlug: string;
name: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateAccessApprovalPolicy = {
policyId: string;
approvals?: number;
approvers?: string[];
secretPath?: string;
name?: string;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteAccessApprovalPolicy = {
policyId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetAccessPolicyCountByEnvironmentDTO = {
envSlug: string;
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TListAccessApprovalPoliciesDTO = {
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -0,0 +1,266 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { AccessApprovalRequestsSchema, TableName, TAccessApprovalRequests } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
import { ApprovalStatus } from "./access-approval-request-types";
export type TAccessApprovalRequestDALFactory = ReturnType<typeof accessApprovalRequestDALFactory>;
export const accessApprovalRequestDALFactory = (db: TDbClient) => {
const accessApprovalRequestOrm = ormify(db, TableName.AccessApprovalRequest);
const findRequestsWithPrivilegeByPolicyIds = async (policyIds: string[]) => {
try {
const docs = await db(TableName.AccessApprovalRequest)
.whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds)
.leftJoin(
TableName.ProjectUserAdditionalPrivilege,
`${TableName.AccessApprovalRequest}.privilegeId`,
`${TableName.ProjectUserAdditionalPrivilege}.id`
)
.leftJoin(
TableName.AccessApprovalPolicy,
`${TableName.AccessApprovalRequest}.policyId`,
`${TableName.AccessApprovalPolicy}.id`
)
.leftJoin(
TableName.AccessApprovalRequestReviewer,
`${TableName.AccessApprovalRequest}.id`,
`${TableName.AccessApprovalRequestReviewer}.requestId`
)
.leftJoin(
TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId`
)
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(
db.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"),
db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId")
)
.select(db.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(
db.ref("projectId").withSchema(TableName.Environment),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName")
)
.select(
db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"),
db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")
)
.select(
db
.ref("projectMembershipId")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("privilegeMembershipId"),
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"),
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"),
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("privilegeTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("privilegeTemporaryAccessEndTime"),
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegePermissions")
)
.orderBy(`${TableName.AccessApprovalRequest}.createdAt`, "desc");
const formattedDocs = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (doc) => ({
...AccessApprovalRequestsSchema.parse(doc),
projectId: doc.projectId,
environment: doc.envSlug,
environmentName: doc.envName,
policy: {
id: doc.policyId,
name: doc.policyName,
approvals: doc.policyApprovals,
secretPath: doc.policySecretPath,
envId: doc.policyEnvId
},
privilege: doc.privilegeId
? {
membershipId: doc.privilegeMembershipId,
isTemporary: doc.privilegeIsTemporary,
temporaryMode: doc.privilegeTemporaryMode,
temporaryRange: doc.privilegeTemporaryRange,
temporaryAccessStartTime: doc.privilegeTemporaryAccessStartTime,
temporaryAccessEndTime: doc.privilegeTemporaryAccessEndTime,
permissions: doc.privilegePermissions
}
: null,
isApproved: !!doc.privilegeId
}),
childrenMapper: [
{
key: "reviewerMemberId",
label: "reviewers" as const,
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
},
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
]
});
if (!formattedDocs) return [];
return formattedDocs.map((doc) => ({
...doc,
policy: { ...doc.policy, approvers: doc.approvers }
}));
} catch (error) {
throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" });
}
};
const findQuery = (filter: TFindFilter<TAccessApprovalRequests>, tx: Knex) =>
tx(TableName.AccessApprovalRequest)
.where(filter)
.join(
TableName.AccessApprovalPolicy,
`${TableName.AccessApprovalRequest}.policyId`,
`${TableName.AccessApprovalPolicy}.id`
)
.join(
TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId`
)
.leftJoin(
TableName.AccessApprovalRequestReviewer,
`${TableName.AccessApprovalRequest}.id`,
`${TableName.AccessApprovalRequestReviewer}.requestId`
)
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(
tx.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"),
tx.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"),
tx.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"),
tx.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
tx.ref("projectId").withSchema(TableName.Environment),
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover)
);
const findById = async (id: string, tx?: Knex) => {
try {
const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db);
const docs = await sql;
const formatedDoc = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({
...AccessApprovalRequestsSchema.parse(el),
projectId: el.projectId,
environment: el.environment,
policy: {
id: el.policyId,
name: el.policyName,
approvals: el.policyApprovals,
secretPath: el.policySecretPath
}
}),
childrenMapper: [
{
key: "reviewerMemberId",
label: "reviewers" as const,
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
},
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
]
});
if (!formatedDoc?.[0]) return;
return {
...formatedDoc[0],
policy: { ...formatedDoc[0].policy, approvers: formatedDoc[0].approvers }
};
} catch (error) {
throw new DatabaseError({ error, name: "FindByIdAccessApprovalRequest" });
}
};
const getCount = async ({ projectId }: { projectId: string }) => {
try {
const accessRequests = await db(TableName.AccessApprovalRequest)
.leftJoin(
TableName.AccessApprovalPolicy,
`${TableName.AccessApprovalRequest}.policyId`,
`${TableName.AccessApprovalPolicy}.id`
)
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.leftJoin(
TableName.ProjectUserAdditionalPrivilege,
`${TableName.AccessApprovalRequest}.privilegeId`,
`${TableName.ProjectUserAdditionalPrivilege}.id`
)
.leftJoin(
TableName.AccessApprovalRequestReviewer,
`${TableName.AccessApprovalRequest}.id`,
`${TableName.AccessApprovalRequestReviewer}.requestId`
)
.where(`${TableName.Environment}.projectId`, projectId)
.select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
.select(db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"));
const formattedRequests = sqlNestRelationships({
data: accessRequests,
key: "id",
parentMapper: (doc) => ({
...AccessApprovalRequestsSchema.parse(doc)
}),
childrenMapper: [
{
key: "reviewerMemberId",
label: "reviewers" as const,
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
}
]
});
// an approval is pending if there is no reviewer rejections and no privilege ID is set
const pendingApprovals = formattedRequests.filter(
(req) => !req.privilegeId && !req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED)
);
// an approval is finalized if there are any rejections or a privilege ID is set
const finalizedApprovals = formattedRequests.filter(
(req) => req.privilegeId || req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED)
);
return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length };
} catch (error) {
throw new DatabaseError({ error, name: "GetCountAccessApprovalRequest" });
}
};
return { ...accessApprovalRequestOrm, findById, findRequestsWithPrivilegeByPolicyIds, getCount };
};

View File

@@ -0,0 +1,53 @@
import { PackRule, unpackRules } from "@casl/ability/extra";
import { UnauthorizedError } from "@app/lib/errors";
import { TVerifyPermission } from "./access-approval-request-types";
function filterUnique(value: string, index: number, array: string[]) {
return array.indexOf(value) === index;
}
export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) => {
const permission = unpackRules(
permissions as PackRule<{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
conditions?: Record<string, any>;
action: string;
subject: [string];
}>[]
);
if (!permission || !permission.length) {
throw new UnauthorizedError({ message: "No permission provided" });
}
const requestedPermissions: string[] = [];
for (const p of permission) {
if (p.action[0] === "read") requestedPermissions.push("Read Access");
if (p.action[0] === "create") requestedPermissions.push("Create Access");
if (p.action[0] === "delete") requestedPermissions.push("Delete Access");
if (p.action[0] === "edit") requestedPermissions.push("Edit Access");
}
const firstPermission = permission[0];
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const permissionSecretPath = firstPermission.conditions?.secretPath?.$glob;
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment
const permissionEnv = firstPermission.conditions?.environment;
if (!permissionEnv || typeof permissionEnv !== "string") {
throw new UnauthorizedError({ message: "Permission environment is not a string" });
}
if (!permissionSecretPath || typeof permissionSecretPath !== "string") {
throw new UnauthorizedError({ message: "Permission path is not a string" });
}
return {
envSlug: permissionEnv,
secretPath: permissionSecretPath,
accessTypes: requestedPermissions.filter(filterUnique)
};
};

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 TAccessApprovalRequestReviewerDALFactory = ReturnType<typeof accessApprovalRequestReviewerDALFactory>;
export const accessApprovalRequestReviewerDALFactory = (db: TDbClient) => {
const secretApprovalRequestReviewerOrm = ormify(db, TableName.AccessApprovalRequestReviewer);
return secretApprovalRequestReviewerOrm;
};

View File

@@ -0,0 +1,369 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { ProjectMembershipRole } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-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 { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal";
import { verifyRequestedPermissions } from "./access-approval-request-fns";
import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal";
import {
ApprovalStatus,
TCreateAccessApprovalRequestDTO,
TGetAccessRequestCountDTO,
TListApprovalRequestsDTO,
TReviewAccessRequestDTO
} from "./access-approval-request-types";
type TSecretApprovalRequestServiceFactoryDep = {
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "create" | "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
accessApprovalPolicyApproverDAL: Pick<TAccessApprovalPolicyApproverDALFactory, "find">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
accessApprovalRequestDAL: Pick<
TAccessApprovalRequestDALFactory,
| "create"
| "find"
| "findRequestsWithPrivilegeByPolicyIds"
| "findById"
| "transaction"
| "updateById"
| "findOne"
| "getCount"
>;
accessApprovalPolicyDAL: Pick<TAccessApprovalPolicyDALFactory, "findOne" | "find">;
accessApprovalRequestReviewerDAL: Pick<
TAccessApprovalRequestReviewerDALFactory,
"create" | "find" | "findOne" | "transaction"
>;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "findUserByProjectMembershipId" | "findUsersByProjectMembershipIds">;
};
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
export const accessApprovalRequestServiceFactory = ({
projectDAL,
projectEnvDAL,
permissionService,
accessApprovalRequestDAL,
accessApprovalRequestReviewerDAL,
projectMembershipDAL,
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
additionalPrivilegeDAL,
smtpService,
userDAL
}: TSecretApprovalRequestServiceFactoryDep) => {
const createAccessApprovalRequest = async ({
isTemporary,
temporaryRange,
actorId,
permissions: requestedPermissions,
actor,
actorOrgId,
actorAuthMethod,
projectSlug
}: TCreateAccessApprovalRequestDTO) => {
const cfg = getConfig();
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new UnauthorizedError({ message: "Project not found" });
// Anyone can create an access approval request.
const { membership } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
const requestedByUser = await userDAL.findUserByProjectMembershipId(membership.id);
if (!requestedByUser) throw new UnauthorizedError({ message: "User not found" });
await projectDAL.checkProjectUpgradeStatus(project.id);
const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({ permissions: requestedPermissions });
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
if (!environment) throw new UnauthorizedError({ message: "Environment not found" });
const policy = await accessApprovalPolicyDAL.findOne({
envId: environment.id,
secretPath
});
if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." });
const approvers = await accessApprovalPolicyApproverDAL.find({
policyId: policy.id
});
const approverUsers = await userDAL.findUsersByProjectMembershipIds(
approvers.map((approver) => approver.approverId)
);
const duplicateRequests = await accessApprovalRequestDAL.find({
policyId: policy.id,
requestedBy: membership.id,
permissions: JSON.stringify(requestedPermissions),
isTemporary
});
if (duplicateRequests?.length > 0) {
for await (const duplicateRequest of duplicateRequests) {
if (duplicateRequest.privilegeId) {
const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId);
const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string));
if (!isExpired || !privilege.isTemporary) {
throw new BadRequestError({ message: "You already have an active privilege with the same criteria" });
}
} else {
const reviewers = await accessApprovalRequestReviewerDAL.find({
requestId: duplicateRequest.id
});
const isRejected = reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED);
if (!isRejected) {
throw new BadRequestError({ message: "You already have a pending access request with the same criteria" });
}
}
}
}
const approval = await accessApprovalRequestDAL.transaction(async (tx) => {
const approvalRequest = await accessApprovalRequestDAL.create(
{
policyId: policy.id,
requestedBy: membership.id,
temporaryRange: temporaryRange || null,
permissions: JSON.stringify(requestedPermissions),
isTemporary
},
tx
);
await smtpService.sendMail({
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
subjectLine: "Access Approval Request",
substitutions: {
projectName: project.name,
requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`,
requesterEmail: requestedByUser.email,
isTemporary,
...(isTemporary && {
expiresIn: ms(ms(temporaryRange || ""), { long: true })
}),
secretPath,
environment: envSlug,
permissions: accessTypes,
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
},
template: SmtpTemplates.AccessApprovalRequest
});
return approvalRequest;
});
return { request: approval };
};
const listApprovalRequests = async ({
projectSlug,
authorProjectMembershipId,
envSlug,
actor,
actorOrgId,
actorId,
actorAuthMethod
}: TListApprovalRequestsDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new UnauthorizedError({ message: "Project not found" });
const { membership } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
if (authorProjectMembershipId) {
requests = requests.filter((request) => request.requestedBy === authorProjectMembershipId);
}
if (envSlug) {
requests = requests.filter((request) => request.environment === envSlug);
}
return { requests };
};
const reviewAccessRequest = async ({
requestId,
actor,
status,
actorId,
actorAuthMethod,
actorOrgId
}: TReviewAccessRequestDTO) => {
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
if (!accessApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
const { policy } = accessApprovalRequest;
const { membership, hasRole } = await permissionService.getProjectPermission(
actor,
actorId,
accessApprovalRequest.projectId,
actorAuthMethod,
actorOrgId
);
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
if (
!hasRole(ProjectMembershipRole.Admin) &&
accessApprovalRequest.requestedBy !== membership.id && // The request wasn't made by the current user
!policy.approvers.find((approverId) => approverId === membership.id) // The request isn't performed by an assigned approver
) {
throw new UnauthorizedError({ message: "You are not authorized to approve this request" });
}
const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id);
await verifyApprovers({
projectId: accessApprovalRequest.projectId,
orgId: actorOrgId,
envSlug: accessApprovalRequest.environment,
secretPath: accessApprovalRequest.policy.secretPath!,
actorAuthMethod,
permissionService,
userIds: [reviewerProjectMembership.userId]
});
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
}
const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => {
const review = await accessApprovalRequestReviewerDAL.findOne(
{
requestId: accessApprovalRequest.id,
member: membership.id
},
tx
);
if (!review) {
const newReview = await accessApprovalRequestReviewerDAL.create(
{
status,
requestId: accessApprovalRequest.id,
member: membership.id
},
tx
);
const allReviews = [...existingReviews, newReview];
const approvedReviews = allReviews.filter((r) => r.status === ApprovalStatus.APPROVED);
// approvals is the required number of approvals. If the number of approved reviews is equal to the number of required approvals, then the request is approved.
if (approvedReviews.length === policy.approvals) {
if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
throw new BadRequestError({ message: "Temporary range is required for temporary access" });
}
let privilegeId: string | null = null;
if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
// Permanent access
const privilege = await additionalPrivilegeDAL.create(
{
projectMembershipId: accessApprovalRequest.requestedBy,
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
permissions: JSON.stringify(accessApprovalRequest.permissions)
},
tx
);
privilegeId = privilege.id;
} else {
// Temporary access
const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!);
const startTime = new Date();
const privilege = await additionalPrivilegeDAL.create(
{
projectMembershipId: accessApprovalRequest.requestedBy,
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
permissions: JSON.stringify(accessApprovalRequest.permissions),
isTemporary: true,
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative,
temporaryRange: accessApprovalRequest.temporaryRange!,
temporaryAccessStartTime: startTime,
temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs)
},
tx
);
privilegeId = privilege.id;
}
await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { privilegeId }, tx);
}
return newReview;
}
throw new BadRequestError({ message: "You have already reviewed this request" });
});
return reviewStatus;
};
const getCount = async ({ projectSlug, actor, actorAuthMethod, actorId, actorOrgId }: TGetAccessRequestCountDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new UnauthorizedError({ message: "Project not found" });
const { membership } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
if (!membership) throw new BadRequestError({ message: "User not found in project" });
const count = await accessApprovalRequestDAL.getCount({ projectId: project.id });
return { count };
};
return {
createAccessApprovalRequest,
listApprovalRequests,
reviewAccessRequest,
getCount
};
};

View File

@@ -0,0 +1,33 @@
import { TProjectPermission } from "@app/lib/types";
export enum ApprovalStatus {
PENDING = "pending",
APPROVED = "approved",
REJECTED = "rejected"
}
export type TVerifyPermission = {
permissions: unknown;
};
export type TGetAccessRequestCountDTO = {
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TReviewAccessRequestDTO = {
requestId: string;
status: ApprovalStatus;
} & Omit<TProjectPermission, "projectId">;
export type TCreateAccessApprovalRequestDTO = {
projectSlug: string;
permissions: unknown;
isTemporary: boolean;
temporaryRange?: string;
} & Omit<TProjectPermission, "projectId">;
export type TListApprovalRequestsDTO = {
projectSlug: string;
authorProjectMembershipId?: string;
envSlug?: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -1,6 +1,6 @@
import { Knex } from "knex";
import { SecretKeyEncoding, TUsers } from "@app/db/schemas";
import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ScimRequestError } from "@app/lib/errors";
@@ -188,9 +188,9 @@ export const addUsersToGroupByUserIds = async ({
// check if all user(s) are part of the organization
const existingUserOrgMemberships = await orgDAL.findMembership(
{
orgId: group.orgId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: group.orgId,
$in: {
userId: userIds
[`${TableName.OrgMembership}.userId` as "userId"]: userIds
}
},
{ tx }

View File

@@ -1,5 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, unpackRules } from "@casl/ability/extra";
import ms from "ms";
import { z } from "zod";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
@@ -8,7 +10,7 @@ import { TIdentityProjectDALFactory } from "@app/services/identity-project/ident
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission";
import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal";
import {
IdentityProjectAdditionalPrivilegeTemporaryMode,
@@ -30,6 +32,27 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
typeof identityProjectAdditionalPrivilegeServiceFactory
>;
// TODO(akhilmhdh): move this to more centralized
export const UnpackedPermissionSchema = z.object({
subject: z.union([z.string().min(1), z.string().array()]).optional(),
action: z.union([z.string().min(1), z.string().array()]),
conditions: z
.object({
environment: z.string().optional(),
secretPath: z
.object({
$glob: z.string().min(1)
})
.optional()
})
.optional()
});
const unpackPermissions = (permissions: unknown) =>
UnpackedPermissionSchema.array().parse(
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
);
export const identityProjectAdditionalPrivilegeServiceFactory = ({
identityProjectAdditionalPrivilegeDAL,
identityProjectDAL,
@@ -86,7 +109,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
slug,
permissions: customPermission
});
return additionalPrivilege;
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
@@ -100,7 +126,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
});
return additionalPrivilege;
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
};
const updateBySlug = async ({
@@ -163,7 +192,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
});
return additionalPrivilege;
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
@@ -174,7 +207,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
temporaryRange: null,
temporaryMode: null
});
return additionalPrivilege;
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
};
const deleteBySlug = async ({
@@ -220,7 +257,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
return deletedPrivilege;
return {
...deletedPrivilege,
permissions: unpackPermissions(deletedPrivilege.permissions)
};
};
const getPrivilegeDetailsBySlug = async ({
@@ -254,7 +295,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
});
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
return identityPrivilege;
return {
...identityPrivilege,
permissions: unpackPermissions(identityPrivilege.permissions)
};
};
const listIdentityProjectPrivileges = async ({
@@ -284,7 +328,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({
projectMembershipId: identityProjectMembership.id
});
return identityPrivileges;
return identityPrivileges.map((el) => ({
...el,
permissions: unpackPermissions(el.permissions)
}));
};
return {

View File

@@ -1,7 +1,14 @@
import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TLdapConfigsUpdate } from "@app/db/schemas";
import {
OrgMembershipRole,
OrgMembershipStatus,
SecretKeyEncoding,
TableName,
TLdapConfigsUpdate,
TUsers
} from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@@ -19,12 +26,15 @@ import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
@@ -46,6 +56,7 @@ import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal";
type TLdapConfigServiceFactoryDep = {
ldapConfigDAL: Pick<TLdapConfigDALFactory, "create" | "update" | "findOne">;
ldapGroupMapDAL: Pick<TLdapGroupMapDALFactory, "find" | "create" | "delete" | "findLdapGroupMapsByLdapConfigId">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
@@ -75,6 +86,7 @@ export const ldapConfigServiceFactory = ({
ldapConfigDAL,
ldapGroupMapDAL,
orgDAL,
orgMembershipDAL,
orgBotDAL,
groupDAL,
groupProjectDAL,
@@ -379,16 +391,17 @@ export const ldapConfigServiceFactory = ({
username,
firstName,
lastName,
emails,
email,
groups,
orgId,
relayState
}: TLdapLoginDTO) => {
const appCfg = getConfig();
const serverCfg = await getServerCfg();
let userAlias = await userAliasDAL.findOne({
externalId,
orgId,
aliasType: AuthMethod.LDAP
aliasType: UserAliasType.LDAP
});
const organization = await orgDAL.findOrgById(orgId);
@@ -396,7 +409,13 @@ export const ldapConfigServiceFactory = ({
if (userAlias) {
await userDAL.transaction(async (tx) => {
const [orgMembership] = await orgDAL.findMembership({ userId: userAlias.userId }, { tx });
const [orgMembership] = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.userId` as "userId"]: userAlias.userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
{ tx }
);
if (!orgMembership) {
await orgDAL.createMembership(
{
@@ -419,40 +438,75 @@ export const ldapConfigServiceFactory = ({
});
} else {
userAlias = await userDAL.transaction(async (tx) => {
const uniqueUsername = await normalizeUsername(username, userDAL);
const newUser = await userDAL.create(
{
username: uniqueUsername,
email: emails[0],
firstName,
lastName,
authMethods: [AuthMethod.LDAP],
isGhost: false
},
tx
);
let newUser: TUsers | undefined;
if (serverCfg.trustSamlEmails) {
newUser = await userDAL.findOne(
{
email,
isEmailVerified: true
},
tx
);
}
if (!newUser) {
const uniqueUsername = await normalizeUsername(username, userDAL);
newUser = await userDAL.create(
{
username: serverCfg.trustLdapEmails ? email : uniqueUsername,
email,
isEmailVerified: serverCfg.trustLdapEmails,
firstName,
lastName,
authMethods: [],
isGhost: false
},
tx
);
}
const newUserAlias = await userAliasDAL.create(
{
userId: newUser.id,
username,
aliasType: AuthMethod.LDAP,
aliasType: UserAliasType.LDAP,
externalId,
emails,
emails: [email],
orgId
},
tx
);
await orgDAL.createMembership(
const [orgMembership] = await orgDAL.findMembership(
{
userId: newUser.id,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
[`${TableName.OrgMembership}.userId` as "userId"]: newUser.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
tx
{ tx }
);
if (!orgMembership) {
await orgMembershipDAL.create(
{
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
},
tx
);
// Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && newUser.isAccepted) {
await orgDAL.updateMembershipById(
orgMembership.id,
{
status: OrgMembershipStatus.Accepted
},
tx
);
}
return newUserAlias;
});
}
@@ -543,11 +597,14 @@ export const ldapConfigServiceFactory = ({
authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id,
username: user.username,
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
firstName,
lastName,
organizationName: organization.name,
organizationId: organization.id,
organizationSlug: organization.slug,
authMethod: AuthMethod.LDAP,
authType: UserAliasType.LDAP,
isUserCompleted,
...(relayState
? {

View File

@@ -51,7 +51,7 @@ export type TLdapLoginDTO = {
username: string;
firstName: string;
lastName: string;
emails: string[];
email: string;
orgId: string;
groups?: {
dn: string;

View File

@@ -121,8 +121,8 @@ export const licenseServiceFactory = ({
if (isValidOfflineLicense) {
onPremFeatures = contents.license.features;
instanceType = InstanceType.EnterpriseOnPrem;
logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`);
instanceType = InstanceType.EnterpriseOnPremOffline;
logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`);
isValidLicense = true;
return;
}

View File

@@ -3,6 +3,7 @@ import { TOrgPermission } from "@app/lib/types";
export enum InstanceType {
OnPrem = "self-hosted",
EnterpriseOnPrem = "enterprise-self-hosted",
EnterpriseOnPremOffline = "enterprise-self-hosted-offline",
Cloud = "cloud"
}

View File

@@ -7,7 +7,8 @@ import {
SecretKeyEncoding,
TableName,
TSamlConfigs,
TSamlConfigsUpdate
TSamlConfigsUpdate,
TUsers
} from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import {
@@ -19,10 +20,18 @@ import {
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
@@ -31,15 +40,19 @@ import { TSamlConfigDALFactory } from "./saml-config-dal";
import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } from "./saml-config-types";
type TSamlConfigServiceFactoryDep = {
samlConfigDAL: TSamlConfigDALFactory;
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById">;
samlConfigDAL: Pick<TSamlConfigDALFactory, "create" | "findOne" | "update" | "findById">;
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById" | "findById">;
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
>;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">;
};
export type TSamlConfigServiceFactory = ReturnType<typeof samlConfigServiceFactory>;
@@ -48,9 +61,13 @@ export const samlConfigServiceFactory = ({
samlConfigDAL,
orgBotDAL,
orgDAL,
orgMembershipDAL,
userDAL,
userAliasDAL,
permissionService,
licenseService
licenseService,
tokenService,
smtpService
}: TSamlConfigServiceFactoryDep) => {
const createSamlCfg = async ({
cert,
@@ -305,7 +322,7 @@ export const samlConfigServiceFactory = ({
};
const samlLogin = async ({
username,
externalId,
email,
firstName,
lastName,
@@ -314,38 +331,40 @@ export const samlConfigServiceFactory = ({
relayState
}: TSamlLoginDTO) => {
const appCfg = getConfig();
let user = await userDAL.findOne({ username });
const serverCfg = await getServerCfg();
const userAlias = await userAliasDAL.findOne({
externalId,
orgId,
aliasType: UserAliasType.SAML
});
const organization = await orgDAL.findOrgById(orgId);
if (!organization) throw new BadRequestError({ message: "Org not found" });
// TODO(dangtony98): remove this after aliases update
if (authProvider === AuthMethod.KEYCLOAK_SAML && appCfg.LICENSE_SERVER_KEY) {
throw new BadRequestError({ message: "Keycloak SAML is not yet available on Infisical Cloud" });
}
if (user) {
await userDAL.transaction(async (tx) => {
let user: TUsers;
if (userAlias) {
user = await userDAL.transaction(async (tx) => {
const foundUser = await userDAL.findById(userAlias.userId, tx);
const [orgMembership] = await orgDAL.findMembership(
{
userId: user.id,
[`${TableName.OrgMembership}.userId` as "userId"]: foundUser.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
{ tx }
);
if (!orgMembership) {
await orgDAL.createMembership(
await orgMembershipDAL.create(
{
userId: user.id,
orgId,
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
},
tx
);
// Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) {
} else if (orgMembership.status === OrgMembershipStatus.Invited && foundUser.isAccepted) {
await orgDAL.updateMembershipById(
orgMembership.id,
{
@@ -354,40 +373,97 @@ export const samlConfigServiceFactory = ({
tx
);
}
return foundUser;
});
} else {
user = await userDAL.transaction(async (tx) => {
const newUser = await userDAL.create(
let newUser: TUsers | undefined;
if (serverCfg.trustSamlEmails) {
newUser = await userDAL.findOne(
{
email,
isEmailVerified: true
},
tx
);
}
if (!newUser) {
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
newUser = await userDAL.create(
{
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
email,
isEmailVerified: serverCfg.trustSamlEmails,
firstName,
lastName,
authMethods: [],
isGhost: false
},
tx
);
}
await userAliasDAL.create(
{
username,
email,
firstName,
lastName,
authMethods: [AuthMethod.EMAIL],
isGhost: false
userId: newUser.id,
aliasType: UserAliasType.SAML,
externalId,
emails: email ? [email] : [],
orgId
},
tx
);
await orgDAL.createMembership({
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
});
const [orgMembership] = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.userId` as "userId"]: newUser.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
{ tx }
);
if (!orgMembership) {
await orgMembershipDAL.create(
{
userId: newUser.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
},
tx
);
// Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && newUser.isAccepted) {
await orgDAL.updateMembershipById(
orgMembership.id,
{
status: OrgMembershipStatus.Accepted
},
tx
);
}
return newUser;
});
}
const isUserCompleted = Boolean(user.isAccepted);
const providerAuthToken = jwt.sign(
{
authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id,
username: user.username,
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
firstName,
lastName,
organizationName: organization.name,
organizationId: organization.id,
organizationSlug: organization.slug,
authMethod: authProvider,
authType: UserAliasType.SAML,
isUserCompleted,
...(relayState
? {
@@ -403,6 +479,22 @@ export const samlConfigServiceFactory = ({
await samlConfigDAL.update({ orgId }, { lastUsed: new Date() });
if (user.email && !user.isEmailVerified) {
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id
});
await smtpService.sendMail({
template: SmtpTemplates.EmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [user.email],
substitutions: {
code: token
}
});
}
return { isUserCompleted, providerAuthToken };
};

View File

@@ -45,8 +45,8 @@ export type TGetSamlCfgDTO =
};
export type TSamlLoginDTO = {
username: string;
email?: string;
externalId: string;
email: string;
firstName: string;
lastName?: string;
authProvider: string;

View File

@@ -2,31 +2,31 @@ import { TListScimGroups, TListScimUsers, TScimGroup, TScimUser } from "./scim-t
export const buildScimUserList = ({
scimUsers,
offset,
startIndex,
limit
}: {
scimUsers: TScimUser[];
offset: number;
startIndex: number;
limit: number;
}): TListScimUsers => {
return {
Resources: scimUsers,
itemsPerPage: limit,
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
startIndex: offset,
startIndex,
totalResults: scimUsers.length
};
};
export const buildScimUser = ({
userId,
orgMembershipId,
username,
email,
firstName,
lastName,
active
}: {
userId: string;
orgMembershipId: string;
username: string;
email?: string | null;
firstName: string;
@@ -35,7 +35,7 @@ export const buildScimUser = ({
}): TScimUser => {
const scimUser = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
id: userId,
id: orgMembershipId,
userName: username,
displayName: `${firstName} ${lastName}`,
name: {
@@ -65,18 +65,18 @@ export const buildScimUser = ({
export const buildScimGroupList = ({
scimGroups,
offset,
startIndex,
limit
}: {
scimGroups: TScimGroup[];
offset: number;
startIndex: number;
limit: number;
}): TListScimGroups => {
return {
Resources: scimGroups,
itemsPerPage: limit,
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
startIndex: offset,
startIndex,
totalResults: scimGroups.length
};
};

View File

@@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import jwt from "jsonwebtoken";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups } from "@app/db/schemas";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups, TOrgMemberships, TUsers } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@@ -11,16 +11,21 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgPermission } from "@app/lib/types";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { AuthTokenType } from "@app/services/auth/auth-type";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { deleteOrgMembership } from "@app/services/org/org-fns";
import { deleteOrgMembershipFn } from "@app/services/org/org-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
@@ -47,24 +52,32 @@ import {
type TScimServiceFactoryDep = {
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
userDAL: Pick<TUserDALFactory, "find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch">;
userDAL: Pick<
TUserDALFactory,
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction"
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" | "updateMembershipById"
>;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById">;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
groupDAL: Pick<
TGroupDALFactory,
"create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups" | "transaction"
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
userGroupMembershipDAL: TUserGroupMembershipDALFactory; // TODO: Pick
userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,
"find" | "transaction" | "insertMany" | "filterProjectsByUserMembership" | "delete"
>;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
smtpService: TSmtpService;
smtpService: Pick<TSmtpService, "sendMail">;
};
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
@@ -73,7 +86,9 @@ export const scimServiceFactory = ({
licenseService,
scimDAL,
userDAL,
userAliasDAL,
orgDAL,
orgMembershipDAL,
projectDAL,
projectMembershipDAL,
groupDAL,
@@ -160,7 +175,7 @@ export const scimServiceFactory = ({
};
// SCIM server endpoints
const listScimUsers = async ({ offset, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
const listScimUsers = async ({ startIndex, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
const org = await orgDAL.findById(orgId);
if (!org.scimEnabled)
@@ -178,11 +193,11 @@ export const scimServiceFactory = ({
attributeName = "email";
}
return { [attributeName]: parsedValue };
return { [attributeName]: parsedValue.replace(/"/g, "") };
};
const findOpts = {
...(offset && { offset }),
...(startIndex && { offset: startIndex - 1 }),
...(limit && { limit })
};
@@ -194,10 +209,10 @@ export const scimServiceFactory = ({
findOpts
);
const scimUsers = users.map(({ userId, username, firstName, lastName, email }) =>
const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email }) =>
buildScimUser({
userId: userId ?? "",
username,
orgMembershipId: id ?? "",
username: externalId ?? username,
firstName: firstName ?? "",
lastName: lastName ?? "",
email,
@@ -207,16 +222,16 @@ export const scimServiceFactory = ({
return buildScimUserList({
scimUsers,
offset,
startIndex,
limit
});
};
const getScimUser = async ({ userId, orgId }: TGetScimUserDTO) => {
const getScimUser = async ({ orgMembershipId, orgId }: TGetScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
})
.catch(() => {
throw new ScimRequestError({
@@ -238,8 +253,8 @@ export const scimServiceFactory = ({
});
return buildScimUser({
userId: membership.userId as string,
username: membership.username,
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
email: membership.email ?? "",
firstName: membership.firstName as string,
lastName: membership.lastName as string,
@@ -247,7 +262,9 @@ export const scimServiceFactory = ({
});
};
const createScimUser = async ({ username, email, firstName, lastName, orgId }: TCreateScimUserDTO) => {
const createScimUser = async ({ externalId, email, firstName, lastName, orgId }: TCreateScimUserDTO) => {
if (!email) throw new ScimRequestError({ detail: "Invalid request. Missing email.", status: 400 });
const org = await orgDAL.findById(orgId);
if (!org)
@@ -262,67 +279,121 @@ export const scimServiceFactory = ({
status: 403
});
let user = await userDAL.findOne({
username
const appCfg = getConfig();
const serverCfg = await getServerCfg();
const userAlias = await userAliasDAL.findOne({
externalId,
orgId,
aliasType: UserAliasType.SAML
});
if (user) {
await userDAL.transaction(async (tx) => {
const [orgMembership] = await orgDAL.findMembership(
const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => {
let user: TUsers | undefined;
let orgMembership: TOrgMemberships;
if (userAlias) {
user = await userDAL.findById(userAlias.userId, tx);
orgMembership = await orgMembershipDAL.findOne(
{
userId: user.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
orgId
},
{ tx }
tx
);
if (orgMembership)
throw new ScimRequestError({
detail: "User already exists in the database",
status: 409
});
if (!orgMembership) {
await orgDAL.createMembership(
orgMembership = await orgMembershipDAL.create(
{
userId: user.id,
orgId,
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
},
tx
);
} else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) {
orgMembership = await orgMembershipDAL.updateById(
orgMembership.id,
{
status: OrgMembershipStatus.Accepted
},
tx
);
}
});
} else {
user = await userDAL.transaction(async (tx) => {
const newUser = await userDAL.create(
} else {
if (serverCfg.trustSamlEmails) {
user = await userDAL.findOne(
{
email,
isEmailVerified: true
},
tx
);
}
if (!user) {
const uniqueUsername = await normalizeUsername(`${firstName}-${lastName}`, userDAL);
user = await userDAL.create(
{
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
email,
isEmailVerified: serverCfg.trustSamlEmails,
firstName,
lastName,
authMethods: [],
isGhost: false
},
tx
);
}
await userAliasDAL.create(
{
username,
email,
firstName,
lastName,
authMethods: [AuthMethod.EMAIL],
isGhost: false
userId: user.id,
aliasType: UserAliasType.SAML,
externalId,
emails: email ? [email] : [],
orgId
},
tx
);
await orgDAL.createMembership(
const [foundOrgMembership] = await orgDAL.findMembership(
{
inviteEmail: email,
orgId,
userId: newUser.id,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
tx
{ tx }
);
return newUser;
});
}
const appCfg = getConfig();
orgMembership = foundOrgMembership;
if (!orgMembership) {
orgMembership = await orgMembershipDAL.create(
{
userId: user.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
},
tx
);
// Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) {
orgMembership = await orgDAL.updateMembershipById(
orgMembership.id,
{
status: OrgMembershipStatus.Accepted
},
tx
);
}
}
return { user, orgMembership };
});
if (email) {
await smtpService.sendMail({
@@ -337,20 +408,20 @@ export const scimServiceFactory = ({
}
return buildScimUser({
userId: user.id,
username: user.username,
firstName: user.firstName as string,
lastName: user.lastName as string,
email: user.email ?? "",
orgMembershipId: createdOrgMembership.id,
username: externalId,
firstName: createdUser.firstName as string,
lastName: createdUser.lastName as string,
email: createdUser.email ?? "",
active: true
});
};
const updateScimUser = async ({ userId, orgId, operations }: TUpdateScimUserDTO) => {
const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
})
.catch(() => {
throw new ScimRequestError({
@@ -386,18 +457,20 @@ export const scimServiceFactory = ({
});
if (!active) {
await deleteOrgMembership({
await deleteOrgMembershipFn({
orgMembershipId: membership.id,
orgId: membership.orgId,
orgDAL,
projectDAL,
projectMembershipDAL
projectMembershipDAL,
projectKeyDAL,
userAliasDAL,
licenseService
});
}
return buildScimUser({
userId: membership.userId as string,
username: membership.username,
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
email: membership.email,
firstName: membership.firstName as string,
lastName: membership.lastName as string,
@@ -405,11 +478,11 @@ export const scimServiceFactory = ({
});
};
const replaceScimUser = async ({ userId, active, orgId }: TReplaceScimUserDTO) => {
const replaceScimUser = async ({ orgMembershipId, active, orgId }: TReplaceScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
})
.catch(() => {
throw new ScimRequestError({
@@ -431,19 +504,20 @@ export const scimServiceFactory = ({
});
if (!active) {
// tx
await deleteOrgMembership({
await deleteOrgMembershipFn({
orgMembershipId: membership.id,
orgId: membership.orgId,
orgDAL,
projectDAL,
projectMembershipDAL
projectMembershipDAL,
projectKeyDAL,
userAliasDAL,
licenseService
});
}
return buildScimUser({
userId: membership.userId as string,
username: membership.username,
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
email: membership.email,
firstName: membership.firstName as string,
lastName: membership.lastName as string,
@@ -451,18 +525,11 @@ export const scimServiceFactory = ({
});
};
const deleteScimUser = async ({ userId, orgId }: TDeleteScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
})
.catch(() => {
throw new ScimRequestError({
detail: "User not found",
status: 404
});
});
const deleteScimUser = async ({ orgMembershipId, orgId }: TDeleteScimUserDTO) => {
const [membership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
});
if (!membership)
throw new ScimRequestError({
@@ -477,18 +544,20 @@ export const scimServiceFactory = ({
});
}
await deleteOrgMembership({
await deleteOrgMembershipFn({
orgMembershipId: membership.id,
orgId: membership.orgId,
orgDAL,
projectDAL,
projectMembershipDAL
projectMembershipDAL,
projectKeyDAL,
userAliasDAL,
licenseService
});
return {}; // intentionally return empty object upon success
};
const listScimGroups = async ({ orgId, offset, limit }: TListScimGroupsDTO) => {
const listScimGroups = async ({ orgId, startIndex, limit }: TListScimGroupsDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
@@ -509,21 +578,27 @@ export const scimServiceFactory = ({
status: 403
});
const groups = await groupDAL.findGroups({
orgId
});
const groups = await groupDAL.findGroups(
{
orgId
},
{
offset: startIndex - 1,
limit
}
);
const scimGroups = groups.map((group) =>
buildScimGroup({
groupId: group.id,
name: group.name,
members: []
members: [] // does this need to be populated?
})
);
return buildScimGroupList({
scimGroups,
offset,
startIndex,
limit
});
};
@@ -562,9 +637,15 @@ export const scimServiceFactory = ({
);
if (members && members.length) {
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: members.map((member) => member.value)
}
});
const newMembers = await addUsersToGroupByUserIds({
group,
userIds: members.map((member) => member.value),
userIds: orgMemberships.map((membership) => membership.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
@@ -581,12 +662,19 @@ export const scimServiceFactory = ({
return { group, newMembers: [] };
});
const orgMemberships = await orgDAL.findMembership({
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
$in: {
[`${TableName.OrgMembership}.userId` as "userId"]: newGroup.newMembers.map((member) => member.id)
}
});
return buildScimGroup({
groupId: newGroup.group.id,
name: newGroup.group.name,
members: newGroup.newMembers.map((member) => ({
value: member.id,
display: `${member.firstName} ${member.lastName}`
members: orgMemberships.map(({ id, firstName, lastName }) => ({
value: id,
display: `${firstName} ${lastName}`
}))
});
};
@@ -615,15 +703,22 @@ export const scimServiceFactory = ({
groupId: group.id
});
const orgMemberships = await orgDAL.findMembership({
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
$in: {
[`${TableName.OrgMembership}.userId` as "userId"]: users
.filter((user) => user.isPartOfGroup)
.map((user) => user.id)
}
});
return buildScimGroup({
groupId: group.id,
name: group.name,
members: users
.filter((user) => user.isPartOfGroup)
.map((user) => ({
value: user.id,
display: `${user.firstName} ${user.lastName}`
}))
members: orgMemberships.map(({ id, firstName, lastName }) => ({
value: id,
display: `${firstName} ${lastName}`
}))
});
};
@@ -667,7 +762,13 @@ export const scimServiceFactory = ({
}
if (members) {
const membersIdsSet = new Set(members.map((member) => member.value));
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: members.map((member) => member.value)
}
});
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId));
const directMemberUserIds = (
await userGroupMembershipDAL.find({
@@ -686,13 +787,13 @@ export const scimServiceFactory = ({
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
const allMembersUserIdsSet = new Set(allMembersUserIds);
const toAddUserIds = members.filter((member) => !allMembersUserIdsSet.has(member.value));
const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string));
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
if (toAddUserIds.length) {
await addUsersToGroupByUserIds({
group,
userIds: toAddUserIds.map((member) => member.value),
userIds: toAddUserIds.map((member) => member.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,

View File

@@ -12,7 +12,7 @@ export type TDeleteScimTokenDTO = {
// SCIM server endpoint types
export type TListScimUsersDTO = {
offset: number;
startIndex: number;
limit: number;
filter?: string;
orgId: string;
@@ -27,12 +27,12 @@ export type TListScimUsers = {
};
export type TGetScimUserDTO = {
userId: string;
orgMembershipId: string;
orgId: string;
};
export type TCreateScimUserDTO = {
username: string;
externalId: string;
email?: string;
firstName: string;
lastName: string;
@@ -40,7 +40,7 @@ export type TCreateScimUserDTO = {
};
export type TUpdateScimUserDTO = {
userId: string;
orgMembershipId: string;
orgId: string;
operations: {
op: string;
@@ -54,18 +54,18 @@ export type TUpdateScimUserDTO = {
};
export type TReplaceScimUserDTO = {
userId: string;
orgMembershipId: string;
active: boolean;
orgId: string;
};
export type TDeleteScimUserDTO = {
userId: string;
orgMembershipId: string;
orgId: string;
};
export type TListScimGroupsDTO = {
offset: number;
startIndex: number;
limit: number;
orgId: string;
};

View File

@@ -272,6 +272,7 @@ export const SECRETS = {
export const RAW_SECRETS = {
LIST: {
expand: "Whether or not to expand secret references",
recursive:
"Whether or not to fetch all secrets from the specified base path, and all of its subdirectories. Note, the max depth is 20 deep.",
workspaceId: "The ID of the project to list secrets from.",
@@ -464,12 +465,21 @@ export const SECRET_TAGS = {
export const IDENTITY_ADDITIONAL_PRIVILEGE = {
CREATE: {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to delete.",
identityId: "The ID of the identity to create.",
slug: "The slug of the privilege to create.",
permissions: `The permission object for the privilege.
1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]]
2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]]
2. [["read", "secrets", {environment: "dev"}]]
- Read secrets
\`\`\`
{ "permissions": [{"action": "read", "subject": "secrets"]}
\`\`\`
- Read and Write secrets
\`\`\`
{ "permissions": [{"action": "read", "subject": "secrets"], {"action": "write", "subject": "secrets"]}
\`\`\`
- Read secrets scoped to an environment and secret path
\`\`\`
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
\`\`\`
`,
isPackPermission: "Whether the server should pack(compact) the permission object.",
isTemporary: "Whether the privilege is temporary.",
@@ -483,11 +493,19 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
slug: "The slug of the privilege to update.",
newSlug: "The new slug of the privilege to update.",
permissions: `The permission object for the privilege.
1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]]
2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]]
2. [["read", "secrets", {environment: "dev"}]]
- Read secrets
\`\`\`
{ "permissions": [{"action": "read", "subject": "secrets"]}
\`\`\`
- Read and Write secrets
\`\`\`
{ "permissions": [{"action": "read", "subject": "secrets"], {"action": "write", "subject": "secrets"]}
\`\`\`
- Read secrets scoped to an environment and secret path
\`\`\`
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
\`\`\`
`,
isPackPermission: "Whether the server should pack(compact) the permission object.",
isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative",
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",

View File

@@ -108,6 +108,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
if (req.url.includes("/api/v3/auth/")) {
return;
}
if (!authMode) return;
switch (authMode) {

View File

@@ -2,6 +2,12 @@ import { Knex } from "knex";
import { z } from "zod";
import { registerV1EERoutes } from "@app/ee/routes/v1";
import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal";
import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal";
import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal";
import { accessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
@@ -88,6 +94,7 @@ import { orgDALFactory } from "@app/services/org/org-dal";
import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
import { orgServiceFactory } from "@app/services/org/org-service";
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { projectDALFactory } from "@app/services/project/project-dal";
import { projectQueueFactory } from "@app/services/project/project-queue";
import { projectServiceFactory } from "@app/services/project/project-service";
@@ -155,6 +162,7 @@ export const registerRoutes = async (
const authDAL = authDALFactory(db);
const authTokenDAL = tokenDALFactory(db);
const orgDAL = orgDALFactory(db);
const orgMembershipDAL = orgMembershipDALFactory(db);
const orgBotDAL = orgBotDALFactory(db);
const incidentContactDAL = incidentContactDALFactory(db);
const orgRoleDAL = orgRoleDALFactory(db);
@@ -205,6 +213,12 @@ export const registerRoutes = async (
const scimDAL = scimDALFactory(db);
const ldapConfigDAL = ldapConfigDALFactory(db);
const ldapGroupMapDAL = ldapGroupMapDALFactory(db);
const accessApprovalPolicyDAL = accessApprovalPolicyDALFactory(db);
const accessApprovalRequestDAL = accessApprovalRequestDALFactory(db);
const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db);
const accessApprovalRequestReviewerDAL = accessApprovalRequestReviewerDALFactory(db);
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
@@ -262,13 +276,19 @@ export const registerRoutes = async (
permissionService,
secretApprovalPolicyDAL
});
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
const samlService = samlConfigServiceFactory({
permissionService,
orgBotDAL,
orgDAL,
orgMembershipDAL,
userDAL,
userAliasDAL,
samlConfigDAL,
licenseService
licenseService,
tokenService,
smtpService
});
const groupService = groupServiceFactory({
userDAL,
@@ -297,7 +317,9 @@ export const registerRoutes = async (
licenseService,
scimDAL,
userDAL,
userAliasDAL,
orgDAL,
orgMembershipDAL,
projectDAL,
projectMembershipDAL,
groupDAL,
@@ -313,6 +335,7 @@ export const registerRoutes = async (
ldapConfigDAL,
ldapGroupMapDAL,
orgDAL,
orgMembershipDAL,
orgBotDAL,
groupDAL,
groupProjectDAL,
@@ -336,8 +359,13 @@ export const registerRoutes = async (
queueService
});
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
const userService = userServiceFactory({ userDAL });
const userService = userServiceFactory({
userDAL,
userAliasDAL,
orgMembershipDAL,
tokenService,
smtpService
});
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL });
const passwordService = authPaswordServiceFactory({
tokenService,
@@ -346,6 +374,7 @@ export const registerRoutes = async (
userDAL
});
const orgService = orgServiceFactory({
userAliasDAL,
licenseService,
samlConfigDAL,
orgRoleDAL,
@@ -580,6 +609,30 @@ export const registerRoutes = async (
secretVersionTagDAL,
secretQueueService
});
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
permissionService,
projectEnvDAL,
projectMembershipDAL,
projectDAL
});
const accessApprovalRequestService = accessApprovalRequestServiceFactory({
projectDAL,
permissionService,
accessApprovalRequestReviewerDAL,
additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL,
projectMembershipDAL,
accessApprovalPolicyDAL,
accessApprovalRequestDAL,
projectEnvDAL,
userDAL,
smtpService,
accessApprovalPolicyApproverDAL
});
const secretRotationQueue = secretRotationQueueFactory({
telemetryService,
secretRotationDAL,
@@ -716,6 +769,8 @@ export const registerRoutes = async (
identityProject: identityProjectService,
identityUa: identityUaService,
secretApprovalPolicy: sapService,
accessApprovalPolicy: accessApprovalPolicyService,
accessApprovalRequest: accessApprovalRequestService,
secretApprovalRequest: sarService,
secretRotation: secretRotationService,
dynamicSecret: dynamicSecretService,

View File

@@ -2,10 +2,12 @@ import { z } from "zod";
import {
DynamicSecretsSchema,
IdentityProjectAdditionalPrivilegeSchema,
IntegrationAuthsSchema,
SecretApprovalPoliciesSchema,
UsersSchema
} from "@app/db/schemas";
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
// sometimes the return data must be santizied to avoid leaking important values
// always prefer pick over omit in zod
@@ -62,6 +64,35 @@ export const secretRawSchema = z.object({
secretComment: z.string().optional()
});
export const PermissionSchema = z.object({
action: z
.string()
.min(1)
.describe("Describe what action an entity can take. Possible actions: create, edit, delete, and read"),
subject: z
.string()
.min(1)
.describe("The entity this permission pertains to. Possible options: secrets, environments"),
conditions: z
.object({
environment: z.string().describe("The environment slug this permission should allow.").optional(),
secretPath: z
.object({
$glob: z
.string()
.min(1)
.describe("The secret path this permission should allow. Can be a glob pattern such as /folder-name/*/** ")
})
.optional()
})
.describe("When specified, only matching conditions will be allowed to access given resource.")
.optional()
});
export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({
permissions: UnpackedPermissionSchema.array()
});
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
inputIV: true,
inputTag: true,

View File

@@ -42,7 +42,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
schema: {
body: z.object({
allowSignUp: z.boolean().optional(),
allowedSignUpDomain: z.string().optional().nullable()
allowedSignUpDomain: z.string().optional().nullable(),
trustSamlEmails: z.boolean().optional(),
trustLdapEmails: z.boolean().optional()
}),
response: {
200: z.object({

View File

@@ -2,11 +2,52 @@ import { z } from "zod";
import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMethod, AuthMode } from "@app/services/auth/auth-type";
export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/me/emails/code",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
username: z.string().trim()
}),
response: {
200: z.object({})
}
},
handler: async (req) => {
await server.services.user.sendEmailVerificationCode(req.body.username);
return {};
}
});
server.route({
method: "POST",
url: "/me/emails/verify",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
username: z.string().trim(),
code: z.string().trim()
}),
response: {
200: z.object({})
}
},
handler: async (req) => {
await server.services.user.verifyEmailVerificationCode(req.body.username, req.body.code);
return {};
}
});
server.route({
method: "PATCH",
url: "/me/mfa",

View File

@@ -166,6 +166,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath),
expandSecretReferences: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.expand),
recursive: z
.enum(["true", "false"])
.default("false")
@@ -233,6 +238,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environment,
expandSecretReferences: req.query.expandSecretReferences,
actorAuthMethod: req.permission.authMethod,
projectId: workspaceId,
path: secretPath,

View File

@@ -27,10 +27,17 @@ export const getTokenConfig = (tokenType: TokenType) => {
const expiresAt = new Date(new Date().getTime() + 86400000);
return { token, expiresAt };
}
case TokenType.TOKEN_EMAIL_VERIFICATION: {
// generate random 6-digit code
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
const triesLeft = 3;
const expiresAt = new Date(new Date().getTime() + 86400000);
return { token, triesLeft, expiresAt };
}
case TokenType.TOKEN_EMAIL_MFA: {
// generate random 6-digit code
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
const triesLeft = 5;
const triesLeft = 3;
const expiresAt = new Date(new Date().getTime() + 300000);
return { token, triesLeft, expiresAt };
}

View File

@@ -1,5 +1,6 @@
export enum TokenType {
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
TOKEN_EMAIL_MFA = "emailMfa",
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset"

View File

@@ -361,6 +361,7 @@ export const authLoginServiceFactory = ({
user = await userDAL.create({
username: email,
email,
isEmailVerified: true,
firstName,
lastName,
authMethods: [authMethod],
@@ -374,6 +375,8 @@ export const authLoginServiceFactory = ({
authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id,
username: user.username,
email: user.email,
isEmailVerified: user.isEmailVerified,
firstName: user.firstName,
lastName: user.lastName,
authMethod,

View File

@@ -1,6 +1,6 @@
import jwt from "jsonwebtoken";
import { OrgMembershipStatus } from "@app/db/schemas";
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -80,7 +80,7 @@ export const authSignupServiceFactory = ({
});
await smtpService.sendMail({
template: SmtpTemplates.EmailVerification,
template: SmtpTemplates.SignupEmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [user.email as string],
substitutions: {
@@ -102,6 +102,8 @@ export const authSignupServiceFactory = ({
code
});
await userDAL.updateById(user.id, { isEmailVerified: true });
// generate jwt token this is a temporary token
const jwtToken = jwt.sign(
{
@@ -169,12 +171,11 @@ export const authSignupServiceFactory = ({
tx
);
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
if (isAuthMethodSaml(authMethod) && organizationId) {
if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && organizationId) {
const [pendingOrgMembership] = await orgDAL.findMembership({
inviteEmail: email,
userId: user.id,
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
status: OrgMembershipStatus.Invited,
orgId: organizationId
[`${TableName.OrgMembership}.orgId` as "orgId"]: organizationId
});
if (pendingOrgMembership) {

View File

@@ -0,0 +1,13 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TOrgMembershipDALFactory = ReturnType<typeof orgMembershipDALFactory>;
export const orgMembershipDALFactory = (db: TDbClient) => {
const orgMembershipOrm = ormify(db, TableName.OrgMembership);
return {
...orgMembershipOrm
};
};

View File

@@ -262,13 +262,19 @@ export const orgDALFactory = (db: TDbClient) => {
.where(buildFindFilter(filter))
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
.leftJoin(TableName.UserAliases, function joinUserAlias() {
this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.OrgMembership}.userId`)
.andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.OrgMembership}.orgId`)
.andOn(`${TableName.UserAliases}.aliasType`, "=", (tx || db).raw("?", ["saml"]));
})
.select(
selectAllTableCols(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("scimEnabled").withSchema(TableName.Organization)
db.ref("scimEnabled").withSchema(TableName.Organization),
db.ref("externalId").withSchema(TableName.UserAliases)
)
.where({ isGhost: false });

View File

@@ -1,41 +1,78 @@
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
type TDeleteOrgMembership = {
orgMembershipId: string;
orgId: string;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipById" | "transaction">;
projectDAL: Pick<TProjectDALFactory, "find">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "delete" | "findProjectMembershipsByUserId">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
};
export const deleteOrgMembership = async ({
export const deleteOrgMembershipFn = async ({
orgMembershipId,
orgId,
orgDAL,
projectDAL,
projectMembershipDAL
projectMembershipDAL,
projectKeyDAL,
userAliasDAL,
licenseService
}: TDeleteOrgMembership) => {
const membership = await orgDAL.transaction(async (tx) => {
// delete org membership
const deletedMembership = await orgDAL.transaction(async (tx) => {
const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx);
const projects = await projectDAL.find({ orgId }, { tx });
if (!orgMembership.userId) {
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
}
// delete associated project memberships
await projectMembershipDAL.delete(
await userAliasDAL.delete(
{
$in: {
projectId: projects.map((project) => project.id)
},
userId: orgMembership.userId as string
userId: orgMembership.userId,
orgId
},
tx
);
// Get all the project memberships of the user in the organization
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId);
// Delete all the project memberships of the user in the organization
await projectMembershipDAL.delete(
{
$in: {
id: projectMemberships.map((membership) => membership.id)
}
},
tx
);
// Get all the project keys of the user in the organization
const projectKeys = await projectKeyDAL.find({
$in: {
projectId: projectMemberships.map((membership) => membership.projectId)
},
receiverId: orgMembership.userId
});
// Delete all the project keys of the user in the organization
await projectKeyDAL.delete(
{
$in: {
id: projectKeys.map((key) => key.id)
}
},
tx
);
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
});
return membership;
return deletedMembership;
};

View File

@@ -4,7 +4,7 @@ import crypto from "crypto";
import jwt from "jsonwebtoken";
import { Knex } from "knex";
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
import { OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -18,6 +18,7 @@ import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
@@ -30,6 +31,7 @@ import { TUserDALFactory } from "../user/user-dal";
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
import { TOrgBotDALFactory } from "./org-bot-dal";
import { TOrgDALFactory } from "./org-dal";
import { deleteOrgMembershipFn } from "./org-fns";
import { TOrgRoleDALFactory } from "./org-role-dal";
import {
TDeleteOrgMembershipDTO,
@@ -43,6 +45,7 @@ import {
} from "./org-types";
type TOrgServiceFactoryDep = {
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
orgDAL: TOrgDALFactory;
orgBotDAL: TOrgBotDALFactory;
orgRoleDAL: TOrgRoleDALFactory;
@@ -65,6 +68,7 @@ type TOrgServiceFactoryDep = {
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
export const orgServiceFactory = ({
userAliasDAL,
orgDAL,
userDAL,
groupDAL,
@@ -427,7 +431,13 @@ export const orgServiceFactory = ({
if (inviteeUser) {
// if user already exist means its already part of infisical
// Thus the signup flow is not needed anymore
const [inviteeMembership] = await orgDAL.findMembership({ orgId, userId: inviteeUser.id }, { tx });
const [inviteeMembership] = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id
},
{ tx }
);
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
throw new BadRequestError({
message: "Failed to invite an existing member of org",
@@ -519,9 +529,9 @@ export const orgServiceFactory = ({
throw new BadRequestError({ message: "Invalid request", name: "Verify user to org" });
}
const [orgMembership] = await orgDAL.findMembership({
userId: user.id,
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
status: OrgMembershipStatus.Invited,
orgId
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
});
if (!orgMembership)
throw new BadRequestError({
@@ -572,47 +582,14 @@ export const orgServiceFactory = ({
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
const deletedMembership = await orgDAL.transaction(async (tx) => {
const orgMembership = await orgDAL.deleteMembershipById(membershipId, orgId, tx);
if (!orgMembership.userId) {
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
}
// Get all the project memberships of the user in the organization
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId);
// Delete all the project memberships of the user in the organization
await projectMembershipDAL.delete(
{
$in: {
id: projectMemberships.map((membership) => membership.id)
}
},
tx
);
// Get all the project keys of the user in the organization
const projectKeys = await projectKeyDAL.find({
$in: {
projectId: projectMemberships.map((membership) => membership.projectId)
},
receiverId: orgMembership.userId
});
// Delete all the project keys of the user in the organization
await projectKeyDAL.delete(
{
$in: {
id: projectKeys.map((key) => key.id)
}
},
tx
);
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
const deletedMembership = await deleteOrgMembershipFn({
orgMembershipId: membershipId,
orgId,
orgDAL,
projectMembershipDAL,
projectKeyDAL,
userAliasDAL,
licenseService
});
return deletedMembership;

View File

@@ -110,7 +110,7 @@ export const projectMembershipServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const orgMembers = await orgDAL.findMembership({
orgId: project.orgId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId,
$in: {
[`${TableName.OrgMembership}.id` as "id"]: members.map(({ orgMembershipId }) => orgMembershipId)
}
@@ -119,7 +119,7 @@ export const projectMembershipServiceFactory = ({
const existingMembers = await projectMembershipDAL.find({
projectId,
$in: { userId: orgMembers.map(({ userId }) => userId).filter(Boolean) as string[] }
$in: { userId: orgMembers.map(({ userId }) => userId).filter(Boolean) }
});
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
@@ -134,7 +134,7 @@ export const projectMembershipServiceFactory = ({
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ userId }) => ({
projectId,
userId: userId as string
userId
})),
tx
);
@@ -145,12 +145,12 @@ export const projectMembershipServiceFactory = ({
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany(
orgMembers
.filter(({ userId }) => !userIdsToExcludeForProjectKeyAddition.has(userId as string))
.filter(({ userId }) => !userIdsToExcludeForProjectKeyAddition.has(userId))
.map(({ userId, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: actorId,
receiverId: userId as string,
receiverId: userId,
projectId
})),
tx

View File

@@ -8,6 +8,7 @@ import {
SecretKeyEncoding,
SecretsSchema,
SecretVersionsSchema,
TableName,
TIntegrationAuths,
TSecretApprovalRequestsSecrets,
TSecrets,
@@ -273,7 +274,10 @@ export const projectQueueFactory = ({
for (const key of existingProjectKeys) {
const user = await userDAL.findUserEncKeyByUserId(key.receiverId);
const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId });
const [orgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.userId` as "userId"]: key.receiverId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId
});
if (!user) {
throw new Error(`User with ID ${key.receiverId} was not found during upgrade.`);

View File

@@ -27,6 +27,7 @@ import {
fnSecretBlindIndexCheck,
fnSecretBulkInsert,
fnSecretBulkUpdate,
interpolateSecrets,
recursivelyGetSecretPaths
} from "./secret-fns";
import { TSecretQueueFactory } from "./secret-queue";
@@ -885,6 +886,7 @@ export const secretServiceFactory = ({
actorAuthMethod,
environment,
includeImports,
expandSecretReferences,
recursive
}: TGetSecretsRawDTO) => {
const botKey = await projectBotService.getBotKey(projectId);
@@ -902,17 +904,66 @@ export const secretServiceFactory = ({
recursive
});
return {
secrets: secrets.map((el) => decryptSecretRaw(el, botKey)),
imports: (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({
...el,
secrets: importedSecrets.map((sec) =>
decryptSecretRaw(
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath },
botKey
)
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey));
const decryptedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({
...el,
secrets: importedSecrets.map((sec) =>
decryptSecretRaw(
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath },
botKey
)
}))
)
}));
if (expandSecretReferences) {
const expandSecrets = interpolateSecrets({
folderDAL,
projectId,
secretDAL,
secretEncKey: botKey
});
const batchSecretsExpand = async (
secretBatch: {
secretKey: string;
secretValue: string;
secretComment?: string;
}[]
) => {
const secretRecord: Record<
string,
{
value: string;
comment?: string;
skipMultilineEncoding?: boolean;
}
> = {};
secretBatch.forEach((decryptedSecret) => {
secretRecord[decryptedSecret.secretKey] = {
value: decryptedSecret.secretValue,
comment: decryptedSecret.secretComment
};
});
await expandSecrets(secretRecord);
secretBatch.forEach((decryptedSecret, index) => {
// eslint-disable-next-line no-param-reassign
secretBatch[index].secretValue = secretRecord[decryptedSecret.secretKey].value;
});
};
// expand secrets
await batchSecretsExpand(decryptedSecrets);
// expand imports by batch
await Promise.all(decryptedImports.map((decryptedImport) => batchSecretsExpand(decryptedImport.secrets)));
}
return {
secrets: decryptedSecrets,
imports: decryptedImports
};
};

View File

@@ -138,6 +138,7 @@ export type TDeleteBulkSecretDTO = {
} & TProjectPermission;
export type TGetSecretsRawDTO = {
expandSecretReferences?: boolean;
path: string;
environment: string;
includeImports?: boolean;

View File

@@ -17,9 +17,11 @@ export type TSmtpSendMail = {
export type TSmtpService = ReturnType<typeof smtpServiceFactory>;
export enum SmtpTemplates {
SignupEmailVerification = "signupEmailVerification.handlebars",
EmailVerification = "emailVerification.handlebars",
SecretReminder = "secretReminder.handlebars",
EmailMfa = "emailMfa.handlebars",
AccessApprovalRequest = "accessApprovalRequest.handlebars",
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
NewDeviceJoin = "newDevice.handlebars",
OrgInvite = "organizationInvitation.handlebars",

View File

@@ -0,0 +1,50 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Access Approval Request</title>
</head>
<body>
<h2>Infisical</h2>
<h2>New access approval request pending your review</h2>
<p>You have a new access approval request pending review in project "{{projectName}}".</p>
<p>
{{requesterFullName}}
({{requesterEmail}}) has requested
{{#if isTemporary}}
temporary
{{else}}
permanent
{{/if}}
access to
{{secretPath}}
in the
{{environment}}
environment.
{{#if isTemporary}}
<br />
This access will expire
{{expiresIn}}
after it has been approved.
{{/if}}
</p>
<p>
The following permissions are requested:
<ul>
{{#each permissions}}
<li>{{this}}</li>
{{/each}}
</ul>
</p>
<p>
View the request and approve or deny it
<a href="{{approvalUrl}}">here</a>.
</p>
</body>
</html>

View File

@@ -1,17 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Code</title>
</head>
</head>
<body>
<body>
<h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
<p>Your confirmation code is below — enter it in the browser window where you've started confirming your email.</p>
<h1>{{code}}</h1>
<p>Questions about setting up Infisical? Email us at support@infisical.com</p>
</body>
</body>
</html>

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Code</title>
</head>
<body>
<h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
<h1>{{code}}</h1>
<p>Questions about setting up Infisical? Email us at support@infisical.com</p>
</body>
</html>

View File

@@ -102,7 +102,8 @@ export const superAdminServiceFactory = ({
superAdmin: true,
isGhost: false,
isAccepted: true,
authMethods: [AuthMethod.EMAIL]
authMethods: [AuthMethod.EMAIL],
isEmailVerified: true
},
tx
);

View File

@@ -0,0 +1,4 @@
export enum UserAliasType {
LDAP = "ldap",
SAML = "saml"
}

View File

@@ -74,6 +74,17 @@ export const userDALFactory = (db: TDbClient) => {
}
};
const findUsersByProjectMembershipIds = async (projectMembershipIds: string[]) => {
try {
return await db(TableName.ProjectMembership)
.whereIn(`${TableName.ProjectMembership}.id`, projectMembershipIds)
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.select("*");
} catch (error) {
throw new DatabaseError({ error, name: "Find users by project membership ids" });
}
};
const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => {
try {
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*");
@@ -140,6 +151,7 @@ export const userDALFactory = (db: TDbClient) => {
findUserEncKeyByUserId,
updateUserEncryptionByUserId,
findUserByProjectMembershipId,
findUsersByProjectMembershipIds,
upsertUserEncryptionKey,
createUserEncryption,
findOneUserAction,

View File

@@ -4,7 +4,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TUserDALFactory } from "@app/services/user/user-dal";
export const normalizeUsername = async (username: string, userDAL: Pick<TUserDALFactory, "findOne">) => {
let attempt = slugify(username);
let attempt = slugify(`${username}-${alphaNumericNanoId(4)}`);
let user = await userDAL.findOne({ username: attempt });
if (!user) return attempt;

View File

@@ -1,15 +1,151 @@
import { BadRequestError } from "@app/lib/errors";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { AuthMethod } from "../auth/auth-type";
import { TUserDALFactory } from "./user-dal";
type TUserServiceFactoryDep = {
userDAL: TUserDALFactory;
userDAL: Pick<
TUserDALFactory,
| "find"
| "findOne"
| "findById"
| "transaction"
| "updateById"
| "update"
| "deleteById"
| "findOneUserAction"
| "createUserAction"
| "findUserEncKeyByUserId"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">;
};
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
export const userServiceFactory = ({ userDAL }: TUserServiceFactoryDep) => {
export const userServiceFactory = ({
userDAL,
userAliasDAL,
orgMembershipDAL,
tokenService,
smtpService
}: TUserServiceFactoryDep) => {
const sendEmailVerificationCode = async (username: string) => {
const user = await userDAL.findOne({ username });
if (!user) throw new BadRequestError({ name: "Failed to find user" });
if (!user.email)
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
if (user.isEmailVerified)
throw new BadRequestError({ name: "Failed to send email verification code due to email already verified" });
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id
});
await smtpService.sendMail({
template: SmtpTemplates.EmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [user.email],
substitutions: {
code: token
}
});
};
const verifyEmailVerificationCode = async (username: string, code: string) => {
const user = await userDAL.findOne({ username });
if (!user) throw new BadRequestError({ name: "Failed to find user" });
if (!user.email)
throw new BadRequestError({ name: "Failed to verify email verification code due to no email on user" });
if (user.isEmailVerified)
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id,
code
});
const { email } = user;
await userDAL.transaction(async (tx) => {
await userDAL.updateById(
user.id,
{
isEmailVerified: true
},
tx
);
// check if there are users with the same email.
const users = await userDAL.find(
{
email,
isEmailVerified: true
},
{ tx }
);
if (users.length > 1) {
// merge users
const mergeUser = users.find((u) => u.id !== user.id);
if (!mergeUser) throw new BadRequestError({ name: "Failed to find merge user" });
const mergeUserOrgMembershipSet = new Set(
(await orgMembershipDAL.find({ userId: mergeUser.id }, { tx })).map((m) => m.orgId)
);
const myOrgMemberships = (await orgMembershipDAL.find({ userId: user.id }, { tx })).filter(
(m) => !mergeUserOrgMembershipSet.has(m.orgId)
);
const userAliases = await userAliasDAL.find(
{
userId: user.id
},
{ tx }
);
await userDAL.deleteById(user.id, tx);
if (myOrgMemberships.length) {
await orgMembershipDAL.insertMany(
myOrgMemberships.map((orgMembership) => ({
...orgMembership,
userId: mergeUser.id
})),
tx
);
}
if (userAliases.length) {
await userAliasDAL.insertMany(
userAliases.map((userAlias) => ({
...userAlias,
userId: mergeUser.id
})),
tx
);
}
} else {
// update current user's username to [email]
await userDAL.updateById(
user.id,
{
username: email
},
tx
);
}
});
};
const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => {
const user = await userDAL.findById(userId);
@@ -72,6 +208,8 @@ export const userServiceFactory = ({ userDAL }: TUserServiceFactoryDep) => {
};
return {
sendEmailVerificationCode,
verifyEmailVerificationCode,
toggleUserMfa,
updateUserName,
updateAuthMethods,

View File

@@ -12,6 +12,10 @@ description: "Learn how to log in to Infisical with LDAP."
You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)
Prerequisites:
- You must have an email address to use LDAP, regardless of whether or not you use that email address to sign in.
<Steps>
<Step title="Prepare the LDAP configuration in Infisical">
In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**.

View File

@@ -10,6 +10,10 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi
it.
</Info>
Prerequisites:
- You must have an email address to use LDAP, regardless of whether or not you use that email address to sign in.
<Steps>
<Step title="Prepare LDAP in JumpCloud">
In JumpCloud, head to USER MANAGEMENT > Users and create a new user via the **Manual user entry** option. This user

View File

@@ -3,11 +3,13 @@ title: "LDAP Overview"
sidebarTitle: "Overview"
description: "Learn how to authenticate into Infisical with LDAP."
---
<Info>
LDAP is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact sales@infisical.com to purchase an enterprise license to use it.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact sales@infisical.com to purchase an enterprise license to use it.
</Info>
You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol).
@@ -25,3 +27,18 @@ Read the general instructions for configuring LDAP [here](/documentation/platfor
If the documentation for your required identity provider is not shown in the list above, please reach out to [team@infisical.com](mailto:team@infisical.com) for assistance.
## FAQ
<AccordionGroup>
<Accordion title="Why does Infisical require additional email verification for users connected via LDAP?">
By default, Infisical Cloud is configured to not trust emails from external
identity providers to prevent any malicious account takeover attempts via
email spoofing. Accordingly, Infisical creates a new user for anyone provisioned
through an external identity provider and requires an additional email
verification step upon their first login.
If you're running a self-hosted instance of Infisical and would like it to trust emails from external identity providers,
you can configure this behavior in the admin panel.
</Accordion>
</AccordionGroup>

View File

@@ -4,10 +4,10 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
---
<Info>
Okta SAML SSO is a paid feature.
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
then you should contact sales@infisical.com to purchase an enterprise license to use it.
Okta SAML SSO is a paid feature. If you're using Infisical Cloud, then it is
available under the **Pro Tier**. If you're self-hosting Infisical, then you
should contact sales@infisical.com to purchase an enterprise license to use
it.
</Info>
<Steps>
@@ -22,24 +22,24 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
button.
![SAML Okta create app integration](../../../images/sso/okta/create-app-integration.png)
In the Create a New Application Integration dialog, select the **SAML 2.0** radio button:
![SAML Okta create SAML 2.0 integration](../../../images/sso/okta/create-saml-app.png)
On the General Settings screen, give the application a unique name like Infisical and select **Next**.
![SAML Okta create SAML 2.0 integration](../../../images/sso/okta/general-settings.png)
On the Configure SAML screen, set the **Single sign-on URL** and **Audience URI (SP Entity ID)** from step 1.
![SAML Okta configure IdP fields](../../../images/sso/okta/configure-saml.png)
<Note>
If you're self-hosting Infisical, then you will want to replace
`https://app.infisical.com` with your own domain.
</Note>
Also on the Configure SAML screen, configure the **Attribute Statements** to map:
- `id -> user.id`,
@@ -50,6 +50,7 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
![SAML Okta attribute statements](../../../images/sso/okta/attribute-statements.png)
Once configured, select **Next** to proceed to the Feedback screen and select **Finish**.
</Step>
<Step title="Retrieve Identity Provider (IdP) Information from Okta">
Once your application is created, select the **Sign On** tab for the app and select the **View Setup Instructions** button located on the right side of the screen:
@@ -59,12 +60,14 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
Copy the **Identity Provider Single Sign-On URL**, the **Identity Provider Issuer**, and the **X.509 Certificate** to use when finishing configuring Okta SAML in Infisical.
![SAML Okta IdP values](../../../images/sso/okta/idp-values.png)
</Step>
<Step title="Finish configuring SAML in Infisical">
Back in Infisical, set **Identity Provider Single Sign-On URL**, **Identity Provider Issuer**,
and **Certificate** to **X.509 Certificate** from step 3. Once you've done that, press **Update** to complete the required configuration.
![SAML Okta paste values into Infisical](../../../images/sso/okta/idp-values-2.png)
</Step>
<Step title="Assign users in Okta to the application">
Back in Okta, navigate to the **Assignments** tab and select **Assign**. You can assign access to the application on a user-by-user basis using the Assign to People option, or in-bulk using the Assign to Groups option.
@@ -72,11 +75,13 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
![SAML Okta assignment](../../../images/sso/okta/assignment.png)
At this point, you have configured everything you need within the context of the Okta Admin Portal.
</Step>
<Step title="Enable SAML SSO in Infisical">
Enabling SAML SSO allows members in your organization to log into Infisical via Okta.
![SAML Okta enable SAML](../../../images/sso/okta/enable-saml.png)
</Step>
<Step title="Enforce SAML SSO in Infisical">
Enforcing SAML SSO ensures that members in your organization can only access Infisical
@@ -89,13 +94,15 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
We recommend ensuring that your account is provisioned the application in Okta
prior to enforcing SAML SSO to prevent any unintended issues.
</Warning>
</Step>
</Steps>
<Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to
set the `AUTH_SECRET` and `SITE_URL` environment variable for it to work:
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
</Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work: - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
can be a random 32-byte base64 string generated with `openssl rand -base64
32`. - `SITE_URL`: The URL of your self-hosted instance of Infisical - should
be an absolute URL including the protocol (e.g. https://app.infisical.com)
</Note>

View File

@@ -5,11 +5,12 @@ description: "Learn how to log in to Infisical via SSO protocols."
---
<Info>
Infisical offers Google SSO and GitHub SSO for free across both Infisical Cloud and Infisical Self-hosted.
Infisical also offers SAML SSO authentication but as paid features that can be unlocked on Infisical Cloud's **Pro** tier
or via enterprise license on self-hosted instances of Infisical. On this front, we support industry-leading providers including
Okta, Azure AD, and JumpCloud; with any questions, please reach out to team@infisical.com.
Infisical offers Google SSO and GitHub SSO for free across both Infisical
Cloud and Infisical Self-hosted. Infisical also offers SAML SSO authentication
but as paid features that can be unlocked on Infisical Cloud's **Pro** tier or
via enterprise license on self-hosted instances of Infisical. On this front,
we support industry-leading providers including Okta, Azure AD, and JumpCloud;
with any questions, please reach out to team@infisical.com.
</Info>
You can configure your organization in Infisical to have members authenticate with the platform via protocols like [SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0).
@@ -31,3 +32,19 @@ Infisical supports these and many other identity providers:
- [Google SAML](/documentation/platform/sso/google-saml)
If your required identity provider is not shown in the list above, please reach out to [team@infisical.com](mailto:team@infisical.com) for assistance.
## FAQ
<AccordionGroup>
<Accordion title="Why does Infisical require additional email verification for users connected via SAML?">
By default, Infisical Cloud is configured to not trust emails from external
identity providers to prevent any malicious account takeover attempts via
email spoofing. Accordingly, Infisical creates a new user for anyone provisioned
through an external identity provider and requires an additional email
verification step upon their first login.
If you're running a self-hosted instance of Infisical and would like it to trust emails from external identity providers,
you can configure this behavior in the admin panel.
</Accordion>
</AccordionGroup>

View File

@@ -77,6 +77,8 @@ spec:
projectSlug: <project-slug>
envSlug: <env-slug> # "dev", "staging", "prod", etc..
secretsPath: "<secrets-path>" # Root is "/"
recursive: true # Fetch all secrets from the specified path and all sub-directories. Default is false.
credentialsRef:
secretName: universal-auth-credentials
secretNamespace: default
@@ -89,6 +91,7 @@ spec:
secretsScope:
envSlug: <env-slug>
secretsPath: <secrets-path> # Root is "/"
recursive: true # Fetch all secrets from the specified path and all sub-directories. Default is false.
managedSecretReference:
secretName: managed-secret

View File

@@ -3,30 +3,34 @@ title: "Configurations"
description: "Read how to configure environment variables for self-hosted Infisical."
---
Infisical accepts all configurations via environment variables. For a minimal self-hosted instance, at least `ENCRYPTION_KEY`, `AUTH_SECRET`, `DB_CONNECTION_URI` and `REDIS_URL` must be defined.
Infisical accepts all configurations via environment variables. For a minimal self-hosted instance, at least `ENCRYPTION_KEY`, `AUTH_SECRET`, `DB_CONNECTION_URI` and `REDIS_URL` must be defined.
However, you can configure additional settings to activate more features as needed.
## General platform
## General platform
Used to configure platform-specific security and operational settings
<ParamField query="ENCRYPTION_KEY" type="string" default="none" required>
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex
16`
</ParamField>
<ParamField query="AUTH_SECRET" type="string" default="none" required>
Must be a random 32 byte base64 string. Can be generated with `openssl rand -base64 32`
Must be a random 32 byte base64 string. Can be generated with `openssl rand
-base64 32`
</ParamField>
<ParamField query="SITE_URL" type="string" default="none" optional>
Must be an absolute URL including the protocol (e.g. https://app.infisical.com).
Must be an absolute URL including the protocol (e.g.
https://app.infisical.com).
</ParamField>
## Data Layer
## Data Layer
The platform utilizes Postgres to persist all of its data and Redis for caching and backgroud tasks
<ParamField query="DB_CONNECTION_URI" type="string" default="" required>
Postgres database connection string.
Postgres database connection string.
</ParamField>
<ParamField query="DB_ROOT_CERT" type="string" default="" optional>
@@ -39,9 +43,8 @@ The platform utilizes Postgres to persist all of its data and Redis for caching
Redis connection string.
</ParamField>
## Email service
Without email configuration, Infisical's core functions like sign-up/login and secret operations work, but this disables multi-factor authentication, email invites for projects, alerts for suspicious logins, and all other email-dependent features.
<Accordion title="Generic Configuration">
@@ -49,25 +52,36 @@ Without email configuration, Infisical's core functions like sign-up/login and s
Hostname to connect to for establishing SMTP connections
</ParamField>
<ParamField query="SMTP_USERNAME" type="string" default="none" optional>
Credential to connect to host (e.g. team@infisical.com)
</ParamField>
{" "}
<ParamField query="SMTP_PASSWORD" type="string" default="none" optional>
Credential to connect to host
</ParamField>
<ParamField query="SMTP_USERNAME" type="string" default="none" optional>
Credential to connect to host (e.g. team@infisical.com)
</ParamField>
<ParamField query="SMTP_PORT" type="string" default="587" optional>
Port to connect to for establishing SMTP connections
</ParamField>
{" "}
<ParamField query="SMTP_SECURE" type="string" default="none" optional>
If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported
</ParamField>
<ParamField query="SMTP_PASSWORD" type="string" default="none" optional>
Credential to connect to host
</ParamField>
<ParamField query="SMTP_FROM_ADDRESS" type="string" default="none" optional>
Email address to be used for sending emails
</ParamField>
{" "}
<ParamField query="SMTP_PORT" type="string" default="587" optional>
Port to connect to for establishing SMTP connections
</ParamField>
{" "}
<ParamField query="SMTP_SECURE" type="string" default="none" optional>
If true, use TLS when connecting to host. If false, TLS will be used if
STARTTLS is supported
</ParamField>
{" "}
<ParamField query="SMTP_FROM_ADDRESS" type="string" default="none" optional>
Email address to be used for sending emails
</ParamField>
<ParamField query="SMTP_FROM_NAME" type="string" default="none" optional>
Name label to be used in From field (e.g. Team)
@@ -76,25 +90,25 @@ Without email configuration, Infisical's core functions like sign-up/login and s
<Accordion title="Twilio SendGrid">
1. Create an account and configure [SendGrid](https://sendgrid.com) to send emails.
2. Create a SendGrid API Key under Settings > [API Keys](https://app.sendgrid.com/settings/api_keys)
3. Set a name for your API Key, we recommend using "Infisical," and select the "Restricted Key" option. You will need to enable the "Mail Send" permission as shown below:
1. Create an account and configure [SendGrid](https://sendgrid.com) to send emails.
2. Create a SendGrid API Key under Settings > [API Keys](https://app.sendgrid.com/settings/api_keys)
3. Set a name for your API Key, we recommend using "Infisical," and select the "Restricted Key" option. You will need to enable the "Mail Send" permission as shown below:
![creating sendgrid api key](../../images/self-hosting/configuration/email/email-sendgrid-create-key.png)
![creating sendgrid api key](../../images/self-hosting/configuration/email/email-sendgrid-create-key.png)
![setting sendgrid api key restriction](../../images/self-hosting/configuration/email/email-sendgrid-restrictions.png)
![setting sendgrid api key restriction](../../images/self-hosting/configuration/email/email-sendgrid-restrictions.png)
4. With the API Key, you can now set your SMTP environment variables:
4. With the API Key, you can now set your SMTP environment variables:
```
SMTP_HOST=smtp.sendgrid.net
SMTP_USERNAME=apikey
SMTP_PASSWORD=SG.rqFsfjxYPiqE1lqZTgD_lz7x8IVLx # your SendGrid API Key from step above
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
```
SMTP_HOST=smtp.sendgrid.net
SMTP_USERNAME=apikey
SMTP_PASSWORD=SG.rqFsfjxYPiqE1lqZTgD_lz7x8IVLx # your SendGrid API Key from step above
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
<Info>
Remember that you will need to restart Infisical for this to work properly.
@@ -105,19 +119,20 @@ Without email configuration, Infisical's core functions like sign-up/login and s
1. Create an account and configure [Mailgun](https://www.mailgun.com) to send emails.
2. Obtain your Mailgun credentials in Sending > Overview > SMTP
![obtain mailhog api key estriction](../../images/self-hosting/configuration/email/email-mailhog-credentials.png)
![obtain mailhog api key estriction](../../images/self-hosting/configuration/email/email-mailhog-credentials.png)
3. With your Mailgun credentials, you can now set up your SMTP environment variables:
3. With your Mailgun credentials, you can now set up your SMTP environment variables:
```
SMTP_HOST=smtp.mailgun.org # obtained from credentials page
SMTP_USERNAME=postmaster@example.mailgun.org # obtained from credentials page
SMTP_PASSWORD=password # obtained from credentials page
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
```
SMTP_HOST=smtp.mailgun.org # obtained from credentials page
SMTP_USERNAME=postmaster@example.mailgun.org # obtained from credentials page
SMTP_PASSWORD=password # obtained from credentials page
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
</Accordion>
<Accordion title="AWS SES">
@@ -149,6 +164,7 @@ Without email configuration, Infisical's core functions like sign-up/login and s
SMTP_FROM_NAME=Infisical
```
</Step>
</Steps>
<Info>
@@ -160,30 +176,32 @@ Without email configuration, Infisical's core functions like sign-up/login and s
1. Create an account and configure [SocketLabs](https://www.socketlabs.com/) to send emails.
2. From the dashboard, navigate to SMTP Credentials > SMTP & APIs > SMTP Credentials to obtain your SocketLabs SMTP credentials.
![opening SocketLabs dashboard](../../images/self-hosting/configuration/email/email-socketlabs-dashboard.png)
![opening SocketLabs dashboard](../../images/self-hosting/configuration/email/email-socketlabs-dashboard.png)
![obtaining SocketLabs credentials](../../images/self-hosting/configuration/email/email-socketlabs-credentials.png)
![obtaining SocketLabs credentials](../../images/self-hosting/configuration/email/email-socketlabs-credentials.png)
3. With your SocketLabs SMTP credentials, you can now set up your SMTP environment variables:
3. With your SocketLabs SMTP credentials, you can now set up your SMTP environment variables:
```
SMTP_HOST=smtp.socketlabs.com
SMTP_USERNAME=username # obtained from your credentials
SMTP_PASSWORD=password # obtained from your credentials
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
```
SMTP_HOST=smtp.socketlabs.com
SMTP_USERNAME=username # obtained from your credentials
SMTP_PASSWORD=password # obtained from your credentials
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
<Note>
The `SMTP_FROM_ADDRESS` environment variable should be an email for an
authenticated domain under Configuration > Domain Management in SocketLabs.
For example, if you're using SocketLabs in sandbox mode, then you may use an
email like `team@sandbox.socketlabs.dev`.
</Note>
{" "}
![SocketLabs domain management](../../images/self-hosting/configuration/email/email-socketlabs-domains.png)
<Note>
The `SMTP_FROM_ADDRESS` environment variable should be an email for an
authenticated domain under Configuration > Domain Management in SocketLabs.
For example, if you're using SocketLabs in sandbox mode, then you may use an
email like `team@sandbox.socketlabs.dev`.
</Note>
![SocketLabs domain management](../../images/self-hosting/configuration/email/email-socketlabs-domains.png)
<Info>
Remember that you will need to restart Infisical for this to work properly.
@@ -194,55 +212,57 @@ Without email configuration, Infisical's core functions like sign-up/login and s
1. Create an account on [Resend](https://resend.com).
2. Add a [Domain](https://resend.com/domains).
![adding resend domain](../../images/self-hosting/configuration/email/email-resend-create-domain.png)
![adding resend domain](../../images/self-hosting/configuration/email/email-resend-create-domain.png)
3. Create an [API Key](https://resend.com/api-keys).
3. Create an [API Key](https://resend.com/api-keys).
![creating resend api key](../../images/self-hosting/configuration/email/email-resend-create-key.png)
![creating resend api key](../../images/self-hosting/configuration/email/email-resend-create-key.png)
4. Go to the [SMTP page](https://resend.com/settings/smtp) and copy the values.
4. Go to the [SMTP page](https://resend.com/settings/smtp) and copy the values.
![go to resend smtp settings](../../images/self-hosting/configuration/email/email-resend-smtp-settings.png)
![go to resend smtp settings](../../images/self-hosting/configuration/email/email-resend-smtp-settings.png)
5. With the API Key, you can now set your SMTP environment variables variables:
5. With the API Key, you can now set your SMTP environment variables variables:
```
SMTP_HOST=smtp.resend.com
SMTP_USERNAME=resend
SMTP_PASSWORD=YOUR_API_KEY
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
```
SMTP_HOST=smtp.resend.com
SMTP_USERNAME=resend
SMTP_PASSWORD=YOUR_API_KEY
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
<Info>
Remember that you will need to restart Infisical for this to work properly.
</Info>
</Accordion>
<Accordion title="Gmail">
Create an account and enable "less secure app access" in Gmail Account Settings > Security. This will allow
applications like Infisical to authenticate with Gmail via your username and password.
![Gmail secure app access](../../images/self-hosting/configuration/email/email-gmail-app-access.png)
![Gmail secure app access](../../images/self-hosting/configuration/email/email-gmail-app-access.png)
With your Gmail username and password, you can set your SMTP environment variables:
With your Gmail username and password, you can set your SMTP environment variables:
```
SMTP_HOST=smtp.gmail.com
SMTP_USERNAME=hey@gmail.com # your email
SMTP_PASSWORD=password # your password
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@gmail.com
SMTP_FROM_NAME=Infisical
```
```
SMTP_HOST=smtp.gmail.com
SMTP_USERNAME=hey@gmail.com # your email
SMTP_PASSWORD=password # your password
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@gmail.com
SMTP_FROM_NAME=Infisical
```
<Warning>
As per the [notice](https://support.google.com/accounts/answer/6010255?hl=en) by Google, you should note that using Gmail credentials for SMTP configuration
will only work for Google Workspace or Google Cloud Identity customers as of May 30, 2022.
Put differently, the SMTP configuration is only possible with business (not personal) Gmail credentials.
Put differently, the SMTP configuration is only possible with business (not personal) Gmail credentials.
</Warning>
</Accordion>
@@ -250,51 +270,51 @@ Without email configuration, Infisical's core functions like sign-up/login and s
<Accordion title="Office365">
1. Create an account and configure [Office365](https://www.office.com/) to send emails.
2. With your login credentials, you can now set up your SMTP environment variables:
2. With your login credentials, you can now set up your SMTP environment variables:
```
SMTP_HOST=smtp.office365.com
SMTP_USERNAME=username@yourdomain.com # your username
SMTP_PASSWORD=password # your password
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=username@yourdomain.com
SMTP_FROM_NAME=Infisical
```
```
SMTP_HOST=smtp.office365.com
SMTP_USERNAME=username@yourdomain.com # your username
SMTP_PASSWORD=password # your password
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=username@yourdomain.com
SMTP_FROM_NAME=Infisical
```
</Accordion>
<Accordion title="Zoho Mail">
1. Create an account and configure [Zoho Mail](https://www.zoho.com/mail/) to send emails.
2. With your email credentials, you can now set up your SMTP environment variables:
2. With your email credentials, you can now set up your SMTP environment variables:
```
SMTP_HOST=smtp.zoho.com
SMTP_USERNAME=username # your email
SMTP_PASSWORD=password # your password
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your personal Zoho email or domain-based email linked to Zoho Mail
SMTP_FROM_NAME=Infisical
```
```
SMTP_HOST=smtp.zoho.com
SMTP_USERNAME=username # your email
SMTP_PASSWORD=password # your password
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your personal Zoho email or domain-based email linked to Zoho Mail
SMTP_FROM_NAME=Infisical
```
<Note>
You can use either your personal Zoho email address like `you@zohomail.com` or
a domain-based email address like `you@yourdomain.com`. If using a
domain-based email address, then please make sure that you've configured and
verified it with Zoho Mail.
</Note>
{" "}
<Note>
You can use either your personal Zoho email address like `you@zohomail.com` or
a domain-based email address like `you@yourdomain.com`. If using a
domain-based email address, then please make sure that you've configured and
verified it with Zoho Mail.
</Note>
<Info>
Remember that you will need to restart Infisical for this to work properly.
</Info>
</Accordion>
## Authentication
## SSO based login
By default, users can only login via email/password based login method.
To login into Infisical with OAuth providers such as Google, configure the associated variables.
@@ -335,33 +355,39 @@ To login into Infisical with OAuth providers such as Google, configure the assoc
</Accordion>
<Accordion title="Okta SAML">
Requires enterprise license. Please contact team@infisical.com to get more information.
Requires enterprise license. Please contact team@infisical.com to get more
information.
</Accordion>
<Accordion title="Azure SAML">
Requires enterprise license. Please contact team@infisical.com to get more information.
Requires enterprise license. Please contact team@infisical.com to get more
information.
</Accordion>
<Accordion title="JumpCloud SAML">
Requires enterprise license. Please contact team@infisical.com to get more information.
Requires enterprise license. Please contact team@infisical.com to get more
information.
</Accordion>
<ParamField query="NEXT_PUBLIC_SAML_ORG_SLUG" type="string">
Configure SAML organization slug to automatically redirect all users of your Infisical instance to the identity provider.
Configure SAML organization slug to automatically redirect all users of your
Infisical instance to the identity provider.
</ParamField>
## Native secret integrations
To help you sync secrets from Infisical to services such as Github and Gitlab, Infisical provides native integrations out of the box.
<Accordion title="Heroku">
<ParamField query="CLIENT_ID_HEROKU" type="string" default="none" optional>
OAuth2 client ID for Heroku integration
</ParamField>
<ParamField query="CLIENT_SECRET_HEROKU" type="string" default="none" optional>
<ParamField
query="CLIENT_SECRET_HEROKU"
type="string"
default="none"
optional
>
OAuth2 client secret for Heroku integration
</ParamField>
</Accordion>
@@ -371,9 +397,11 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I
OAuth2 client ID for Vercel integration
</ParamField>
<ParamField query="CLIENT_SECRET_VERCEL" type="string" default="none" optional>
OAuth2 client secret for Vercel integration
</ParamField>
{" "}
<ParamField query="CLIENT_SECRET_VERCEL" type="string" default="none" optional>
OAuth2 client secret for Vercel integration
</ParamField>
<ParamField query="CLIENT_SLUG_VERCEL" type="string" default="none" optional>
OAuth2 slug for Vercel integration

View File

@@ -2,8 +2,7 @@
title: "Docker Compose"
description: "Read how to run Infisical with Docker Compose template."
---
Install Infisical using Docker compose. This self hosting method contains all of the required components needed
to run a functional instance of Infisical.
This self hosting guide will walk you though the steps to self host Infisical using Docker compose.
## Prerequisites
- [Docker](https://docs.docker.com/engine/install/)
@@ -80,4 +79,4 @@ docker-compose -f docker-compose.prod.yml up
Your Infisical instance should now be running on port `80`. To access your instance, visit `http://localhost:80`.
![self host sign up](/images/self-hosting/applicable-to-all/selfhost-signup.png)
![self host sign up](/images/self-hosting/applicable-to-all/selfhost-signup.png)

View File

@@ -4,6 +4,7 @@
"requires": true,
"packages": {
"": {
"name": "frontend",
"dependencies": {
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",
@@ -12165,9 +12166,9 @@
"dev": true
},
"node_modules/ejs": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dev": true,
"dependencies": {
"jake": "^10.8.5"
@@ -22439,9 +22440,9 @@
}
},
"node_modules/tar": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
"integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"dev": true,
"dependencies": {
"chownr": "^2.0.0",

View File

@@ -17,23 +17,20 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children
containerClassName
)}
>
<div
className={twMerge(
"flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300",
className
)}
>
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
{children || (
<div className="text-sm">
Your role has limited permissions, please <br /> contact your administrator to gain
access
</div>
)}
<div className={twMerge("rounded-md bg-mineshaft-800 p-16 text-bunker-300", className)}>
<div className="flex items-end space-x-12">
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
{children || (
<div className="text-sm">
Your role has limited permissions, please <br /> contact your administrator to gain
access
</div>
)}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,32 @@
import { cva, VariantProps } from "cva";
import { twMerge } from "tailwind-merge";
interface IProps {
children: React.ReactNode;
className?: string;
}
const badgeVariants = cva(
[
"inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-xs text-yellow opacity-80 hover:opacity-100"
],
{
variants: {
variant: {
primary: "bg-yellow/20 text-yellow",
danger: "bg-red/20 text-red",
success: "bg-green/20 text-green"
}
}
}
);
export type BadgeProps = VariantProps<typeof badgeVariants> & IProps;
export const Badge = ({ children, className, variant }: BadgeProps) => {
return (
<div className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}>
{children}
</div>
);
};

View File

@@ -0,0 +1 @@
export { Badge } from "./Badge";

View File

@@ -29,7 +29,7 @@ const buttonVariants = cva(
colorSchema: {
primary: ["bg-primary", "text-black", "border-primary bg-opacity-90 hover:bg-opacity-100"],
secondary: ["bg-mineshaft", "text-gray-300", "border-mineshaft hover:bg-opacity-80"],
danger: ["bg-red", "text-white", "border-red hover:bg-opacity-90"],
danger: ["!bg-red", "!text-white", "!border-red hover:!bg-opacity-90"],
gray: ["bg-bunker-500", "text-bunker-200"]
},
variant: {

View File

@@ -0,0 +1,13 @@
import { twMerge } from "tailwind-merge";
interface IProps {
className?: string;
}
export const Divider = ({ className }: IProps): JSX.Element => {
return (
<div className={twMerge("flex items-center px-2 opacity-50", className)}>
<div aria-hidden="true" className="h-5 w-full grow border border-t border-mineshaft-200" />
</div>
);
};

View File

@@ -0,0 +1 @@
export { Divider } from "./Divider";

View File

@@ -46,14 +46,6 @@ export const SecretPathInput = ({
setInputValue(propValue ?? "/");
}, [propValue]);
useEffect(() => {
if (environment) {
setInputValue("/");
setSecretPath("/");
onChange?.("/");
}
}, [environment]);
useEffect(() => {
// update secret path if input is valid
if (
@@ -158,9 +150,8 @@ export const SecretPathInput = ({
key={`secret-reference-secret-${i + 1}`}
>
<div
className={`${
highlightedIndex === i ? "bg-gray-600" : ""
} text-md relative mb-0.5 flex w-full cursor-pointer select-none items-center justify-between rounded-md px-2 py-1 outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-500`}
className={`${highlightedIndex === i ? "bg-gray-600" : ""
} text-md relative mb-0.5 flex w-full cursor-pointer select-none items-center justify-between rounded-md px-2 py-1 outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-500`}
>
<div className="flex gap-2">
<div className="flex items-center text-yellow-700">

View File

@@ -41,18 +41,22 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
ref={ref}
className={twMerge(
`inline-flex items-center justify-between rounded-md
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200 focus:bg-mineshaft-700/80`,
className
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200`,
className,
isDisabled && "cursor-not-allowed opacity-50"
)}
>
<SelectPrimitive.Value placeholder={placeholder}>
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
</SelectPrimitive.Value>
{!isDisabled && (
<SelectPrimitive.Icon className="ml-3">
<FontAwesomeIcon icon={faCaretDown} size="sm" />
</SelectPrimitive.Icon>
)}
<SelectPrimitive.Icon className="ml-3">
<FontAwesomeIcon
icon={faCaretDown}
size="sm"
className={twMerge(isDisabled && "opacity-30")}
/>
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal>
<SelectPrimitive.Content

View File

@@ -6,6 +6,7 @@ export const publicPaths = [
"/signup",
"/signup/sso",
"/login",
"/login/ldap",
"/blog",
"/docs",
"/changelog",

View File

@@ -0,0 +1,13 @@
export {
useCreateAccessApprovalPolicy,
useCreateAccessRequest,
useDeleteAccessApprovalPolicy,
useReviewAccessRequest,
useUpdateAccessApprovalPolicy
} from "./mutation";
export {
useGetAccessApprovalPolicies,
useGetAccessApprovalRequests,
useGetAccessPolicyApprovalCount,
useGetAccessRequestsCount
} from "./queries";

View File

@@ -0,0 +1,123 @@
import { packRules } from "@casl/ability/extra";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { accessApprovalKeys } from "./queries";
import {
TAccessApproval,
TCreateAccessPolicyDTO,
TCreateAccessRequestDTO,
TDeleteSecretPolicyDTO,
TUpdateAccessPolicyDTO
} from "./types";
export const useCreateAccessApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateAccessPolicyDTO>({
mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath }) => {
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
environment,
projectSlug,
approvals,
approvers,
secretPath,
name
});
return data;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug));
}
});
};
export const useUpdateAccessApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateAccessPolicyDTO>({
mutationFn: async ({ id, approvers, approvals, name, secretPath }) => {
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
approvals,
approvers,
secretPath,
name
});
return data;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug));
}
});
};
export const useDeleteAccessApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TDeleteSecretPolicyDTO>({
mutationFn: async ({ id }) => {
const { data } = await apiRequest.delete(`/api/v1/access-approvals/policies/${id}`);
return data;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug));
}
});
};
export const useCreateAccessRequest = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateAccessRequestDTO>({
mutationFn: async ({ projectSlug, ...request }) => {
const { data } = await apiRequest.post<TAccessApproval>(
"/api/v1/access-approvals/requests",
{
...request,
permissions: request.permissions ? packRules(request.permissions) : undefined
},
{
params: {
projectSlug
}
}
);
return data;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequestCount(projectSlug));
}
});
};
export const useReviewAccessRequest = () => {
const queryClient = useQueryClient();
return useMutation<
{},
{},
{
requestId: string;
status: "approved" | "rejected";
projectSlug: string;
envSlug?: string;
requestedBy?: string;
}
>({
mutationFn: async ({ requestId, status }) => {
const { data } = await apiRequest.post(
`/api/v1/access-approvals/requests/${requestId}/review`,
{
status
}
);
return data;
},
onSuccess: (_, { projectSlug, envSlug, requestedBy }) => {
queryClient.invalidateQueries(
accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, requestedBy)
);
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequestCount(projectSlug));
}
});
};

View File

@@ -0,0 +1,159 @@
import { PackRule, unpackRules } from "@casl/ability/extra";
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TProjectPermission } from "../roles/types";
import {
TAccessApprovalPolicy,
TAccessApprovalRequest,
TAccessRequestCount,
TGetAccessApprovalRequestsDTO,
TGetAccessPolicyApprovalCountDTO
} from "./types";
export const accessApprovalKeys = {
getAccessApprovalPolicies: (projectSlug: string) =>
[{ projectSlug }, "access-approval-policies"] as const,
getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) =>
[{ workspaceId, environment }, "access-approval-policy"] as const,
getAccessApprovalRequests: (projectSlug: string, envSlug?: string, requestedBy?: string) =>
[{ projectSlug, envSlug, requestedBy }, "access-approvals-requests"] as const,
getAccessApprovalRequestCount: (projectSlug: string) =>
[{ projectSlug }, "access-approval-request-count"] as const
};
export const fetchPolicyApprovalCount = async ({
projectSlug,
envSlug
}: TGetAccessPolicyApprovalCountDTO) => {
const { data } = await apiRequest.get<{ count: number }>(
"/api/v1/access-approvals/policies/count",
{
params: { projectSlug, envSlug }
}
);
return data.count;
};
export const useGetAccessPolicyApprovalCount = ({
projectSlug,
envSlug,
options = {}
}: TGetAccessPolicyApprovalCountDTO & {
options?: UseQueryOptions<
number,
unknown,
number,
ReturnType<typeof accessApprovalKeys.getAccessApprovalPolicies>
>;
}) =>
useQuery({
queryFn: () => fetchPolicyApprovalCount({ projectSlug, envSlug }),
...options,
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
});
const fetchApprovalPolicies = async ({ projectSlug }: TGetAccessApprovalRequestsDTO) => {
const { data } = await apiRequest.get<{ approvals: TAccessApprovalPolicy[] }>(
"/api/v1/access-approvals/policies",
{ params: { projectSlug } }
);
return data.approvals;
};
const fetchApprovalRequests = async ({
projectSlug,
envSlug,
authorProjectMembershipId
}: TGetAccessApprovalRequestsDTO) => {
const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>(
"/api/v1/access-approvals/requests",
{ params: { projectSlug, envSlug, authorProjectMembershipId } }
);
return data.requests.map((request) => ({
...request,
privilege: request.privilege
? {
...request.privilege,
permissions: unpackRules(
request.privilege.permissions as unknown as PackRule<TProjectPermission>[]
)
}
: null,
permissions: unpackRules(request.permissions as unknown as PackRule<TProjectPermission>[])
}));
};
const fetchAccessRequestsCount = async (projectSlug: string) => {
const { data } = await apiRequest.get<TAccessRequestCount>(
"/api/v1/access-approvals/requests/count",
{ params: { projectSlug } }
);
return data;
};
export const useGetAccessRequestsCount = ({
projectSlug,
options = {}
}: TGetAccessApprovalRequestsDTO & {
options?: UseQueryOptions<
TAccessRequestCount,
unknown,
{ pendingCount: number; finalizedCount: number },
ReturnType<typeof accessApprovalKeys.getAccessApprovalRequestCount>
>;
}) =>
useQuery({
queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug),
queryFn: () => fetchAccessRequestsCount(projectSlug),
...options,
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
});
export const useGetAccessApprovalPolicies = ({
projectSlug,
envSlug,
authorProjectMembershipId,
options = {}
}: TGetAccessApprovalRequestsDTO & {
options?: UseQueryOptions<
TAccessApprovalPolicy[],
unknown,
TAccessApprovalPolicy[],
ReturnType<typeof accessApprovalKeys.getAccessApprovalPolicies>
>;
}) =>
useQuery({
queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug),
queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorProjectMembershipId }),
...options,
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
});
export const useGetAccessApprovalRequests = ({
projectSlug,
envSlug,
authorProjectMembershipId,
options = {}
}: TGetAccessApprovalRequestsDTO & {
options?: UseQueryOptions<
TAccessApprovalRequest[],
unknown,
TAccessApprovalRequest[],
ReturnType<typeof accessApprovalKeys.getAccessApprovalRequests>
>;
}) =>
useQuery({
queryKey: accessApprovalKeys.getAccessApprovalRequests(
projectSlug,
envSlug,
authorProjectMembershipId
),
queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorProjectMembershipId }),
...options,
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
});

View File

@@ -0,0 +1,139 @@
import { TProjectPermission } from "../roles/types";
import { WorkspaceEnv } from "../workspace/types";
export type TAccessApprovalPolicy = {
id: string;
name: string;
approvals: number;
secretPath: string;
envId: string;
workspace: string;
environment: WorkspaceEnv;
projectId: string;
approvers: string[];
};
export type TAccessApprovalRequest = {
id: string;
policyId: string;
privilegeId: string | null;
requestedBy: string;
createdAt: Date;
updatedAt: Date;
isTemporary: boolean;
temporaryRange: string | null | undefined;
permissions: TProjectPermission[] | null;
// Computed
environmentName: string;
isApproved: boolean;
privilege: {
membershipId: string;
isTemporary: boolean;
temporaryMode?: string | null;
temporaryRange?: string | null;
temporaryAccessStartTime?: Date | null;
temporaryAccessEndTime?: Date | null;
permissions: TProjectPermission[];
isApproved: boolean;
} | null;
policy: {
id: string;
name: string;
approvals: number;
approvers: string[];
secretPath?: string | null;
envId: string;
};
reviewers: {
member: string;
status: string;
}[];
};
export type TAccessApproval = {
id: string;
policyId: string;
privilegeId: string;
requestedBy: string;
};
export type TAccessRequestCount = {
pendingCount: number;
finalizedCount: number;
};
export type TProjectUserPrivilege = {
projectMembershipId: string;
slug: string;
id: string;
createdAt: Date;
updatedAt: Date;
permissions?: TProjectPermission[];
} & (
| {
isTemporary: true;
temporaryMode: string;
temporaryRange: string;
temporaryAccessStartTime: string;
temporaryAccessEndTime?: string;
}
| {
isTemporary: false;
temporaryMode?: null;
temporaryRange?: null;
temporaryAccessStartTime?: null;
temporaryAccessEndTime?: null;
}
);
export type TCreateAccessRequestDTO = {
projectSlug: string;
} & Omit<TProjectUserPrivilege, "id" | "createdAt" | "updatedAt" | "slug" | "projectMembershipId">;
export type TGetAccessApprovalRequestsDTO = {
projectSlug: string;
envSlug?: string;
authorProjectMembershipId?: string;
};
export type TGetAccessPolicyApprovalCountDTO = {
projectSlug: string;
envSlug: string;
};
export type TGetSecretApprovalPolicyOfBoardDTO = {
workspaceId: string;
environment: string;
secretPath: string;
};
export type TCreateAccessPolicyDTO = {
projectSlug: string;
name?: string;
environment: string;
approvers?: string[];
approvals?: number;
secretPath?: string;
};
export type TUpdateAccessPolicyDTO = {
id: string;
name?: string;
approvers?: string[];
secretPath?: string;
environment?: string;
approvals?: number;
// for invalidating list
projectSlug: string;
};
export type TDeleteSecretPolicyDTO = {
id: string;
// for invalidating list
projectSlug: string;
};

View File

@@ -3,6 +3,8 @@ export type TServerConfig = {
allowSignUp: boolean;
allowedSignUpDomain?: string | null;
isMigrationModeOn?: boolean;
trustSamlEmails: boolean;
trustLdapEmails: boolean;
};
export type TCreateAdminUserDTO = {

View File

@@ -5,7 +5,6 @@ export {
useSendMfaToken,
useSendPasswordResetEmail,
useSendVerificationEmail,
useVerifyEmailVerificationCode,
useVerifyMfaToken,
useVerifyPasswordResetCode
} from "./queries";
useVerifyPasswordResetCode,
useVerifySignupEmailVerificationCode} from "./queries";

View File

@@ -164,7 +164,7 @@ export const useSendVerificationEmail = () => {
});
};
export const useVerifyEmailVerificationCode = () => {
export const useVerifySignupEmailVerificationCode = () => {
return useMutation({
mutationFn: async ({ email, code }: { email: string; code: string }) => {
const { data } = await apiRequest.post("/api/v3/signup/email/verify", {

View File

@@ -1,9 +1,7 @@
import { PackRule, unpackRules } from "@casl/ability/extra";
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TProjectPermission } from "../roles/types";
import {
TGetIdentityProejctPrivilegeDetails as TGetIdentityProjectPrivilegeDetails,
TIdentityProjectPrivilege,
@@ -36,17 +34,14 @@ export const useGetIdentityProjectPrivilegeDetails = ({
const {
data: { privilege }
} = await apiRequest.get<{
privilege: Omit<TIdentityProjectPrivilege, "permissions"> & { permissions: unknown };
privilege: TIdentityProjectPrivilege;
}>(`/api/v1/additional-privilege/identity/${privilegeSlug}`, {
params: {
identityId,
projectSlug
}
});
return {
...privilege,
permissions: unpackRules(privilege.permissions as PackRule<TProjectPermission>[])
};
return privilege;
}
});
};
@@ -62,16 +57,11 @@ export const useListIdentityProjectPrivileges = ({
const {
data: { privileges }
} = await apiRequest.get<{
privileges: Array<
Omit<TIdentityProjectPrivilege, "permissions"> & { permissions: unknown }
>;
privileges: Array<TIdentityProjectPrivilege>;
}>("/api/v1/additional-privilege/identity", {
params: { identityId, projectSlug, unpacked: false }
params: { identityId, projectSlug }
});
return privileges.map((el) => ({
...el,
permissions: unpackRules(el.permissions as PackRule<TProjectPermission>[])
}));
return privileges;
}
});
};

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