Compare commits

..

381 Commits

Author SHA1 Message Date
Daniel Hougaard
53983d13f3 Fix: Delete access approval requests 2024-05-06 10:08:04 +02:00
Daniel Hougaard
5d9e47aec6 Fix: Migration timestamps and cleanup 2024-05-04 08:01:45 +02:00
Daniel Hougaard
be968be813 Fix: Fixed migration timestamp to match main 2024-05-04 07:53:22 +02:00
Daniel Hougaard
dc60d59e2e Merge branch 'daniel/fix-db-ref' of https://github.com/Infisical/infisical into daniel/fix-db-ref 2024-05-04 07:41:49 +02:00
Daniel Hougaard
e3f48e72b0 Feat: Secret approval deletion and more final changes 2024-05-04 07:41:44 +02:00
Daniel Hougaard
3c6b7aee9a Fix: Seperate groups / project users additional privileges 2024-05-04 07:41:44 +02:00
Daniel Hougaard
a183e94ff4 Schemas 2024-05-04 07:41:44 +02:00
Daniel Hougaard
b54e780443 Fix: Remove group-specific fields 2024-05-04 07:41:44 +02:00
Daniel Hougaard
5376bb72b3 Feat: Refactor to support groups 2024-05-04 07:41:44 +02:00
Daniel Hougaard
56d0d59ddc Feat: Group user additional privileges 2024-05-04 07:41:44 +02:00
Daniel Hougaard
ef9d4a4eee Update permission-dal.ts 2024-05-04 07:41:44 +02:00
Daniel Hougaard
873c6eea18 Fix: Updating approvers, now based on user ID 2024-05-04 07:41:44 +02:00
Daniel Hougaard
8d8e0bb794 Update project-membership-service.ts 2024-05-04 07:41:44 +02:00
Daniel Hougaard
348cf1c50c Fix: Cleanup secret & access approvals on project membership deletion 2024-05-04 07:41:44 +02:00
Daniel Hougaard
05669efdd8 Update group-project-service.ts 2024-05-04 07:41:44 +02:00
Daniel Hougaard
c302630551 Fix: Add project ID 2024-05-04 07:41:44 +02:00
Daniel Hougaard
2a4c9100be Fix ambiguous field 2024-05-04 07:41:44 +02:00
Daniel Hougaard
9ced5717ac Fix: Cleanup secret & access approvals on user group unassignment 2024-05-04 07:41:44 +02:00
Daniel Hougaard
b2f2541d0b Fix: Cleanup secret & access approvals on group deletion 2024-05-04 07:41:44 +02:00
Daniel Hougaard
3a9ad8d306 Update access-approval-request-service.ts 2024-05-04 07:41:44 +02:00
Daniel Hougaard
10207d03dd Fix: Delete potential privileges associated with access request on deletion 2024-05-04 07:41:44 +02:00
Daniel Hougaard
832dd62158 Schema update 2024-05-04 07:41:44 +02:00
Daniel Hougaard
df29f3499f Update 20240429175301_fix-db-reference-for-groups-and-project-memberships.ts 2024-05-04 07:41:44 +02:00
Daniel Hougaard
0d4c05f537 Update 20240429172301_access_approval_requests.ts 2024-05-04 07:41:44 +02:00
Daniel Hougaard
fb0407fec8 Fix: Duplicate users when user has both group access & project access 2024-05-04 07:41:44 +02:00
Daniel Hougaard
7d899463b4 Feat: Cleanup on group user removal 2024-05-04 07:41:44 +02:00
Daniel Hougaard
cfaf076352 Fix: Cleanup on membership delete & group disconnection 2024-05-04 07:41:44 +02:00
Daniel Hougaard
a875489172 Update access-approval-policy-router.ts 2024-05-04 07:41:44 +02:00
Daniel Hougaard
8634f8348b Chore: Cleanup 2024-05-04 07:41:44 +02:00
Daniel Hougaard
ad5b16d448 Type error 2024-05-04 07:41:44 +02:00
Daniel Hougaard
62e6acb7dc Update audit-log-types.ts 2024-05-04 07:41:44 +02:00
Daniel Hougaard
bf50eed8b0 Fix: Type errors 2024-05-04 07:41:44 +02:00
Daniel Hougaard
dcd69b5d99 Chore: Generate accurate schemas 2024-05-04 07:41:44 +02:00
Daniel Hougaard
dda98a0036 Removed comments 2024-05-04 07:41:44 +02:00
Daniel Hougaard
24ab66f61f Fix: Audit Log support for secret approval requests 2024-05-04 07:41:44 +02:00
Daniel Hougaard
9b97afad1c Update index.tsx 2024-05-04 07:41:44 +02:00
Daniel Hougaard
dca3832fd4 Update project-membership-types.ts 2024-05-04 07:41:44 +02:00
Daniel Hougaard
c3458a9d34 Update audit logs 2024-05-04 07:41:44 +02:00
Daniel Hougaard
76d371f13c Fix: Remove project membership dependency 2024-05-04 07:41:44 +02:00
Daniel Hougaard
7e9bcc5ce1 Chore: Spelling 2024-05-04 07:41:44 +02:00
Daniel Hougaard
029f2fa3af Fix: Refactor to use user ID's 2024-05-04 07:41:44 +02:00
Daniel Hougaard
0e9b2a8045 Update secret-approval-policy-service.ts 2024-05-04 07:41:44 +02:00
Daniel Hougaard
514cf07ba5 Fix: Refactor to use user ID's 2024-05-04 07:41:44 +02:00
Daniel Hougaard
55efc58566 Fix: Spelling and refactor to use user ID's 2024-05-04 07:41:44 +02:00
Daniel Hougaard
9b03f4984a Feat: Delete access approval request 2024-05-04 07:41:44 +02:00
Daniel Hougaard
a04f938b6c Update queries.tsx 2024-05-04 07:41:44 +02:00
Daniel Hougaard
596a22e9eb Fix: Update types 2024-05-04 07:41:44 +02:00
Daniel Hougaard
2ea01537c0 Update types.ts 2024-05-04 07:41:44 +02:00
Daniel Hougaard
ab57c658a8 Feat: Include group members option 2024-05-04 07:41:44 +02:00
Daniel Hougaard
89030c92c7 Fix: Secret approval refactor to user ID 2024-05-04 07:41:44 +02:00
Daniel Hougaard
d842aef714 Fix: Type errors 2024-05-04 07:41:44 +02:00
Daniel Hougaard
53089e26b9 Fix: Use user ID's instead of project memberships 2024-05-04 07:41:44 +02:00
Daniel Hougaard
be890b4189 Fix: Use user ID's instead of project memberships 2024-05-04 07:41:44 +02:00
Daniel Hougaard
cd0f126cf2 Update group-project-dal.ts 2024-05-04 07:41:44 +02:00
Daniel Hougaard
a092b9a19f Feat: Get group members 2024-05-04 07:41:44 +02:00
Daniel Hougaard
2f043849c9 Feat: Group support for project specific operations 2024-05-04 07:41:44 +02:00
Daniel Hougaard
d15fa9f176 Update index.ts 2024-05-04 07:41:44 +02:00
Daniel Hougaard
61508ec90a Feat: Delete access request 2024-05-04 07:41:44 +02:00
Daniel Hougaard
9d84bfa69e Feat: Additional priv support for groups 2024-05-04 07:41:44 +02:00
Daniel Hougaard
6a1d465778 Fix: Audit log support for user ID 2024-05-04 07:41:44 +02:00
Daniel Hougaard
5eff705486 Feat: Fix Groups for project membership specific operations 2024-05-04 07:41:44 +02:00
Daniel Hougaard
cd532bc20d Fix: Refactor to use new user ID field 2024-05-04 07:41:44 +02:00
Daniel Hougaard
18cdaaf024 Fix: Update for user ID 2024-05-04 07:41:44 +02:00
Daniel Hougaard
74e1dbdf9b Feat: Fix Groups for project membership specific operations (Manually modified) 2024-05-04 07:41:44 +02:00
Daniel Hougaard
64ab75748c Feat: Fix Groups for project membership specific operations 2024-05-04 07:41:44 +02:00
Daniel Hougaard
f7b689158d Update SpecificPrivilegeSection.tsx 2024-05-04 07:40:37 +02:00
Daniel Hougaard
19b9a31f0b Draft 2024-05-04 07:40:37 +02:00
Tuan Dang
0568cdcec6 Update instance recognition of offline license 2024-05-04 07:40:37 +02:00
Daniel Hougaard
a4bc459576 Fix: Duplicate access request check 2024-05-04 07:40:37 +02:00
Daniel Hougaard
b0b73acc21 Update SecretApprovalPage.tsx 2024-05-04 07:40:36 +02:00
Daniel Hougaard
07d66cbb65 Fix: Moved from email to username 2024-05-04 07:40:36 +02:00
Daniel Hougaard
ee97782860 Cleanup 2024-05-04 07:40:36 +02:00
Daniel Hougaard
c856de534b Fix: Move standalone components to individual files 2024-05-04 07:40:36 +02:00
Daniel Hougaard
eefd71f4cc Chore: Remove unused files 2024-05-04 07:40:36 +02:00
Daniel Hougaard
77e9609d0c Fix: Use username instead of email 2024-05-04 07:40:36 +02:00
Daniel Hougaard
afbbe5b7ba Fix: Columns 2024-05-04 07:40:36 +02:00
Daniel Hougaard
54d5cdedab Fix: Use username instead of email 2024-05-04 07:40:36 +02:00
Daniel Hougaard
9e12935a9f Feat: Badge component 2024-05-04 07:40:36 +02:00
Daniel Hougaard
101fa56d83 Fix: Moved Divider to v2 2024-05-04 07:40:36 +02:00
Daniel Hougaard
9bceb99110 Update index.ts 2024-05-04 07:40:36 +02:00
Daniel Hougaard
ca7a0a73be Fix: Pick 2024-05-04 07:40:36 +02:00
Daniel Hougaard
3632361f3c Chore: Moved verifyApprovers 2024-05-04 07:40:36 +02:00
Daniel Hougaard
f5c0274844 Fix: Make verifyApprovers independent on memberships 2024-05-04 07:40:36 +02:00
Daniel Hougaard
36a11387dd Fix: Made API endpoints more REST compliant 2024-05-04 07:40:36 +02:00
Daniel Hougaard
a82c94472a Chore: Cleaned up models 2024-05-04 07:40:36 +02:00
Daniel Hougaard
508f9610ca Fix: Improved migrations 2024-05-04 07:40:36 +02:00
Daniel Hougaard
59065c0648 Delete access-approval-request-secret-dal.ts 2024-05-04 07:40:36 +02:00
Daniel Hougaard
6443c94283 Fix: Don't display requested by when user has no access to read workspace members 2024-05-04 07:40:36 +02:00
Daniel Hougaard
26611881bc Fix: Don't display requested by when user has no access to read workspace members 2024-05-04 07:40:36 +02:00
Daniel Hougaard
2852989ac1 Fix: Add tooltip for clarity and fix wording 2024-05-04 07:40:36 +02:00
Daniel Hougaard
124bb7c205 Fix: Requesting approvals on previously rejected resources 2024-05-04 07:40:36 +02:00
Daniel Hougaard
697445cb1f Fix: Sort by createdAt 2024-05-04 07:40:36 +02:00
Daniel Hougaard
04108907ba Migration improvements 2024-05-04 07:40:36 +02:00
Daniel Hougaard
411cac2a31 Fixed bugs 2024-05-04 07:40:36 +02:00
Daniel Hougaard
afb9920fca Update SecretApprovalPage.tsx 2024-05-04 07:40:36 +02:00
Daniel Hougaard
ccf99d2465 Fix: Rebase errors 2024-05-04 07:40:36 +02:00
Daniel Hougaard
bca84f74c5 Removed unnessecary types 2024-05-04 07:40:36 +02:00
Daniel Hougaard
6c93973db7 Update AccessApprovalRequest.tsx 2024-05-04 07:40:36 +02:00
Daniel Hougaard
8d3f8c94fb Update AccessApprovalRequest.tsx 2024-05-04 07:40:36 +02:00
Daniel Hougaard
2eeb7dbc41 Update AccessApprovalRequest.tsx 2024-05-04 07:40:36 +02:00
Vladyslav Matsiiako
f18624d2e4 style changes 2024-05-04 07:40:36 +02:00
Daniel Hougaard
42a49da17b Update licence-fns.ts 2024-05-04 07:40:36 +02:00
Daniel Hougaard
5d87ce866c Update SpecificPrivilegeSection.tsx 2024-05-04 07:40:36 +02:00
Daniel Hougaard
02d7f90ec2 Update generate-schema-types.ts 2024-05-04 07:40:36 +02:00
Daniel Hougaard
03564fc59b Update SecretApprovalPage.tsx 2024-05-04 07:40:36 +02:00
Daniel Hougaard
8669f5c39a Fix: Added support for request access 2024-05-04 07:40:36 +02:00
Daniel Hougaard
c2bd2e6963 Fix: Remove redundant code 2024-05-04 07:40:36 +02:00
Daniel Hougaard
eb23d114a2 Fix: Validate approvers access 2024-05-04 07:40:36 +02:00
Daniel Hougaard
dec2cd465b Feat: Request access (new routes) 2024-05-04 07:40:36 +02:00
Daniel Hougaard
4cdec49751 Feat: Request Access (migrations) 2024-05-04 07:40:36 +02:00
Daniel Hougaard
43967ef848 Feat: Request access 2024-05-04 07:40:36 +02:00
Daniel Hougaard
55046d4144 Draft 2024-05-04 07:40:36 +02:00
Daniel Hougaard
124acfd279 Fix: Multiple approvers acceptance bug 2024-05-04 07:40:36 +02:00
Daniel Hougaard
62e12269b8 Fix: Rename change -> secret 2024-05-04 07:40:36 +02:00
Daniel Hougaard
f03d8b718e Style: Fix styling 2024-05-04 07:40:36 +02:00
Daniel Hougaard
acf13df0f3 Capitalization 2024-05-04 07:40:36 +02:00
Daniel Hougaard
cb8ec57177 Removed unnessecary types 2024-05-04 07:40:36 +02:00
Daniel Hougaard
b543f2ce50 Remove unnessecary types and projectMembershipid 2024-05-04 07:40:36 +02:00
Daniel Hougaard
f852e629ef Renaming 2024-05-04 07:40:36 +02:00
Daniel Hougaard
58b74d97bb Update smtp-service.ts 2024-05-04 07:40:36 +02:00
Daniel Hougaard
ba12aab65a Feat: Find users by project membership ID's 2024-05-04 07:40:36 +02:00
Daniel Hougaard
952c4a3931 Feat: access request emails 2024-05-04 07:40:36 +02:00
Daniel Hougaard
4a1bae07ca Update index.ts 2024-05-04 07:40:36 +02:00
Daniel Hougaard
c24f72435a Update access-approval-request-types.ts 2024-05-04 07:40:36 +02:00
Daniel Hougaard
4bf378c28d Feat: Send emails for access requests 2024-05-04 07:40:36 +02:00
Daniel Hougaard
407c8e17d3 Feat: Request access, extract permission details 2024-05-04 07:40:36 +02:00
Daniel Hougaard
67b7fb819a Fix: Security vulnurbility making it possible to spoof env & secret path requested. 2024-05-04 07:40:36 +02:00
Daniel Hougaard
edfccb2ae2 Update AccessApprovalRequest.tsx 2024-05-04 07:40:36 +02:00
Daniel Hougaard
df8dc43bcf Update AccessApprovalRequest.tsx 2024-05-04 07:40:36 +02:00
Daniel Hougaard
0d610f2644 Update AccessApprovalRequest.tsx 2024-05-04 07:40:36 +02:00
Vladyslav Matsiiako
a422d211fe style changes 2024-05-04 07:40:36 +02:00
Daniel Hougaard
f66d5e3d28 Fix: Status filtering & query invalidation 2024-05-04 07:40:36 +02:00
Daniel Hougaard
2c4e951fe2 Fix: Access request query invalidation 2024-05-04 07:40:36 +02:00
Vladyslav Matsiiako
e23d2dff64 fix privilegeId issue 2024-05-04 07:40:35 +02:00
Daniel Hougaard
e7de6ad5d9 Fix: Request access permissions 2024-05-04 07:40:35 +02:00
Daniel Hougaard
ca0d79d664 Update licence-fns.ts 2024-05-04 07:40:35 +02:00
Daniel Hougaard
adc0552df0 Add count 2024-05-04 07:40:35 +02:00
Daniel Hougaard
cff79e7c8c Fix: Don't allow users to request access to the same resource with same permissions multiple times 2024-05-04 07:40:35 +02:00
Daniel Hougaard
450e653005 Removed unused parameter 2024-05-04 07:40:35 +02:00
Daniel Hougaard
c866e55d1b Removed logs 2024-05-04 07:40:35 +02:00
Daniel Hougaard
d66c2a85f4 Removed logs 2024-05-04 07:40:35 +02:00
Daniel Hougaard
3b8c0a5cb1 Update SpecificPrivilegeSection.tsx 2024-05-04 07:40:35 +02:00
Daniel Hougaard
b77f0fed45 Update generate-schema-types.ts 2024-05-04 07:40:35 +02:00
Daniel Hougaard
e8bc47b573 Update SecretApprovalPage.tsx 2024-05-04 07:40:35 +02:00
Daniel Hougaard
c6785eff3a Fix: Minor fixes 2024-05-04 07:40:35 +02:00
Daniel Hougaard
bc1a9055ee Create index.tsx 2024-05-04 07:40:35 +02:00
Daniel Hougaard
dbe1f2bcff Feat: Request access 2024-05-04 07:40:35 +02:00
Daniel Hougaard
15107ebfaa Feat: Request access 2024-05-04 07:40:35 +02:00
Daniel Hougaard
435a395a15 Feat: Request access 2024-05-04 07:40:35 +02:00
Daniel Hougaard
fe829af054 Fix: Move to project slug 2024-05-04 07:40:35 +02:00
Daniel Hougaard
bd9dc44a69 Fix: Move to project slug 2024-05-04 07:40:35 +02:00
Daniel Hougaard
765dd84d19 Fix: Move to project slug 2024-05-04 07:40:35 +02:00
Daniel Hougaard
ac100e17f4 Fix: Added support for request access 2024-05-04 07:40:35 +02:00
Daniel Hougaard
e349f9aa3b Feat: Request access 2024-05-04 07:40:35 +02:00
Daniel Hougaard
29c3c41ebb Update index.tsx 2024-05-04 07:40:35 +02:00
Daniel Hougaard
e4af0759b8 Fix: Improve disabled Select 2024-05-04 07:40:35 +02:00
Daniel Hougaard
c681774709 Fix: Access Request setup 2024-05-04 07:40:35 +02:00
Daniel Hougaard
f63f2d9c69 Fix: Danger color not working on disabled buttons 2024-05-04 07:40:35 +02:00
Daniel Hougaard
044662901a Fix: Remove redundant code 2024-05-04 07:40:35 +02:00
Daniel Hougaard
8cdb2082d9 Feat: Request Access 2024-05-04 07:40:35 +02:00
Daniel Hougaard
52d0f5e1be Feat: Request access 2024-05-04 07:40:35 +02:00
Daniel Hougaard
be1e7be0d5 Feat: Request access 2024-05-04 07:40:35 +02:00
Daniel Hougaard
e1b0bc1b97 Fix: Types mismatch 2024-05-04 07:40:35 +02:00
Daniel Hougaard
f05d1b9d95 Fix: Validate approvers access 2024-05-04 07:40:35 +02:00
Daniel Hougaard
fa2bd6a75e Feat: Request access 2024-05-04 07:40:35 +02:00
Daniel Hougaard
2402ce2a12 Fix: Access Approval Policy DAL bugs 2024-05-04 07:40:35 +02:00
Daniel Hougaard
f770a18d41 Feat: Request access (new routes) 2024-05-04 07:40:35 +02:00
Daniel Hougaard
8ab7470f74 Fix: Move to project slug 2024-05-04 07:40:35 +02:00
Daniel Hougaard
eb56c23db1 Feat: Request access (models) 2024-05-04 07:40:35 +02:00
Daniel Hougaard
14812adade Feat: Request Access (migrations) 2024-05-04 07:40:35 +02:00
Daniel Hougaard
99b1efffc7 Feat: Request access 2024-05-04 07:40:35 +02:00
Daniel Hougaard
af6189c82b Feat: Request access 2024-05-04 07:40:35 +02:00
Daniel Hougaard
b6ca18af5d Fix: Remove logs 2024-05-04 07:40:26 +02:00
Daniel Hougaard
ee7bb6d60d Feat: Request Access 2024-05-04 07:40:26 +02:00
Daniel Hougaard
bfde867ba7 Draft 2024-05-04 07:40:12 +02:00
Daniel Hougaard
1a20f3148c Feat: Secret approval deletion and more final changes 2024-05-04 07:38:17 +02:00
Daniel Hougaard
ce5b14222f Fix: Seperate groups / project users additional privileges 2024-05-04 07:38:17 +02:00
Daniel Hougaard
74a43d55f7 Schemas 2024-05-04 07:38:17 +02:00
Daniel Hougaard
85cce4274e Fix: Remove group-specific fields 2024-05-04 07:38:17 +02:00
Daniel Hougaard
9eb88836e9 Feat: Refactor to support groups 2024-05-04 07:38:17 +02:00
Daniel Hougaard
d6c9658747 Feat: Group user additional privileges 2024-05-04 07:38:17 +02:00
Daniel Hougaard
f9967c0cc8 Update permission-dal.ts 2024-05-04 07:38:17 +02:00
Daniel Hougaard
bd8dfe4089 Fix: Updating approvers, now based on user ID 2024-05-04 07:38:17 +02:00
Daniel Hougaard
03fcaadab2 Update project-membership-service.ts 2024-05-04 07:38:17 +02:00
Daniel Hougaard
d3a0a84815 Fix: Cleanup secret & access approvals on project membership deletion 2024-05-04 07:38:17 +02:00
Daniel Hougaard
49ae146470 Update group-project-service.ts 2024-05-04 07:38:17 +02:00
Daniel Hougaard
f73b362c84 Fix: Add project ID 2024-05-04 07:38:17 +02:00
Daniel Hougaard
d9043fa9e0 Fix ambiguous field 2024-05-04 07:38:17 +02:00
Daniel Hougaard
98f6dc8df9 Fix: Cleanup secret & access approvals on user group unassignment 2024-05-04 07:38:17 +02:00
Daniel Hougaard
12c67d921d Fix: Cleanup secret & access approvals on group deletion 2024-05-04 07:38:17 +02:00
Daniel Hougaard
7dea2ba916 Update access-approval-request-service.ts 2024-05-04 07:38:17 +02:00
Daniel Hougaard
ace27a3605 Fix: Delete potential privileges associated with access request on deletion 2024-05-04 07:38:17 +02:00
Daniel Hougaard
e85ea1a458 Schema update 2024-05-04 07:38:17 +02:00
Daniel Hougaard
fb16464fda Update 20240429175301_fix-db-reference-for-groups-and-project-memberships.ts 2024-05-04 07:38:17 +02:00
Daniel Hougaard
c6b636bb42 Update 20240429172301_access_approval_requests.ts 2024-05-04 07:38:17 +02:00
Daniel Hougaard
034ac68b58 Fix: Duplicate users when user has both group access & project access 2024-05-04 07:38:17 +02:00
Daniel Hougaard
33e2c52f14 Feat: Cleanup on group user removal 2024-05-04 07:38:17 +02:00
Daniel Hougaard
b435a06a92 Fix: Cleanup on membership delete & group disconnection 2024-05-04 07:38:17 +02:00
Daniel Hougaard
48c23db3f9 Update access-approval-policy-router.ts 2024-05-04 07:38:17 +02:00
Daniel Hougaard
3159972ec3 Chore: Cleanup 2024-05-04 07:38:17 +02:00
Daniel Hougaard
8a5c293a6e Type error 2024-05-04 07:38:17 +02:00
Daniel Hougaard
1d9c18d155 Update audit-log-types.ts 2024-05-04 07:38:17 +02:00
Daniel Hougaard
13945bb31d Fix: Type errors 2024-05-04 07:38:17 +02:00
Daniel Hougaard
9df9197cac Chore: Generate accurate schemas 2024-05-04 07:38:17 +02:00
Daniel Hougaard
3809729e31 Removed comments 2024-05-04 07:38:17 +02:00
Daniel Hougaard
03d29a4afc Fix: Audit Log support for secret approval requests 2024-05-04 07:38:17 +02:00
Daniel Hougaard
a4264335fe Update index.tsx 2024-05-04 07:38:16 +02:00
Daniel Hougaard
7752bab0f0 Update project-membership-types.ts 2024-05-04 07:38:16 +02:00
Daniel Hougaard
56a20dc397 Update audit logs 2024-05-04 07:38:16 +02:00
Daniel Hougaard
6f79d8bb6c Fix: Remove project membership dependency 2024-05-04 07:38:16 +02:00
Daniel Hougaard
044ac01100 Chore: Spelling 2024-05-04 07:38:16 +02:00
Daniel Hougaard
641c0308f9 Fix: Refactor to use user ID's 2024-05-04 07:38:16 +02:00
Daniel Hougaard
ecfb833797 Update secret-approval-policy-service.ts 2024-05-04 07:38:16 +02:00
Daniel Hougaard
256f14cf6a Fix: Refactor to use user ID's 2024-05-04 07:38:16 +02:00
Daniel Hougaard
32c28227b2 Fix: Spelling and refactor to use user ID's 2024-05-04 07:38:16 +02:00
Daniel Hougaard
3be6402727 Feat: Delete access approval request 2024-05-04 07:38:16 +02:00
Daniel Hougaard
90c09c64cb Update queries.tsx 2024-05-04 07:38:16 +02:00
Daniel Hougaard
d0da69b999 Fix: Update types 2024-05-04 07:38:16 +02:00
Daniel Hougaard
7fb3730b22 Update types.ts 2024-05-04 07:38:16 +02:00
Daniel Hougaard
49e154ddd1 Feat: Include group members option 2024-05-04 07:38:16 +02:00
Daniel Hougaard
3742976bcb Fix: Secret approval refactor to user ID 2024-05-04 07:38:16 +02:00
Daniel Hougaard
5695137f24 Fix: Type errors 2024-05-04 07:38:16 +02:00
Daniel Hougaard
13d7cfd41b Fix: Use user ID's instead of project memberships 2024-05-04 07:38:16 +02:00
Daniel Hougaard
81fc5d3c18 Fix: Use user ID's instead of project memberships 2024-05-04 07:38:16 +02:00
Daniel Hougaard
8e8f44895d Update group-project-dal.ts 2024-05-04 07:38:16 +02:00
Daniel Hougaard
45570490a0 Feat: Get group members 2024-05-04 07:38:16 +02:00
Daniel Hougaard
1add5d6a24 Feat: Group support for project specific operations 2024-05-04 07:38:16 +02:00
Daniel Hougaard
7ac0536236 Update index.ts 2024-05-04 07:38:16 +02:00
Daniel Hougaard
89e9f46ae5 Feat: Delete access request 2024-05-04 07:38:16 +02:00
Daniel Hougaard
e3728b8a61 Feat: Additional priv support for groups 2024-05-04 07:38:16 +02:00
Daniel Hougaard
92bbabde3c Fix: Audit log support for user ID 2024-05-04 07:38:16 +02:00
Daniel Hougaard
11b4c5381a Feat: Fix Groups for project membership specific operations 2024-05-04 07:38:16 +02:00
Daniel Hougaard
97496c1b3c Fix: Refactor to use new user ID field 2024-05-04 07:38:16 +02:00
Daniel Hougaard
3cac1acf08 Fix: Update for user ID 2024-05-04 07:38:16 +02:00
Daniel Hougaard
c3756b8cc0 Feat: Fix Groups for project membership specific operations (Manually modified) 2024-05-04 07:38:16 +02:00
Daniel Hougaard
8678c79c02 Feat: Fix Groups for project membership specific operations 2024-05-04 07:38:16 +02:00
Daniel Hougaard
d2f010d17d Update SpecificPrivilegeSection.tsx 2024-05-04 07:38:16 +02:00
Daniel Hougaard
5c8d5e8430 Draft 2024-05-04 07:38:16 +02:00
Tuan Dang
7c8d99875a Update instance recognition of offline license 2024-05-04 07:38:16 +02:00
Daniel Hougaard
ab30b0803f Fix: Duplicate access request check 2024-05-04 07:38:16 +02:00
Daniel Hougaard
e2d68f07d1 Update SecretApprovalPage.tsx 2024-05-04 07:38:16 +02:00
Daniel Hougaard
07ced66538 Fix: Moved from email to username 2024-05-04 07:38:16 +02:00
Daniel Hougaard
9cb0ec231b Cleanup 2024-05-04 07:38:16 +02:00
Daniel Hougaard
8b169b2b9e Fix: Move standalone components to individual files 2024-05-04 07:38:16 +02:00
Daniel Hougaard
b9c02264c7 Chore: Remove unused files 2024-05-04 07:38:16 +02:00
Daniel Hougaard
9f96a9d188 Fix: Use username instead of email 2024-05-04 07:38:16 +02:00
Daniel Hougaard
55f232a642 Fix: Columns 2024-05-04 07:38:16 +02:00
Daniel Hougaard
34ff65d09c Fix: Use username instead of email 2024-05-04 07:38:16 +02:00
Daniel Hougaard
fe38c79f68 Feat: Badge component 2024-05-04 07:38:16 +02:00
Daniel Hougaard
a8aecc378b Fix: Moved Divider to v2 2024-05-04 07:38:16 +02:00
Daniel Hougaard
9ce7161aea Update index.ts 2024-05-04 07:38:16 +02:00
Daniel Hougaard
1951ca723c Fix: Pick 2024-05-04 07:38:16 +02:00
Daniel Hougaard
416f85f7e2 Chore: Moved verifyApprovers 2024-05-04 07:38:16 +02:00
Daniel Hougaard
75bef6fc8b Fix: Make verifyApprovers independent on memberships 2024-05-04 07:38:16 +02:00
Daniel Hougaard
5fa6e8bcf2 Fix: Made API endpoints more REST compliant 2024-05-04 07:38:16 +02:00
Daniel Hougaard
f4a5d9c391 Chore: Cleaned up models 2024-05-04 07:38:16 +02:00
Daniel Hougaard
3c6b976d8f Fix: Improved migrations 2024-05-04 07:38:16 +02:00
Daniel Hougaard
787d2287a0 Delete access-approval-request-secret-dal.ts 2024-05-04 07:38:16 +02:00
Daniel Hougaard
92f73d66f0 Fix: Don't display requested by when user has no access to read workspace members 2024-05-04 07:38:16 +02:00
Daniel Hougaard
3cd8670064 Fix: Don't display requested by when user has no access to read workspace members 2024-05-04 07:38:15 +02:00
Daniel Hougaard
4e3dd15d67 Fix: Add tooltip for clarity and fix wording 2024-05-04 07:38:15 +02:00
Daniel Hougaard
4c97ba1221 Fix: Requesting approvals on previously rejected resources 2024-05-04 07:38:15 +02:00
Daniel Hougaard
b89128fb32 Fix: Sort by createdAt 2024-05-04 07:38:15 +02:00
Daniel Hougaard
c788c0cb80 Migration improvements 2024-05-04 07:38:15 +02:00
Daniel Hougaard
e3fde17622 Fixed bugs 2024-05-04 07:38:15 +02:00
Daniel Hougaard
2eb9f30ef5 Update SecretApprovalPage.tsx 2024-05-04 07:38:15 +02:00
Daniel Hougaard
9432f3ce4a Fix: Rebase errors 2024-05-04 07:38:15 +02:00
Daniel Hougaard
5393afbd05 Removed unnessecary types 2024-05-04 07:38:15 +02:00
Daniel Hougaard
cb304d9a10 Update AccessApprovalRequest.tsx 2024-05-04 07:38:15 +02:00
Daniel Hougaard
a5f29db670 Update AccessApprovalRequest.tsx 2024-05-04 07:38:15 +02:00
Daniel Hougaard
009b49685c Update AccessApprovalRequest.tsx 2024-05-04 07:38:15 +02:00
Vladyslav Matsiiako
90d3f4d643 style changes 2024-05-04 07:38:15 +02:00
Daniel Hougaard
cc08d31300 Update licence-fns.ts 2024-05-04 07:38:15 +02:00
Daniel Hougaard
71deb7c62a Update SpecificPrivilegeSection.tsx 2024-05-04 07:38:15 +02:00
Daniel Hougaard
efec1c0a96 Update generate-schema-types.ts 2024-05-04 07:38:15 +02:00
Daniel Hougaard
efa4b7a4b6 Update SecretApprovalPage.tsx 2024-05-04 07:38:15 +02:00
Daniel Hougaard
65e0077d6c Fix: Added support for request access 2024-05-04 07:38:15 +02:00
Daniel Hougaard
1be311ffd9 Fix: Remove redundant code 2024-05-04 07:38:15 +02:00
Daniel Hougaard
3994962d0b Fix: Validate approvers access 2024-05-04 07:38:15 +02:00
Daniel Hougaard
0b9334f34c Feat: Request access (new routes) 2024-05-04 07:38:15 +02:00
Daniel Hougaard
2b4396547d Feat: Request Access (migrations) 2024-05-04 07:38:15 +02:00
Daniel Hougaard
761ec8dcc0 Feat: Request access 2024-05-04 07:38:15 +02:00
Daniel Hougaard
56e69bc5e9 Draft 2024-05-04 07:38:15 +02:00
Daniel Hougaard
067faef6a2 Fix: Multiple approvers acceptance bug 2024-05-04 07:38:15 +02:00
Daniel Hougaard
026b934a87 Fix: Rename change -> secret 2024-05-04 07:38:15 +02:00
Daniel Hougaard
90eef0495e Style: Fix styling 2024-05-04 07:38:15 +02:00
Daniel Hougaard
119fe97b14 Capitalization 2024-05-04 07:38:15 +02:00
Daniel Hougaard
dab3daee86 Removed unnessecary types 2024-05-04 07:38:15 +02:00
Daniel Hougaard
f2e344c11d Remove unnessecary types and projectMembershipid 2024-05-04 07:38:15 +02:00
Daniel Hougaard
df1a879e73 Renaming 2024-05-04 07:38:15 +02:00
Daniel Hougaard
fb21d4e13d Update smtp-service.ts 2024-05-04 07:38:15 +02:00
Daniel Hougaard
ca6f50a257 Feat: Find users by project membership ID's 2024-05-04 07:38:15 +02:00
Daniel Hougaard
26f0adbf7e Feat: access request emails 2024-05-04 07:38:15 +02:00
Daniel Hougaard
456d9ca5ce Update index.ts 2024-05-04 07:38:15 +02:00
Daniel Hougaard
e652fd962c Update access-approval-request-types.ts 2024-05-04 07:38:15 +02:00
Daniel Hougaard
bc16484f3f Feat: Send emails for access requests 2024-05-04 07:38:15 +02:00
Daniel Hougaard
4e87cc7c28 Feat: Request access, extract permission details 2024-05-04 07:38:15 +02:00
Daniel Hougaard
d9e2b99338 Fix: Security vulnurbility making it possible to spoof env & secret path requested. 2024-05-04 07:38:15 +02:00
Daniel Hougaard
9bac996c7a Update AccessApprovalRequest.tsx 2024-05-04 07:38:15 +02:00
Daniel Hougaard
089d57ea59 Update AccessApprovalRequest.tsx 2024-05-04 07:38:15 +02:00
Daniel Hougaard
14a17d638d Update AccessApprovalRequest.tsx 2024-05-04 07:38:15 +02:00
Vladyslav Matsiiako
5d9755b332 style changes 2024-05-04 07:38:15 +02:00
Daniel Hougaard
4f6b73518e Fix: Status filtering & query invalidation 2024-05-04 07:38:15 +02:00
Daniel Hougaard
2f4965659c Fix: Access request query invalidation 2024-05-04 07:38:15 +02:00
Vladyslav Matsiiako
2dc12693b0 fix privilegeId issue 2024-05-04 07:38:15 +02:00
Daniel Hougaard
5305139a55 Fix: Request access permissions 2024-05-04 07:38:15 +02:00
Daniel Hougaard
db72c07e81 Update licence-fns.ts 2024-05-04 07:38:15 +02:00
Daniel Hougaard
2f3ae5429a Add count 2024-05-04 07:38:15 +02:00
Daniel Hougaard
56e216c37c Fix: Don't allow users to request access to the same resource with same permissions multiple times 2024-05-04 07:38:14 +02:00
Daniel Hougaard
8db3544885 Removed unused parameter 2024-05-04 07:38:14 +02:00
Daniel Hougaard
f196c6a0ce Removed logs 2024-05-04 07:38:14 +02:00
Daniel Hougaard
246eecc23c Removed logs 2024-05-04 07:38:14 +02:00
Daniel Hougaard
013b744706 Update SpecificPrivilegeSection.tsx 2024-05-04 07:38:14 +02:00
Daniel Hougaard
fe68328aeb Update generate-schema-types.ts 2024-05-04 07:38:14 +02:00
Daniel Hougaard
1dcfd14431 Update SecretApprovalPage.tsx 2024-05-04 07:38:14 +02:00
Daniel Hougaard
f232f00f77 Fix: Minor fixes 2024-05-04 07:38:14 +02:00
Daniel Hougaard
82517477cb Create index.tsx 2024-05-04 07:38:14 +02:00
Daniel Hougaard
4e149cce81 Feat: Request access 2024-05-04 07:38:14 +02:00
Daniel Hougaard
5894cb4049 Feat: Request access 2024-05-04 07:38:14 +02:00
Daniel Hougaard
4938dda303 Feat: Request access 2024-05-04 07:38:14 +02:00
Daniel Hougaard
62112447a6 Fix: Move to project slug 2024-05-04 07:38:14 +02:00
Daniel Hougaard
bead911e0f Fix: Move to project slug 2024-05-04 07:38:14 +02:00
Daniel Hougaard
49987ca1e5 Fix: Move to project slug 2024-05-04 07:38:14 +02:00
Daniel Hougaard
cec083aa9b Fix: Added support for request access 2024-05-04 07:38:14 +02:00
Daniel Hougaard
9146079317 Feat: Request access 2024-05-04 07:38:14 +02:00
Daniel Hougaard
243ffc9904 Update index.tsx 2024-05-04 07:38:14 +02:00
Daniel Hougaard
b07d29faa2 Fix: Improve disabled Select 2024-05-04 07:38:14 +02:00
Daniel Hougaard
60fcc42d8c Fix: Access Request setup 2024-05-04 07:38:14 +02:00
Daniel Hougaard
fabf7181fa Fix: Danger color not working on disabled buttons 2024-05-04 07:38:14 +02:00
Daniel Hougaard
c9a7b6abb6 Fix: Remove redundant code 2024-05-04 07:38:14 +02:00
Daniel Hougaard
3c53befb3e Feat: Request Access 2024-05-04 07:38:14 +02:00
Daniel Hougaard
1f0cf6cc9b Feat: Request access 2024-05-04 07:38:14 +02:00
Daniel Hougaard
84c19a7554 Feat: Request access 2024-05-04 07:38:14 +02:00
Daniel Hougaard
d3ea91c54b Fix: Types mismatch 2024-05-04 07:38:14 +02:00
Daniel Hougaard
ecb58b8680 Fix: Validate approvers access 2024-05-04 07:38:14 +02:00
Daniel Hougaard
1972a3c6ed Feat: Request access 2024-05-04 07:38:14 +02:00
Daniel Hougaard
a0b1fb23df Fix: Access Approval Policy DAL bugs 2024-05-04 07:38:14 +02:00
Daniel Hougaard
5910c11d88 Feat: Request access (new routes) 2024-05-04 07:38:14 +02:00
Daniel Hougaard
a768496c5e Fix: Move to project slug 2024-05-04 07:38:14 +02:00
Daniel Hougaard
e59cc138d9 Feat: Request access (models) 2024-05-04 07:38:14 +02:00
Daniel Hougaard
7a7d41ca83 Feat: Request Access (migrations) 2024-05-04 07:38:14 +02:00
Daniel Hougaard
bd8b56a224 Feat: Request access 2024-05-04 07:38:14 +02:00
Daniel Hougaard
aa5bd117e6 Feat: Request access 2024-05-04 07:38:14 +02:00
Daniel Hougaard
e66e6a7490 Fix: Remove logs 2024-05-04 07:38:00 +02:00
Daniel Hougaard
e54f499026 Feat: Request Access 2024-05-04 07:38:00 +02:00
Daniel Hougaard
7a5e0e9463 Draft 2024-05-04 07:37:42 +02:00
Tuan Dang
87b571d6ff Merge remote-tracking branch 'origin' 2024-05-03 09:52:48 -07:00
Tuan Dang
1e6af8ad8f Update email in beginEmailSignupProcess 2024-05-03 09:49:10 -07:00
Maidul Islam
a771ddf859 Merge pull request #1721 from akhilmhdh/feat/audit-log-stream
Audit log streams
2024-05-03 12:48:55 -04:00
Akhil Mohan
c4cd6909bb docs: improved datadog log stream doc 2024-05-03 20:09:57 +05:30
Akhil Mohan
49642480d3 fix: resolved headers not working in queue 2024-05-03 20:06:24 +05:30
Akhil Mohan
b667dccc0d docs: improved text audit log stream 2024-05-03 18:19:37 +05:30
Akhil Mohan
fdda247120 feat: added a catch and override error message for ping check 2024-05-03 18:18:57 +05:30
Maidul Islam
ee8a88d062 Update docker-swarm.mdx 2024-05-03 08:44:43 -04:00
Maidul Islam
33349839cd Merge pull request #1780 from Infisical/maidul-1221
Make migration notice visible
2024-05-03 08:24:07 -04:00
Maidul Islam
8f3883c7d4 update date 2024-05-03 08:20:20 -04:00
Maidul Islam
38cfb7fd41 patch migration notice bug 2024-05-03 08:19:27 -04:00
Akhil Mohan
a331eb8dc4 docs: updated docs with header inputs for audit log stream and datadog section added 2024-05-03 17:43:58 +05:30
Akhil Mohan
2dcb409d3b feat: changed from token to headers for audit log streams api 2024-05-03 17:43:14 +05:30
Akhil Mohan
39bcb73f3d Merge pull request #1779 from Infisical/adjustment/added-workspace-slug-to-api-projects-get
Added slug to API response from workspace get all
2024-05-03 15:38:34 +05:30
Sheen Capadngan
52189111d7 adjustment: added slug to response 2024-05-03 18:03:21 +08:00
Akhil Mohan
f369761920 feat: rollback license-fns 2024-05-03 00:31:40 +05:30
Akhil Mohan
8eb22630b6 docs: added docs for audit log stream 2024-05-03 00:23:59 +05:30
Akhil Mohan
d650fd68c0 feat: improved api desc, added ping check before accepting stream 2024-05-03 00:23:59 +05:30
Maidul Islam
387c899193 add line breaks for readiblity 2024-05-03 00:23:59 +05:30
Maidul Islam
37882e6344 rephrase ui texts 2024-05-03 00:23:59 +05:30
Akhil Mohan
68a1aa6f46 feat: switched audit log stream from project level to org level 2024-05-03 00:23:59 +05:30
Akhil Mohan
fa18ca41ac feat(server): fixed if projectid is missing 2024-05-03 00:23:59 +05:30
Akhil Mohan
8485fdc1cd feat(ui): audit log page completed 2024-05-03 00:23:59 +05:30
Akhil Mohan
49ae2386c0 feat(ui): audit log api hooks 2024-05-03 00:23:59 +05:30
Akhil Mohan
f2b1f3f0e7 feat(server): audit log streams services and api routes 2024-05-03 00:23:58 +05:30
Akhil Mohan
69aa20e35c feat(server): audit log streams db schema changes 2024-05-03 00:23:58 +05:30
Maidul Islam
524c7ae78f Merge pull request #1776 from akhilmhdh/fix/bulk-op-sidebar
doc: resolved missing bulk secret api operations
2024-05-02 12:17:45 -04:00
Akhil Mohan
e13f7a7486 doc: resolved missing bulk secret api operations 2024-05-02 21:40:52 +05:30
Maidul Islam
1867fb2fc4 Merge pull request #1769 from Infisical/fix/address-functional-issues-with-secret-input
fix: address functional issues with secret input
2024-05-02 11:26:06 -04:00
Maidul Islam
5dd144b97b update self host nav items 2024-05-01 22:06:26 -04:00
Maidul Islam
b1b430e003 add more steps and FAQ for docker swarm 2024-05-01 21:57:48 -04:00
Maidul Islam
fb09980413 Create .env.example 2024-05-01 21:42:45 -04:00
Maidul Islam
3b36cb8b3d rename_ha-proxy 2024-05-01 21:16:27 -04:00
Maidul Islam
be6a98d0bb update docker swarm stack 2024-05-01 19:24:05 -04:00
Sheen Capadngan
dcaa7f1fce fix: address functional issues with secret input 2024-05-01 03:03:40 +08:00
145 changed files with 6679 additions and 800 deletions

65
.env.example Normal file
View File

@@ -0,0 +1,65 @@
# Keys
# Required key for platform encryption/decryption ops
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# JWT
# Required secrets to sign JWT tokens
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
# Postgres creds
POSTGRES_PASSWORD=infisical
POSTGRES_USER=infisical
POSTGRES_DB=infisical
# Required
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
# Redis
REDIS_URL=redis://redis:6379
# Website URL
# Required
SITE_URL=http://localhost:8080
# Mail/SMTP
SMTP_HOST=
SMTP_PORT=
SMTP_NAME=
SMTP_USERNAME=
SMTP_PASSWORD=
# Integration
# Optional only if integration is used
CLIENT_ID_HEROKU=
CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB=
CLIENT_ID_GITLAB=
CLIENT_ID_BITBUCKET=
CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB=
CLIENT_SECRET_GITLAB=
CLIENT_SECRET_BITBUCKET=
CLIENT_SLUG_VERCEL=
# Sentry (optional) for monitoring errors
SENTRY_DSN=
# Infisical Cloud-specific configs
# Ignore - Not applicable for self-hosted version
POSTHOG_HOST=
POSTHOG_PROJECT_API_KEY=
# SSO-specific variables
CLIENT_ID_GOOGLE_LOGIN=
CLIENT_SECRET_GOOGLE_LOGIN=
CLIENT_ID_GITHUB_LOGIN=
CLIENT_SECRET_GITHUB_LOGIN=
CLIENT_ID_GITLAB_LOGIN=
CLIENT_SECRET_GITLAB_LOGIN=

View File

@@ -2,4 +2,5 @@
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
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451

View File

@@ -1,8 +1,11 @@
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";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
@@ -112,6 +115,8 @@ declare module "fastify" {
identityAccessToken: TIdentityAccessTokenServiceFactory;
identityProject: TIdentityProjectServiceFactory;
identityUa: TIdentityUaServiceFactory;
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
secretApprovalRequest: TSecretApprovalRequestServiceFactory;
secretRotation: TSecretRotationServiceFactory;
@@ -120,6 +125,7 @@ declare module "fastify" {
scim: TScimServiceFactory;
ldap: TLdapConfigServiceFactory;
auditLog: TAuditLogServiceFactory;
auditLogStream: TAuditLogStreamServiceFactory;
secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory;
trustedIp: TTrustedIpServiceFactory;

View File

@@ -2,11 +2,26 @@ import { Knex } from "knex";
import {
TableName,
TAccessApprovalPolicies,
TAccessApprovalPoliciesApprovers,
TAccessApprovalPoliciesApproversInsert,
TAccessApprovalPoliciesApproversUpdate,
TAccessApprovalPoliciesInsert,
TAccessApprovalPoliciesUpdate,
TAccessApprovalRequests,
TAccessApprovalRequestsInsert,
TAccessApprovalRequestsReviewers,
TAccessApprovalRequestsReviewersInsert,
TAccessApprovalRequestsReviewersUpdate,
TAccessApprovalRequestsUpdate,
TApiKeys,
TApiKeysInsert,
TApiKeysUpdate,
TAuditLogs,
TAuditLogsInsert,
TAuditLogStreams,
TAuditLogStreamsInsert,
TAuditLogStreamsUpdate,
TAuditLogsUpdate,
TAuthTokens,
TAuthTokenSessions,
@@ -35,6 +50,9 @@ import {
TGroupProjectMemberships,
TGroupProjectMembershipsInsert,
TGroupProjectMembershipsUpdate,
TGroupProjectUserAdditionalPrivilege,
TGroupProjectUserAdditionalPrivilegeInsert,
TGroupProjectUserAdditionalPrivilegeUpdate,
TGroups,
TGroupsInsert,
TGroupsUpdate,
@@ -275,6 +293,11 @@ declare module "knex/types/tables" {
TProjectUserMembershipRolesInsert,
TProjectUserMembershipRolesUpdate
>;
[TableName.GroupProjectUserAdditionalPrivilege]: Knex.CompositeTableType<
TGroupProjectUserAdditionalPrivilege,
TGroupProjectUserAdditionalPrivilegeInsert,
TGroupProjectUserAdditionalPrivilegeUpdate
>;
[TableName.ProjectRoles]: Knex.CompositeTableType<TProjectRoles, TProjectRolesInsert, TProjectRolesUpdate>;
[TableName.ProjectUserAdditionalPrivilege]: Knex.CompositeTableType<
TProjectUserAdditionalPrivilege,
@@ -341,6 +364,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,
@@ -404,6 +452,11 @@ declare module "knex/types/tables" {
[TableName.LdapGroupMap]: Knex.CompositeTableType<TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate>;
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
[TableName.AuditLog]: Knex.CompositeTableType<TAuditLogs, TAuditLogsInsert, TAuditLogsUpdate>;
[TableName.AuditLogStream]: Knex.CompositeTableType<
TAuditLogStreams,
TAuditLogStreamsInsert,
TAuditLogStreamsUpdate
>;
[TableName.GitAppInstallSession]: Knex.CompositeTableType<
TGitAppInstallSessions,
TGitAppInstallSessionsInsert,

View File

@@ -0,0 +1,28 @@
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.AuditLogStream))) {
await knex.schema.createTable(TableName.AuditLogStream, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("url").notNullable();
t.text("encryptedHeadersCiphertext");
t.text("encryptedHeadersIV");
t.text("encryptedHeadersTag");
t.string("encryptedHeadersAlgorithm");
t.string("encryptedHeadersKeyEncoding");
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.AuditLogStream);
}
export async function down(knex: Knex): Promise<void> {
await dropOnUpdateTrigger(knex, TableName.AuditLogStream);
await knex.schema.dropTableIfExists(TableName.AuditLogStream);
}

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.uuid("envId").notNullable();
t.string("secretPath");
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("approverUserId").nullable();
t.foreign("approverUserId").references("id").inTable(TableName.Users).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,37 @@
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.GroupProjectUserAdditionalPrivilege))) {
await knex.schema.createTable(TableName.GroupProjectUserAdditionalPrivilege, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("slug", 60).notNullable();
t.uuid("groupProjectMembershipId").notNullable();
t.foreign("groupProjectMembershipId")
.references("id")
.inTable(TableName.GroupProjectMembership)
.onDelete("CASCADE");
t.uuid("requestedByUserId").notNullable();
t.foreign("requestedByUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.boolean("isTemporary").notNullable().defaultTo(false);
t.string("temporaryMode");
t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc
t.datetime("temporaryAccessStartTime");
t.datetime("temporaryAccessEndTime");
t.jsonb("permissions").notNullable();
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.GroupProjectUserAdditionalPrivilege);
}
export async function down(knex: Knex): Promise<void> {
await dropOnUpdateTrigger(knex, TableName.GroupProjectUserAdditionalPrivilege);
await knex.schema.dropTableIfExists(TableName.GroupProjectUserAdditionalPrivilege);
}

View File

@@ -0,0 +1,69 @@
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("projectUserPrivilegeId").nullable();
t.foreign("projectUserPrivilegeId")
.references("id")
.inTable(TableName.ProjectUserAdditionalPrivilege)
.onDelete("CASCADE");
t.uuid("groupProjectUserPrivilegeId").nullable();
t.foreign("groupProjectUserPrivilegeId")
.references("id")
.inTable(TableName.GroupProjectUserAdditionalPrivilege)
.onDelete("CASCADE");
t.uuid("requestedByUserId").notNullable();
t.foreign("requestedByUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.uuid("projectMembershipId").nullable();
t.foreign("projectMembershipId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
t.uuid("groupMembershipId").nullable();
t.foreign("groupMembershipId").references("id").inTable(TableName.GroupProjectMembership).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("memberUserId").notNullable();
t.foreign("memberUserId").references("id").inTable(TableName.Users).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,71 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
// SecretApprovalPolicyApprover, approverUserId
if (!(await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId"))) {
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (t) => {
t.uuid("approverId").nullable().alter();
t.uuid("approverUserId").nullable();
t.foreign("approverUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
});
}
// SecretApprovalRequest, statusChangeByUserId
if (!(await knex.schema.hasColumn(TableName.SecretApprovalRequest, "statusChangeByUserId"))) {
await knex.schema.alterTable(TableName.SecretApprovalRequest, (t) => {
t.uuid("statusChangeBy").nullable().alter();
t.uuid("statusChangeByUserId").nullable();
t.foreign("statusChangeByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
});
}
// SecretApprovalRequest, committerUserId
if (!(await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerUserId"))) {
await knex.schema.alterTable(TableName.SecretApprovalRequest, (t) => {
t.uuid("committerId").nullable().alter();
t.uuid("committerUserId").nullable();
t.foreign("committerUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
});
}
// SecretApprovalRequestReviewer, memberUserId
if (!(await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "memberUserId"))) {
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => {
t.uuid("member").nullable().alter();
t.uuid("memberUserId").nullable();
t.foreign("memberUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId")) {
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (t) => {
t.dropColumn("approverUserId");
});
}
if (await knex.schema.hasColumn(TableName.SecretApprovalRequest, "statusChangeByUserId")) {
await knex.schema.alterTable(TableName.SecretApprovalRequest, (t) => {
t.dropColumn("statusChangeByUserId");
});
}
if (await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerUserId")) {
await knex.schema.alterTable(TableName.SecretApprovalRequest, (t) => {
t.dropColumn("committerUserId");
});
}
if (await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "memberUserId")) {
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => {
t.dropColumn("memberUserId");
});
}
}

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(),
approverUserId: z.string().uuid().nullable().optional(),
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(),
memberUserId: 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,29 @@
// 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(),
projectUserPrivilegeId: z.string().uuid().nullable().optional(),
groupProjectUserPrivilegeId: z.string().uuid().nullable().optional(),
requestedByUserId: z.string().uuid(),
projectMembershipId: z.string().uuid().nullable().optional(),
groupMembershipId: z.string().uuid().nullable().optional(),
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

@@ -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 AuditLogStreamsSchema = z.object({
id: z.string().uuid(),
url: z.string(),
encryptedHeadersCiphertext: z.string().nullable().optional(),
encryptedHeadersIV: z.string().nullable().optional(),
encryptedHeadersTag: z.string().nullable().optional(),
encryptedHeadersAlgorithm: z.string().nullable().optional(),
encryptedHeadersKeyEncoding: z.string().nullable().optional(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TAuditLogStreams = z.infer<typeof AuditLogStreamsSchema>;
export type TAuditLogStreamsInsert = Omit<z.input<typeof AuditLogStreamsSchema>, TImmutableDBKeys>;
export type TAuditLogStreamsUpdate = Partial<Omit<z.input<typeof AuditLogStreamsSchema>, TImmutableDBKeys>>;

View File

@@ -0,0 +1,32 @@
// 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 GroupProjectUserAdditionalPrivilegeSchema = z.object({
id: z.string().uuid(),
slug: z.string(),
groupProjectMembershipId: z.string().uuid(),
requestedByUserId: z.string().uuid(),
isTemporary: z.boolean().default(false),
temporaryMode: z.string().nullable().optional(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional(),
permissions: z.unknown(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TGroupProjectUserAdditionalPrivilege = z.infer<typeof GroupProjectUserAdditionalPrivilegeSchema>;
export type TGroupProjectUserAdditionalPrivilegeInsert = Omit<
z.input<typeof GroupProjectUserAdditionalPrivilegeSchema>,
TImmutableDBKeys
>;
export type TGroupProjectUserAdditionalPrivilegeUpdate = Partial<
Omit<z.input<typeof GroupProjectUserAdditionalPrivilegeSchema>, TImmutableDBKeys>
>;

View File

@@ -1,4 +1,9 @@
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";
export * from "./auth-token-sessions";
export * from "./auth-tokens";
@@ -9,6 +14,7 @@ export * from "./git-app-install-sessions";
export * from "./git-app-org";
export * from "./group-project-membership-roles";
export * from "./group-project-memberships";
export * from "./group-project-user-additional-privilege";
export * from "./groups";
export * from "./identities";
export * from "./identity-access-tokens";

View File

@@ -25,6 +25,7 @@ export enum TableName {
ProjectMembership = "project_memberships",
ProjectRoles = "project_roles",
ProjectUserAdditionalPrivilege = "project_user_additional_privilege",
GroupProjectUserAdditionalPrivilege = "group_project_user_additional_privilege",
ProjectUserMembershipRole = "project_user_membership_roles",
ProjectKeys = "project_keys",
Secret = "secrets",
@@ -50,6 +51,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",
@@ -62,6 +67,7 @@ export enum TableName {
LdapConfig = "ldap_configs",
LdapGroupMap = "ldap_group_maps",
AuditLog = "audit_logs",
AuditLogStream = "audit_log_streams",
GitAppInstallSession = "git_app_install_sessions",
GitAppOrg = "git_app_org",
SecretScanningGitRisk = "secret_scanning_git_risks",

View File

@@ -9,10 +9,11 @@ import { TImmutableDBKeys } from "./models";
export const SecretApprovalPoliciesApproversSchema = z.object({
id: z.string().uuid(),
approverId: z.string().uuid(),
approverId: z.string().uuid().nullable().optional(),
policyId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
approverUserId: z.string().uuid().nullable().optional()
});
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;

View File

@@ -9,11 +9,12 @@ import { TImmutableDBKeys } from "./models";
export const SecretApprovalRequestsReviewersSchema = z.object({
id: z.string().uuid(),
member: z.string().uuid(),
member: z.string().uuid().nullable().optional(),
status: z.string(),
requestId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
memberUserId: z.string().uuid().nullable().optional()
});
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;

View File

@@ -16,9 +16,11 @@ export const SecretApprovalRequestsSchema = z.object({
slug: z.string(),
folderId: z.string().uuid(),
statusChangeBy: z.string().uuid().nullable().optional(),
committerId: z.string().uuid(),
committerId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
statusChangeByUserId: z.string().uuid().nullable().optional(),
committerUserId: z.string().uuid().nullable().optional()
});
export type TSecretApprovalRequests = z.infer<typeof SecretApprovalRequestsSchema>;

View File

@@ -0,0 +1,170 @@
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().nullish().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,192 @@
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(),
authorUserId: 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({
projectMembershipId: z.string().nullish(),
groupMembershipId: z.string().nullish(),
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,
envSlug: req.query.envSlug,
authorUserId: req.query.authorUserId,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
return { requests };
}
});
server.route({
url: "/:requestId",
method: "DELETE",
schema: {
params: z.object({
requestId: z.string().trim()
}),
querystring: z.object({
projectSlug: z.string().trim()
}),
response: {
200: z.object({
request: AccessApprovalRequestsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { request } = await server.services.accessApprovalRequest.deleteAccessApprovalRequest({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
requestId: req.params.requestId,
projectSlug: req.query.projectSlug
});
return { request };
}
});
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

@@ -0,0 +1,215 @@
import { z } from "zod";
import { AUDIT_LOG_STREAMS } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedAuditLogStreamSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
description: "Create an Audit Log Stream.",
security: [
{
bearerAuth: []
}
],
body: z.object({
url: z.string().min(1).describe(AUDIT_LOG_STREAMS.CREATE.url),
headers: z
.object({
key: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.CREATE.headers.key),
value: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.CREATE.headers.value)
})
.describe(AUDIT_LOG_STREAMS.CREATE.headers.desc)
.array()
.optional()
}),
response: {
200: z.object({
auditLogStream: SanitizedAuditLogStreamSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const auditLogStream = await server.services.auditLogStream.create({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
url: req.body.url,
headers: req.body.headers
});
return { auditLogStream };
}
});
server.route({
method: "PATCH",
url: "/:id",
config: {
rateLimit: readLimit
},
schema: {
description: "Update an Audit Log Stream by ID.",
security: [
{
bearerAuth: []
}
],
params: z.object({
id: z.string().describe(AUDIT_LOG_STREAMS.UPDATE.id)
}),
body: z.object({
url: z.string().optional().describe(AUDIT_LOG_STREAMS.UPDATE.url),
headers: z
.object({
key: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.UPDATE.headers.key),
value: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.UPDATE.headers.value)
})
.describe(AUDIT_LOG_STREAMS.UPDATE.headers.desc)
.array()
.optional()
}),
response: {
200: z.object({
auditLogStream: SanitizedAuditLogStreamSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const auditLogStream = await server.services.auditLogStream.updateById({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
id: req.params.id,
url: req.body.url,
headers: req.body.headers
});
return { auditLogStream };
}
});
server.route({
method: "DELETE",
url: "/:id",
config: {
rateLimit: readLimit
},
schema: {
description: "Delete an Audit Log Stream by ID.",
security: [
{
bearerAuth: []
}
],
params: z.object({
id: z.string().describe(AUDIT_LOG_STREAMS.DELETE.id)
}),
response: {
200: z.object({
auditLogStream: SanitizedAuditLogStreamSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const auditLogStream = await server.services.auditLogStream.deleteById({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
id: req.params.id
});
return { auditLogStream };
}
});
server.route({
method: "GET",
url: "/:id",
config: {
rateLimit: readLimit
},
schema: {
description: "Get an Audit Log Stream by ID.",
security: [
{
bearerAuth: []
}
],
params: z.object({
id: z.string().describe(AUDIT_LOG_STREAMS.GET_BY_ID.id)
}),
response: {
200: z.object({
auditLogStream: SanitizedAuditLogStreamSchema.extend({
headers: z
.object({
key: z.string(),
value: z.string()
})
.array()
.optional()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const auditLogStream = await server.services.auditLogStream.getById({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
id: req.params.id
});
return { auditLogStream };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
description: "List Audit Log Streams.",
security: [
{
bearerAuth: []
}
],
response: {
200: z.object({
auditLogStreams: SanitizedAuditLogStreamSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const auditLogStreams = await server.services.auditLogStream.list({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
return { auditLogStreams };
}
});
};

View File

@@ -1,3 +1,6 @@
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerGroupRouter } from "./group-router";
@@ -40,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);
@@ -55,6 +61,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
await server.register(registerGroupRouter, { prefix: "/groups" });
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });
await server.register(
async (privilegeRouter) => {
await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" });

View File

@@ -130,7 +130,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
}),
response: {
200: z.object({
approvals: sapPubSchema.merge(z.object({ approvers: z.string().array() })).array()
approvals: sapPubSchema.merge(z.object({ approvers: z.string().nullish().array() })).array()
})
}
},
@@ -161,7 +161,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
}),
response: {
200: z.object({
policy: sapPubSchema.merge(z.object({ approvers: z.string().array() })).optional()
policy: sapPubSchema.merge(z.object({ approvers: z.string().nullish().array() })).optional()
})
}
},

View File

@@ -197,7 +197,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
type: isClosing ? EventType.SECRET_APPROVAL_CLOSED : EventType.SECRET_APPROVAL_REOPENED,
// eslint-disable-next-line
metadata: {
[isClosing ? ("closedBy" as const) : ("reopenedBy" as const)]: approval.statusChangeBy as string,
[isClosing ? ("closedBy" as const) : ("reopenedBy" as const)]: approval.statusChangeByUserId as string,
secretApprovalRequestId: approval.id,
secretApprovalRequestSlug: approval.slug
// eslint-disable-next-line

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("approverUserId").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",
({ approverUserId, envId, envName: name, envSlug: slug, ...el }) => ({
...el,
envId,
environment: { id: envId, name, slug }
}),
({ approverUserId }) => approverUserId,
"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",
({ approverUserId, envId, envName: name, envSlug: slug, ...el }) => ({
...el,
envId,
environment: { id: envId, name, slug }
}),
({ approverUserId }) => approverUserId,
"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,271 @@
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 { TUserDALFactory } from "@app/services/user/user-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;
userDAL: Pick<TUserDALFactory, "findUsersByProjectId">;
};
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
permissionService,
projectEnvDAL,
userDAL,
projectDAL
}: 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" });
// We need to get the users by project ID to ensure they are part of the project.
const accessApproverUsers = await userDAL.findUsersByProjectId(
project.id,
approvers.map((approverUserId) => approverUserId)
);
if (accessApproverUsers.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: accessApproverUsers.map((user) => user.id)
});
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.create(
{
envId: env.id,
approvals,
secretPath,
name
},
tx
);
await accessApprovalPolicyApproverDAL.insertMany(
accessApproverUsers.map((user) => ({
approverUserId: user.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 secretApproverUsers = await userDAL.findUsersByProjectId(
accessApprovalPolicy.projectId,
approvers.map((approverUserId) => approverUserId)
);
await verifyApprovers({
projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId,
envSlug: accessApprovalPolicy.environment.slug,
secretPath: doc.secretPath!,
actorAuthMethod,
permissionService,
userIds: secretApproverUsers.map((user) => user.id)
});
if (secretApproverUsers.length !== approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await accessApprovalPolicyApproverDAL.insertMany(
secretApproverUsers.map((user) => ({
approverUserId: user.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,378 @@
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 projectUserAdditionalPrivilegeOrm = ormify(db, TableName.ProjectUserAdditionalPrivilege);
const groupProjectUserAdditionalPrivilegeOrm = ormify(db, TableName.GroupProjectUserAdditionalPrivilege);
const deleteMany = async (filter: TFindFilter<TAccessApprovalRequests>, tx?: Knex) => {
const transaction = tx || (await db.transaction());
try {
const accessApprovalRequests = await accessApprovalRequestOrm.find(filter, { tx: transaction });
await projectUserAdditionalPrivilegeOrm.delete(
{
$in: {
id: accessApprovalRequests
.filter((req) => Boolean(req.projectUserPrivilegeId))
.map((req) => req.projectUserPrivilegeId!)
}
},
transaction
);
await groupProjectUserAdditionalPrivilegeOrm.delete(
{
$in: {
id: accessApprovalRequests
.filter((req) => Boolean(req.groupProjectUserPrivilegeId))
.map((req) => req.groupProjectUserPrivilegeId!)
}
},
transaction
);
return await accessApprovalRequestOrm.delete(filter, transaction);
} catch (error) {
throw new DatabaseError({ error, name: "DeleteManyAccessApprovalRequest" });
}
};
const findRequestsWithPrivilegeByPolicyIds = async (policyIds: string[]) => {
try {
const docs = await db(TableName.AccessApprovalRequest)
.whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds)
.leftJoin(
TableName.ProjectUserAdditionalPrivilege,
`${TableName.AccessApprovalRequest}.projectUserPrivilegeId`,
`${TableName.ProjectUserAdditionalPrivilege}.id`
)
.leftJoin(
TableName.GroupProjectUserAdditionalPrivilege,
`${TableName.AccessApprovalRequest}.groupProjectUserPrivilegeId`,
`${TableName.GroupProjectUserAdditionalPrivilege}.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("approverUserId").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("memberUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"),
db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")
)
// Project user additional privilege
.select(
db
.ref("projectMembershipId")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("projectPrivilegeProjectMembershipId"),
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("projectPrivilegeIsTemporary"),
db
.ref("temporaryMode")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("projectPrivilegeTemporaryMode"),
db
.ref("temporaryRange")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("projectPrivilegeTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("projectPrivilegeTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("projectPrivilegeTemporaryAccessEndTime"),
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("projectPrivilegePermissions")
)
// Group project user additional privilege
.select(
db
.ref("groupProjectMembershipId")
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
.as("groupPrivilegeGroupProjectMembershipId"),
db
.ref("requestedByUserId")
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
.as("groupPrivilegeRequestedByUserId"),
db
.ref("isTemporary")
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
.as("groupPrivilegeIsTemporary"),
db
.ref("temporaryMode")
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
.as("groupPrivilegeTemporaryMode"),
db
.ref("temporaryRange")
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
.as("groupPrivilegeTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
.as("groupPrivilegeTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
.as("groupPrivilegeTemporaryAccessEndTime"),
db
.ref("permissions")
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
.as("groupPrivilegePermissions")
)
.orderBy(`${TableName.AccessApprovalRequest}.createdAt`, "desc");
const projectUserFormattedDocs = 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
},
// eslint-disable-next-line no-nested-ternary
privilege: doc.projectUserPrivilegeId
? {
projectMembershipId: doc.projectMembershipId,
groupMembershipId: null,
requestedByUserId: null,
isTemporary: doc.projectPrivilegeIsTemporary,
temporaryMode: doc.projectPrivilegeTemporaryMode,
temporaryRange: doc.projectPrivilegeTemporaryRange,
temporaryAccessStartTime: doc.projectPrivilegeTemporaryAccessStartTime,
temporaryAccessEndTime: doc.projectPrivilegeTemporaryAccessEndTime,
permissions: doc.projectPrivilegePermissions
}
: doc.groupProjectUserPrivilegeId
? {
groupMembershipId: doc.groupPrivilegeGroupProjectMembershipId,
requestedByUserId: doc.groupPrivilegeRequestedByUserId,
projectMembershipId: null,
isTemporary: doc.groupPrivilegeIsTemporary,
temporaryMode: doc.groupPrivilegeTemporaryMode,
temporaryRange: doc.groupPrivilegeTemporaryRange,
temporaryAccessStartTime: doc.groupPrivilegeTemporaryAccessStartTime,
temporaryAccessEndTime: doc.groupPrivilegeTemporaryAccessEndTime,
permissions: doc.groupPrivilegePermissions
}
: null,
isApproved: Boolean(doc.projectUserPrivilegeId || doc.groupProjectUserPrivilegeId)
}),
childrenMapper: [
{
key: "reviewerUserId",
label: "reviewers" as const,
mapper: ({ reviewerUserId, reviewerStatus: status }) =>
reviewerUserId ? { member: reviewerUserId, status } : undefined
},
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId }
]
});
if (!projectUserFormattedDocs) return [];
return projectUserFormattedDocs.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("memberUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"),
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("approverUserId").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: "reviewerUserId",
label: "reviewers" as const,
mapper: ({ reviewerUserId, reviewerStatus: status }) =>
reviewerUserId ? { member: reviewerUserId, status } : undefined
},
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId }
]
});
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.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("memberUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("memberUserId"));
const formattedRequests = sqlNestRelationships({
data: accessRequests,
key: "id",
parentMapper: (doc) => ({
...AccessApprovalRequestsSchema.parse(doc)
}),
childrenMapper: [
{
key: "memberUserId",
label: "reviewers" as const,
mapper: ({ memberUserId, reviewerStatus: status }) =>
memberUserId ? { member: memberUserId, status } : undefined
}
]
});
// an approval is pending if there is no reviewer rejections and no privilege ID is set
const pendingApprovals = formattedRequests.filter(
(req) =>
!req.projectUserPrivilegeId &&
!req.groupProjectUserPrivilegeId &&
!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.projectUserPrivilegeId ||
req.groupProjectUserPrivilegeId ||
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, delete: deleteMany };
};

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,502 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { ProjectMembershipRole, TProjectUserAdditionalPrivilege } 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 { TGroupProjectUserAdditionalPrivilegeDALFactory } from "../group-project-user-additional-privilege/group-project-user-additional-privilege-dal";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
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,
TDeleteApprovalRequestDTO,
TGetAccessRequestCountDTO,
TListApprovalRequestsDTO,
TReviewAccessRequestDTO
} from "./access-approval-request-types";
type TAccessApprovalRequestServiceFactoryDep = {
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "create" | "findById" | "deleteById">;
groupAdditionalPrivilegeDAL: TGroupProjectUserAdditionalPrivilegeDALFactory;
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"
| "deleteById"
>;
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" | "findUsersByProjectId" | "findUserByProjectId"
>;
};
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
export const accessApprovalRequestServiceFactory = ({
projectDAL,
projectEnvDAL,
permissionService,
accessApprovalRequestDAL,
groupAdditionalPrivilegeDAL,
accessApprovalRequestReviewerDAL,
projectMembershipDAL,
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
additionalPrivilegeDAL,
smtpService,
userDAL
}: TAccessApprovalRequestServiceFactoryDep) => {
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" });
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
});
if (approvers.some((approver) => !approver.approverUserId)) {
throw new BadRequestError({ message: "Policy approvers must be assigned to users" });
}
const approverUsers = await userDAL.findUsersByProjectId(
project.id,
approvers.map((approver) => approver.approverUserId!)
);
const requestedByUser = await userDAL.findUserByProjectId(project.id, actorId);
if (!requestedByUser) throw new BadRequestError({ message: "User not found in project" });
const duplicateRequests = await accessApprovalRequestDAL.find({
policyId: policy.id,
requestedByUserId: actorId,
permissions: JSON.stringify(requestedPermissions),
isTemporary
});
if (duplicateRequests?.length > 0) {
for await (const duplicateRequest of duplicateRequests) {
let foundPrivilege: Pick<
TProjectUserAdditionalPrivilege,
"temporaryAccessEndTime" | "isTemporary" | "id"
> | null = null;
if (duplicateRequest.projectUserPrivilegeId) {
foundPrivilege = await additionalPrivilegeDAL.findById(duplicateRequest.projectUserPrivilegeId);
} else if (duplicateRequest.groupProjectUserPrivilegeId) {
foundPrivilege = await groupAdditionalPrivilegeDAL.findById(duplicateRequest.groupProjectUserPrivilegeId);
}
if (foundPrivilege) {
const isExpired = new Date() > new Date(foundPrivilege.temporaryAccessEndTime || ("" as string));
if (!isExpired || !foundPrivilege.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 requesterUser = await userDAL.findUserByProjectId(project.id, actorId);
if (!requesterUser?.projectMembershipId && !requesterUser?.groupProjectMembershipId) {
throw new BadRequestError({ message: "You don't have a membership for this project" });
}
const approvalRequest = await accessApprovalRequestDAL.create(
{
projectMembershipId: requesterUser.projectMembershipId || null,
groupMembershipId: requesterUser.groupProjectMembershipId || null,
policyId: policy.id,
requestedByUserId: actorId, // This is the user ID of the person who made the request
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 deleteAccessApprovalRequest = async ({
projectSlug,
actor,
requestId,
actorOrgId,
actorId,
actorAuthMethod
}: TDeleteApprovalRequestDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new UnauthorizedError({ message: "Project not found" });
const { membership, permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretApproval
);
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
if (!accessApprovalRequest?.projectUserPrivilegeId && !accessApprovalRequest?.groupProjectUserPrivilegeId) {
throw new BadRequestError({ message: "Access request must be approved to be deleted" });
}
if (accessApprovalRequest?.projectId !== project.id) {
throw new UnauthorizedError({ message: "Request not found in project" });
}
const approvers = await accessApprovalPolicyApproverDAL.find({
policyId: accessApprovalRequest.policyId
});
// make sure the actor (actorId) is an approver
if (!approvers.some((approver) => approver.approverUserId === actorId)) {
throw new UnauthorizedError({ message: "Only policy approvers can delete access requests" });
}
if (accessApprovalRequest.projectUserPrivilegeId) {
await additionalPrivilegeDAL.deleteById(accessApprovalRequest.projectUserPrivilegeId);
} else if (accessApprovalRequest.groupProjectUserPrivilegeId) {
await groupAdditionalPrivilegeDAL.deleteById(accessApprovalRequest.groupProjectUserPrivilegeId);
}
return { request: accessApprovalRequest };
};
const listApprovalRequests = async ({
projectSlug,
authorUserId,
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 (authorUserId) requests = requests.filter((request) => request.requestedByUserId === authorUserId);
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.requestedByUserId !== actorId && // The request wasn't made by the current user
!policy.approvers.find((approverUserId) => approverUserId === 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,
memberUserId: actorId
},
tx
);
if (!review) {
const newReview = await accessApprovalRequestReviewerDAL.create(
{
status,
requestId: accessApprovalRequest.id,
memberUserId: actorId
},
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 projectUserPrivilegeId: string | null = null;
let groupProjectMembershipId: string | null = null;
if (!accessApprovalRequest.groupMembershipId && !accessApprovalRequest.projectMembershipId) {
throw new BadRequestError({ message: "Project membership or group membership is required" });
}
// Permanent access
if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
if (accessApprovalRequest.groupMembershipId) {
// Group user privilege
const groupProjectUserAdditionalPrivilege = await groupAdditionalPrivilegeDAL.create(
{
groupProjectMembershipId: accessApprovalRequest.groupMembershipId,
requestedByUserId: accessApprovalRequest.requestedByUserId,
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
permissions: JSON.stringify(accessApprovalRequest.permissions)
},
tx
);
groupProjectMembershipId = groupProjectUserAdditionalPrivilege.id;
} else {
// Project user privilege
const privilege = await additionalPrivilegeDAL.create(
{
projectMembershipId: accessApprovalRequest.projectMembershipId!,
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
permissions: JSON.stringify(accessApprovalRequest.permissions)
},
tx
);
projectUserPrivilegeId = privilege.id;
}
} else {
// Temporary access
const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!);
const startTime = new Date();
if (accessApprovalRequest.groupMembershipId) {
// Group user privilege
const groupProjectUserAdditionalPrivilege = await groupAdditionalPrivilegeDAL.create(
{
groupProjectMembershipId: accessApprovalRequest.groupMembershipId,
requestedByUserId: accessApprovalRequest.requestedByUserId,
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
);
groupProjectMembershipId = groupProjectUserAdditionalPrivilege.id;
} else {
const privilege = await additionalPrivilegeDAL.create(
{
projectMembershipId: accessApprovalRequest.projectMembershipId!,
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
);
projectUserPrivilegeId = privilege.id;
}
}
if (projectUserPrivilegeId) {
await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { projectUserPrivilegeId }, tx);
} else if (groupProjectMembershipId) {
await accessApprovalRequestDAL.updateById(
accessApprovalRequest.id,
{ groupProjectUserPrivilegeId: groupProjectMembershipId },
tx
);
} else {
throw new BadRequestError({ message: "No privilege was created" });
}
}
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,
deleteAccessApprovalRequest,
getCount
};
};

View File

@@ -0,0 +1,38 @@
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;
authorUserId?: string;
envSlug?: string;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteApprovalRequestDTO = {
requestId: string;
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;

View File

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

View File

@@ -0,0 +1,233 @@
import { ForbiddenError } from "@casl/ability";
import { RawAxiosRequestHeaders } from "axios";
import { SecretKeyEncoding } from "@app/db/schemas";
import { request } from "@app/lib/config/request";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { validateLocalIps } from "@app/lib/validator";
import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal";
import {
LogStreamHeaders,
TCreateAuditLogStreamDTO,
TDeleteAuditLogStreamDTO,
TGetDetailsAuditLogStreamDTO,
TListAuditLogStreamDTO,
TUpdateAuditLogStreamDTO
} from "./audit-log-stream-types";
type TAuditLogStreamServiceFactoryDep = {
auditLogStreamDAL: TAuditLogStreamDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TAuditLogStreamServiceFactory = ReturnType<typeof auditLogStreamServiceFactory>;
export const auditLogStreamServiceFactory = ({
auditLogStreamDAL,
permissionService,
licenseService
}: TAuditLogStreamServiceFactoryDep) => {
const create = async ({
url,
actor,
headers = [],
actorId,
actorOrgId,
actorAuthMethod
}: TCreateAuditLogStreamDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.auditLogStreams)
throw new BadRequestError({
message: "Failed to create audit log streams due to plan restriction. Upgrade plan to create group."
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
validateLocalIps(url);
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
if (totalStreams.length >= plan.auditLogStreamLimit) {
throw new BadRequestError({
message:
"Failed to create audit log streams due to plan limit reached. Kindly contact Infisical to add more streams."
});
}
// testing connection first
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
if (headers.length)
headers.forEach(({ key, value }) => {
streamHeaders[key] = value;
});
await request
.post(
url,
{ ping: "ok" },
{
headers: streamHeaders,
// request timeout
timeout: AUDIT_LOG_STREAM_TIMEOUT,
// connection timeout
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
}
)
.catch((err) => {
throw new Error(`Failed to connect with the source ${(err as Error)?.message}`);
});
const encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined;
const logStream = await auditLogStreamDAL.create({
orgId: actorOrgId,
url,
...(encryptedHeaders
? {
encryptedHeadersCiphertext: encryptedHeaders.ciphertext,
encryptedHeadersIV: encryptedHeaders.iv,
encryptedHeadersTag: encryptedHeaders.tag,
encryptedHeadersAlgorithm: encryptedHeaders.algorithm,
encryptedHeadersKeyEncoding: encryptedHeaders.encoding
}
: {})
});
return logStream;
};
const updateById = async ({
id,
url,
actor,
headers = [],
actorId,
actorOrgId,
actorAuthMethod
}: TUpdateAuditLogStreamDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.auditLogStreams)
throw new BadRequestError({
message: "Failed to update audit log streams due to plan restriction. Upgrade plan to create group."
});
const logStream = await auditLogStreamDAL.findById(id);
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
const { orgId } = logStream;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
if (url) validateLocalIps(url);
// testing connection first
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
if (headers.length)
headers.forEach(({ key, value }) => {
streamHeaders[key] = value;
});
await request
.post(
url || logStream.url,
{ ping: "ok" },
{
headers: streamHeaders,
// request timeout
timeout: AUDIT_LOG_STREAM_TIMEOUT,
// connection timeout
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
}
)
.catch((err) => {
throw new Error(`Failed to connect with the source ${(err as Error)?.message}`);
});
const encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined;
const updatedLogStream = await auditLogStreamDAL.updateById(id, {
url,
...(encryptedHeaders
? {
encryptedHeadersCiphertext: encryptedHeaders.ciphertext,
encryptedHeadersIV: encryptedHeaders.iv,
encryptedHeadersTag: encryptedHeaders.tag,
encryptedHeadersAlgorithm: encryptedHeaders.algorithm,
encryptedHeadersKeyEncoding: encryptedHeaders.encoding
}
: {})
});
return updatedLogStream;
};
const deleteById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TDeleteAuditLogStreamDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
const logStream = await auditLogStreamDAL.findById(id);
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
const { orgId } = logStream;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
const deletedLogStream = await auditLogStreamDAL.deleteById(id);
return deletedLogStream;
};
const getById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetDetailsAuditLogStreamDTO) => {
const logStream = await auditLogStreamDAL.findById(id);
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
const { orgId } = logStream;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
const headers =
logStream?.encryptedHeadersCiphertext && logStream?.encryptedHeadersIV && logStream?.encryptedHeadersTag
? (JSON.parse(
infisicalSymmetricDecrypt({
tag: logStream.encryptedHeadersTag,
iv: logStream.encryptedHeadersIV,
ciphertext: logStream.encryptedHeadersCiphertext,
keyEncoding: logStream.encryptedHeadersKeyEncoding as SecretKeyEncoding
})
) as LogStreamHeaders[])
: undefined;
return { ...logStream, headers };
};
const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListAuditLogStreamDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
const logStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
return logStreams;
};
return {
create,
updateById,
deleteById,
getById,
list
};
};

View File

@@ -0,0 +1,27 @@
import { TOrgPermission } from "@app/lib/types";
export type LogStreamHeaders = {
key: string;
value: string;
};
export type TCreateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
url: string;
headers?: LogStreamHeaders[];
};
export type TUpdateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
id: string;
url?: string;
headers?: LogStreamHeaders[];
};
export type TDeleteAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
id: string;
};
export type TListAuditLogStreamDTO = Omit<TOrgPermission, "orgId">;
export type TGetDetailsAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
id: string;
};

View File

@@ -1,13 +1,21 @@
import { RawAxiosRequestHeaders } from "axios";
import { SecretKeyEncoding } from "@app/db/schemas";
import { request } from "@app/lib/config/request";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TAuditLogStreamDALFactory } from "../audit-log-stream/audit-log-stream-dal";
import { LogStreamHeaders } from "../audit-log-stream/audit-log-stream-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { TAuditLogDALFactory } from "./audit-log-dal";
import { TCreateAuditLogDTO } from "./audit-log-types";
type TAuditLogQueueServiceFactoryDep = {
auditLogDAL: TAuditLogDALFactory;
auditLogStreamDAL: Pick<TAuditLogStreamDALFactory, "find">;
queueService: TQueueServiceFactory;
projectDAL: Pick<TProjectDALFactory, "findById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -15,11 +23,15 @@ type TAuditLogQueueServiceFactoryDep = {
export type TAuditLogQueueServiceFactory = ReturnType<typeof auditLogQueueServiceFactory>;
// keep this timeout 5s it must be fast because else the queue will take time to finish
// audit log is a crowded queue thus needs to be fast
export const AUDIT_LOG_STREAM_TIMEOUT = 5 * 1000;
export const auditLogQueueServiceFactory = ({
auditLogDAL,
queueService,
projectDAL,
licenseService
licenseService,
auditLogStreamDAL
}: TAuditLogQueueServiceFactoryDep) => {
const pushToLog = async (data: TCreateAuditLogDTO) => {
await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, {
@@ -47,7 +59,7 @@ export const auditLogQueueServiceFactory = ({
// skip inserting if audit log retention is 0 meaning its not supported
if (ttl === 0) return;
await auditLogDAL.create({
const auditLog = await auditLogDAL.create({
actor: actor.type,
actorMetadata: actor.metadata,
userAgent,
@@ -59,6 +71,46 @@ export const auditLogQueueServiceFactory = ({
eventMetadata: event.metadata,
userAgentType
});
const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : [];
await Promise.allSettled(
logStreams.map(
async ({
url,
encryptedHeadersTag,
encryptedHeadersIV,
encryptedHeadersKeyEncoding,
encryptedHeadersCiphertext
}) => {
const streamHeaders =
encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag
? (JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding,
iv: encryptedHeadersIV,
tag: encryptedHeadersTag,
ciphertext: encryptedHeadersCiphertext
})
) as LogStreamHeaders[])
: [];
const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
if (streamHeaders.length)
streamHeaders.forEach(({ key, value }) => {
headers[key] = value;
});
return request.post(url, auditLog, {
headers,
// request timeout
timeout: AUDIT_LOG_STREAM_TIMEOUT,
// connection timeout
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
});
}
)
);
});
queueService.start(QueueName.AuditLogPrune, async () => {

View File

@@ -625,9 +625,9 @@ interface SecretApprovalReopened {
interface SecretApprovalRequest {
type: EventType.SECRET_APPROVAL_REQUEST;
metadata: {
committedBy: string;
secretApprovalRequestSlug: string;
secretApprovalRequestId: string;
committedByUser?: string | null; // Needs to be nullable for backward compatibility
};
}

View File

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

View File

@@ -5,10 +5,78 @@ import { TableName, TGroups } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
export type TGroupDALFactory = ReturnType<typeof groupDALFactory>;
export const groupDALFactory = (db: TDbClient) => {
export const groupDALFactory = (db: TDbClient, userGroupMembershipDAL: TUserGroupMembershipDALFactory) => {
const groupOrm = ormify(db, TableName.Groups);
const groupMembershipOrm = ormify(db, TableName.GroupProjectMembership);
const accessApprovalRequestOrm = ormify(db, TableName.AccessApprovalRequest);
const secretApprovalRequestOrm = ormify(db, TableName.SecretApprovalRequest);
const deleteMany = async (filterQuery: TFindFilter<TGroups>, tx?: Knex) => {
const transaction = tx || (await db.transaction());
// Find all memberships
const groups = await groupOrm.find(filterQuery, { tx: transaction });
for await (const group of groups) {
// Find all the group memberships of the groups (a group membership is which projects the group is a part of)
const groupProjectMemberships = await groupMembershipOrm.find(
{ groupId: group.id },
{
tx: transaction
}
);
// For each of those group memberships, we need to find all the members of the group that don't have a regular membership in the project
for await (const groupMembership of groupProjectMemberships) {
const members = await userGroupMembershipDAL.findGroupMembersNotInProject(
group.id,
groupMembership.projectId,
transaction
);
// We then delete all the access approval requests and secret approval requests associated with these members
await accessApprovalRequestOrm.delete(
{
groupMembershipId: groupMembership.id,
$in: {
requestedByUserId: members.map(({ user }) => user.id)
}
},
transaction
);
const policies = await (tx || db)(TableName.SecretApprovalPolicy)
.join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.where(`${TableName.Environment}.projectId`, groupMembership.projectId)
.select(selectAllTableCols(TableName.SecretApprovalPolicy));
await secretApprovalRequestOrm.delete(
{
$in: {
policyId: policies.map(({ id }) => id),
committerUserId: members.map(({ user }) => user.id)
}
},
transaction
);
}
}
await groupOrm.delete(
{
$in: {
id: groups.map((group) => group.id)
}
},
transaction
);
return groups;
};
const findGroups = async (filter: TFindFilter<TGroups>, { offset, limit, sort, tx }: TFindOpt<TGroups> = {}) => {
try {
@@ -122,9 +190,10 @@ export const groupDALFactory = (db: TDbClient) => {
};
return {
...groupOrm,
findGroups,
findByOrgId,
findAllGroupMembers,
...groupOrm
delete: deleteMany
};
};

View File

@@ -266,6 +266,9 @@ export const removeUsersFromGroupByUserIds = async ({
userIds,
userDAL,
userGroupMembershipDAL,
accessApprovalRequestDAL,
secretApprovalRequestDAL,
secretApprovalPolicyDAL,
groupProjectDAL,
projectKeyDAL,
tx: outerTx
@@ -322,20 +325,16 @@ export const removeUsersFromGroupByUserIds = async ({
});
if (membersToRemoveFromGroupNonPending.length) {
// check which projects the group is part of
const projectIds = Array.from(
new Set(
(
await groupProjectDAL.find(
{
groupId: group.id
},
{ tx }
)
).map((gp) => gp.projectId)
)
const groupProjectMemberships = await groupProjectDAL.find(
{
groupId: group.id
},
{ tx }
);
// check which projects the group is part of
const projectIds = Array.from(new Set(groupProjectMemberships.map((gp) => gp.projectId)));
// TODO: this part can be optimized
for await (const userId of userIds) {
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(userId, group.id, projectIds, tx);
@@ -353,10 +352,35 @@ export const removeUsersFromGroupByUserIds = async ({
);
}
await accessApprovalRequestDAL.delete(
{
$in: {
groupMembershipId: groupProjectMemberships
.filter((gp) => projectsToDeleteKeyFor.includes(gp.projectId))
.map((gp) => gp.id)
},
requestedByUserId: userId
},
tx
);
const projectSecretApprovalPolicies = await secretApprovalPolicyDAL.findByProjectIds(projectIds);
await secretApprovalRequestDAL.delete(
{
committerUserId: userId,
$in: {
policyId: projectSecretApprovalPolicies.map((p) => p.id)
}
},
tx
);
await userGroupMembershipDAL.delete(
{
groupId: group.id,
userId
$in: {
userId: membersToRemoveFromGroupNonPending.map((member) => member.id)
}
},
tx
);
@@ -364,12 +388,15 @@ export const removeUsersFromGroupByUserIds = async ({
}
if (membersToRemoveFromGroupPending.length) {
await userGroupMembershipDAL.delete({
groupId: group.id,
$in: {
userId: membersToRemoveFromGroupPending.map((member) => member.id)
}
});
await userGroupMembershipDAL.delete(
{
groupId: group.id,
$in: {
userId: membersToRemoveFromGroupPending.map((member) => member.id)
}
},
tx
);
}
return membersToRemoveFromGroupNonPending.concat(membersToRemoveFromGroupPending);

View File

@@ -12,9 +12,12 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { TSecretApprovalRequestDALFactory } from "../secret-approval-request/secret-approval-request-dal";
import { TGroupDALFactory } from "./group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns";
import {
@@ -41,6 +44,9 @@ type TGroupServiceFactoryDep = {
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
};
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
@@ -50,6 +56,9 @@ export const groupServiceFactory = ({
groupDAL,
groupProjectDAL,
orgDAL,
secretApprovalRequestDAL,
secretApprovalPolicyDAL,
accessApprovalRequestDAL,
userGroupMembershipDAL,
projectDAL,
projectBotDAL,
@@ -328,6 +337,9 @@ export const groupServiceFactory = ({
group,
userIds: [user.id],
userDAL,
accessApprovalRequestDAL,
secretApprovalPolicyDAL,
secretApprovalRequestDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL

View File

@@ -10,6 +10,10 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { TSecretApprovalRequestDALFactory } from "../secret-approval-request/secret-approval-request-dal";
export type TCreateGroupDTO = {
name: string;
slug?: string;
@@ -77,6 +81,9 @@ export type TRemoveUsersFromGroupByUserIds = {
group: TGroups;
userIds: string[];
userDAL: Pick<TUserDALFactory, "find" | "transaction">;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "filterProjectsByUserMembership" | "delete">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "delete">;

View File

@@ -26,9 +26,12 @@ 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 { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { TSecretApprovalRequestDALFactory } from "../secret-approval-request/secret-approval-request-dal";
import { TLdapConfigDALFactory } from "./ldap-config-dal";
import {
TCreateLdapCfgDTO,
@@ -67,6 +70,9 @@ type TLdapConfigServiceFactoryDep = {
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
};
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
@@ -78,6 +84,9 @@ export const ldapConfigServiceFactory = ({
orgBotDAL,
groupDAL,
groupProjectDAL,
accessApprovalRequestDAL,
secretApprovalPolicyDAL,
secretApprovalRequestDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
@@ -524,7 +533,10 @@ export const ldapConfigServiceFactory = ({
group,
userIds: [newUser.id],
userDAL,
secretApprovalRequestDAL,
accessApprovalRequestDAL,
userGroupMembershipDAL,
secretApprovalPolicyDAL,
groupProjectDAL,
projectKeyDAL,
tx

View File

@@ -24,6 +24,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
customAlerts: false,
auditLogs: false,
auditLogsRetentionDays: 0,
auditLogStreams: false,
auditLogStreamLimit: 3,
samlSSO: false,
scim: false,
ldap: false,

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"
}
@@ -40,6 +41,8 @@ export type TFeatureSet = {
customAlerts: false;
auditLogs: false;
auditLogsRetentionDays: 0;
auditLogStreams: false;
auditLogStreamLimit: 3;
samlSSO: false;
scim: false;
ldap: false;

View File

@@ -62,6 +62,11 @@ export const permissionDALFactory = (db: TDbClient) => {
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id`
)
.leftJoin(
TableName.GroupProjectUserAdditionalPrivilege,
`${TableName.GroupProjectUserAdditionalPrivilege}.groupProjectMembershipId`,
`${TableName.GroupProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.GroupProjectMembershipRole}.customRoleId`,
@@ -77,11 +82,34 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("projectId").withSchema(TableName.GroupProjectMembership),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("permissions").withSchema(TableName.ProjectRoles)
)
.select("permissions");
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.select(
db.ref("projectId").withSchema(TableName.GroupProjectMembership).as("groupMembershipProjectId"),
db.ref("id").withSchema(TableName.GroupProjectUserAdditionalPrivilege).as("userApId"),
db.ref("permissions").withSchema(TableName.GroupProjectUserAdditionalPrivilege).as("userApPermissions"),
db.ref("temporaryMode").withSchema(TableName.GroupProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
db.ref("isTemporary").withSchema(TableName.GroupProjectUserAdditionalPrivilege).as("userApIsTemporary"),
db.ref("temporaryRange").withSchema(TableName.GroupProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
db.ref("groupProjectMembershipId").withSchema(TableName.GroupProjectUserAdditionalPrivilege),
db
.ref("requestedByUserId")
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
.as("userApRequestedByUserId"),
const docs = await db(TableName.ProjectMembership)
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
.as("userApTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
.as("userApTemporaryAccessEndTime")
);
const projectMemberDocs = await db(TableName.ProjectMembership)
.join(
TableName.ProjectUserMembershipRole,
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
@@ -127,7 +155,7 @@ export const permissionDALFactory = (db: TDbClient) => {
);
const permission = sqlNestRelationships({
data: docs,
data: projectMemberDocs,
key: "projectId",
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
orgId,
@@ -194,6 +222,33 @@ export const permissionDALFactory = (db: TDbClient) => {
permissions: z.unknown(),
customRoleSlug: z.string().optional().nullable()
}).parse(data)
},
{
key: "userApId",
label: "additionalPrivileges" as const,
mapper: ({
groupMembershipProjectId,
groupProjectMembershipId,
userApId,
userApPermissions,
userApRequestedByUserId,
userApIsTemporary,
userApTemporaryMode,
userApTemporaryRange,
userApTemporaryAccessEndTime,
userApTemporaryAccessStartTime
}) => ({
groupProjectMembershipId,
groupMembershipProjectId,
id: userApId,
userId: userApRequestedByUserId,
permissions: userApPermissions,
temporaryRange: userApTemporaryRange,
temporaryMode: userApTemporaryMode,
temporaryAccessEndTime: userApTemporaryAccessEndTime,
temporaryAccessStartTime: userApTemporaryAccessStartTime,
isTemporary: userApIsTemporary
})
}
]
})
@@ -214,15 +269,24 @@ export const permissionDALFactory = (db: TDbClient) => {
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
const activeAdditionalPrivileges =
permission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
const activeGroupAdditionalPrivileges =
groupPermission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime, groupProjectMembershipId, groupMembershipProjectId, userId: user }) =>
groupMembershipProjectId === projectId &&
!!groupProjectMembershipId &&
user === userId &&
(!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime))
) ?? [];
return {
...(permission[0] || groupPermission[0]),
roles: [...activeRoles, ...activeGroupRoles],
additionalPrivileges: activeAdditionalPrivileges
additionalPrivileges: [...activeAdditionalPrivileges, ...activeGroupAdditionalPrivileges]
};
} catch (error) {
throw new DatabaseError({ error, name: "GetProjectPermission" });

View File

@@ -90,6 +90,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
// This is fine. This service is only used for direct user privileges, not group-based privileges
if (!userPrivilege.projectMembershipId)
throw new BadRequestError({ message: "Operation not supported for groups" });
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
@@ -138,6 +142,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
// This is fine. This service is only used for direct user privileges, not group-based privileges
if (!userPrivilege.projectMembershipId)
throw new BadRequestError({ message: "Operation not supported for groups" });
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
@@ -164,6 +172,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
// This is fine. This service is only used for direct user privileges, not group-based privileges
if (!userPrivilege.projectMembershipId)
throw new BadRequestError({ message: "Operation not supported for groups" });
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });

View File

@@ -22,9 +22,12 @@ import { TProjectMembershipDALFactory } from "@app/services/project-membership/p
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { TSecretApprovalRequestDALFactory } from "../secret-approval-request/secret-approval-request-dal";
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList } from "./scim-fns";
import {
TCreateScimGroupDTO,
@@ -64,6 +67,9 @@ type TScimServiceFactoryDep = {
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
smtpService: TSmtpService;
};
@@ -81,6 +87,9 @@ export const scimServiceFactory = ({
userGroupMembershipDAL,
projectKeyDAL,
projectBotDAL,
accessApprovalRequestDAL,
secretApprovalRequestDAL,
secretApprovalPolicyDAL,
permissionService,
smtpService
}: TScimServiceFactoryDep) => {
@@ -710,6 +719,9 @@ export const scimServiceFactory = ({
userIds: toRemoveUserIds,
userDAL,
userGroupMembershipDAL,
secretApprovalPolicyDAL,
accessApprovalRequestDAL,
secretApprovalRequestDAL,
groupProjectDAL,
projectKeyDAL,
tx

View File

@@ -20,7 +20,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId`
)
.select(tx.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover))
.select(tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover))
.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"))
@@ -33,18 +33,18 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
const doc = await sapFindQuery(tx || db, {
[`${TableName.SecretApprovalPolicy}.id` as "id"]: id
});
const formatedDoc = mergeOneToManyRelation(
const formattedDoc = mergeOneToManyRelation(
doc,
"id",
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
({ approverUserId, envId, envName: name, envSlug: slug, ...el }) => ({
...el,
envId,
environment: { id: envId, name, slug }
}),
({ approverId }) => approverId,
({ approverUserId }) => approverUserId,
"approvers"
);
return formatedDoc?.[0];
return formattedDoc?.[0];
} catch (error) {
throw new DatabaseError({ error, name: "FindById" });
}
@@ -53,22 +53,31 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
const find = async (filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, tx?: Knex) => {
try {
const docs = await sapFindQuery(tx || db, filter);
const formatedDoc = mergeOneToManyRelation(
const formattedDoc = mergeOneToManyRelation(
docs,
"id",
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
({ approverUserId, envId, envName: name, envSlug: slug, ...el }) => ({
...el,
envId,
environment: { id: envId, name, slug }
}),
({ approverId }) => approverId,
({ approverUserId }) => approverUserId,
"approvers"
);
return formatedDoc;
return formattedDoc;
} catch (error) {
throw new DatabaseError({ error, name: "Find" });
}
};
return { ...secretApprovalPolicyOrm, findById, find };
const findByProjectIds = async (projectIds: string[], tx?: Knex) => {
const policies = await (tx || db)(TableName.SecretApprovalPolicy)
.join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.whereIn(`${TableName.Environment}.projectId`, projectIds)
.select(selectAllTableCols(TableName.SecretApprovalPolicy));
return policies;
};
return { ...secretApprovalPolicyOrm, findById, find, findByProjectIds };
};

View File

@@ -7,6 +7,7 @@ import { BadRequestError } from "@app/lib/errors";
import { containsGlobPatterns } from "@app/lib/picomatch";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
@@ -29,6 +30,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
userDAL: Pick<TUserDALFactory, "findUsersByProjectId" | "findUserByProjectId">;
};
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
@@ -38,7 +40,7 @@ export const secretApprovalPolicyServiceFactory = ({
permissionService,
secretApprovalPolicyApproverDAL,
projectEnvDAL,
projectMembershipDAL
userDAL
}: TSecretApprovalPolicyServiceFactoryDep) => {
const createSecretApprovalPolicy = async ({
name,
@@ -69,11 +71,12 @@ export const secretApprovalPolicyServiceFactory = ({
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
if (!env) throw new BadRequestError({ message: "Environment not found" });
const secretApprovers = await projectMembershipDAL.find({
const secretApproverUsers = await userDAL.findUsersByProjectId(
projectId,
$in: { id: approvers }
});
if (secretApprovers.length !== approvers.length)
approvers.map((approverUserId) => approverUserId)
);
if (secretApproverUsers.length !== approvers.length)
throw new BadRequestError({ message: "Approver not found in project" });
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
@@ -87,8 +90,8 @@ export const secretApprovalPolicyServiceFactory = ({
tx
);
await secretApprovalPolicyApproverDAL.insertMany(
secretApprovers.map(({ id }) => ({
approverId: id,
secretApproverUsers.map(({ id }) => ({
approverUserId: id,
policyId: doc.id
})),
tx
@@ -132,21 +135,19 @@ export const secretApprovalPolicyServiceFactory = ({
tx
);
if (approvers) {
const secretApprovers = await projectMembershipDAL.find(
{
projectId: secretApprovalPolicy.projectId,
$in: { id: approvers }
},
{ tx }
const secretApproverUsers = await userDAL.findUsersByProjectId(
secretApprovalPolicy.projectId,
approvers.map((approverUserId) => approverUserId)
);
if (secretApprovers.length !== approvers.length)
if (secretApproverUsers.length !== approvers.length)
throw new BadRequestError({ message: "Approver not found in project" });
if (doc.approvals > secretApprovers.length)
if (doc.approvals > secretApproverUsers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await secretApprovalPolicyApproverDAL.insertMany(
secretApprovers.map(({ id }) => ({
approverId: id,
secretApproverUsers.map((user) => ({
approverUserId: user.id,
policyId: doc.id
})),
tx

View File

@@ -16,7 +16,7 @@ export type TSecretApprovalRequestDALFactory = ReturnType<typeof secretApprovalR
type TFindQueryFilter = {
projectId: string;
membershipId: string;
actorId: string;
status?: RequestState;
environment?: string;
committer?: string;
@@ -49,7 +49,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
)
.select(selectAllTableCols(TableName.SecretApprovalRequest))
.select(
tx.ref("member").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"),
tx.ref("memberUserId").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"),
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
@@ -57,14 +57,14 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
tx.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover)
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover)
);
const findById = async (id: string, tx?: Knex) => {
try {
const sql = findQuery({ [`${TableName.SecretApprovalRequest}.id` as "id"]: id }, tx || db);
const docs = await sql;
const formatedDoc = sqlNestRelationships({
const formattedDoc = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({
@@ -84,20 +84,20 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
label: "reviewers" as const,
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
},
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId }
]
});
if (!formatedDoc?.[0]) return;
if (!formattedDoc?.[0]) return;
return {
...formatedDoc[0],
policy: { ...formatedDoc[0].policy, approvers: formatedDoc[0].approvers }
...formattedDoc[0],
policy: { ...formattedDoc[0].policy, approvers: formattedDoc[0].approvers }
};
} catch (error) {
throw new DatabaseError({ error, name: "FindByIdSAR" });
}
};
const findProjectRequestCount = async (projectId: string, membershipId: string, tx?: Knex) => {
const findProjectRequestCount = async (projectId: string, approverUserId: string, tx?: Knex) => {
try {
const docs = await (tx || db)
.with(
@@ -110,12 +110,12 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicyApprover}.policyId`
)
.where({ projectId })
.where({ [`${TableName.Environment}.projectId` as "projectId"]: projectId })
.andWhere(
(bd) =>
void bd
.where(`${TableName.SecretApprovalPolicyApprover}.approverId`, membershipId)
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, approverUserId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, approverUserId)
)
.select("status", `${TableName.SecretApprovalRequest}.id`)
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
@@ -142,7 +142,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
};
const findByProjectId = async (
{ status, limit = 20, offset = 0, projectId, committer, environment, membershipId }: TFindQueryFilter,
{ status, limit = 20, offset = 0, projectId, committer, environment, actorId }: TFindQueryFilter,
tx?: Knex
) => {
try {
@@ -173,21 +173,21 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
)
.where(
stripUndefinedInWhere({
projectId,
[`${TableName.Environment}.projectId`]: projectId,
[`${TableName.Environment}.slug` as "slug"]: environment,
[`${TableName.SecretApprovalRequest}.status`]: status,
committerId: committer
committerUserId: committer
})
)
.andWhere(
(bd) =>
void bd
.where(`${TableName.SecretApprovalPolicyApprover}.approverId`, membershipId)
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, actorId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, actorId)
)
.select(selectAllTableCols(TableName.SecretApprovalRequest))
.select(
db.ref("projectId").withSchema(TableName.Environment),
db.ref("projectId").withSchema(TableName.Environment).as("envProjectId"),
db.ref("slug").withSchema(TableName.Environment).as("environment"),
db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"),
db.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
@@ -201,7 +201,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
),
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
db.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover)
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover)
)
.orderBy("createdAt", "desc");
@@ -217,7 +217,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
parentMapper: (el) => ({
...SecretApprovalRequestsSchema.parse(el),
environment: el.environment,
projectId: el.projectId,
projectId: el.envProjectId,
policy: {
id: el.policyId,
name: el.policyName,
@@ -232,9 +232,9 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
mapper: ({ reviewerMemberId: member, reviewerStatus: s }) => (member ? { member, status: s } : undefined)
},
{
key: "approverId",
key: "approverUserId",
label: "approvers" as const,
mapper: ({ approverId }) => approverId
mapper: ({ approverUserId }) => approverUserId
},
{
key: "commitId",

View File

@@ -113,7 +113,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"),
db.ref("secretCommentCiphertext").withSchema(TableName.SecretVersion).as("secVerCommentCiphertext")
);
const formatedDoc = sqlNestRelationships({
const formattedDoc = sqlNestRelationships({
data: doc,
key: "id",
parentMapper: (data) => SecretApprovalRequestsSecretsSchema.omit({ secretVersion: true }).parse(data),
@@ -212,7 +212,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
}
]
});
return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({
return formattedDoc?.map(({ secret, secretVersion, ...el }) => ({
...el,
secret: secret?.[0],
secretVersion: secretVersion?.[0]

View File

@@ -85,7 +85,7 @@ export const secretApprovalRequestServiceFactory = ({
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const { membership } = await permissionService.getProjectPermission(
await permissionService.getProjectPermission(
actor as ActorType.USER,
actorId,
projectId,
@@ -93,7 +93,7 @@ export const secretApprovalRequestServiceFactory = ({
actorOrgId
);
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, membership.id);
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, actorId);
return count;
};
@@ -111,19 +111,14 @@ export const secretApprovalRequestServiceFactory = ({
}: TListApprovalsDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const { membership } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
const approvals = await secretApprovalRequestDAL.findByProjectId({
projectId,
committer,
environment,
status,
membershipId: membership.id,
actorId,
limit,
offset
});
@@ -143,7 +138,7 @@ export const secretApprovalRequestServiceFactory = ({
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
const { policy } = secretApprovalRequest;
const { membership, hasRole } = await permissionService.getProjectPermission(
const { hasRole } = await permissionService.getProjectPermission(
actor,
actorId,
secretApprovalRequest.projectId,
@@ -152,8 +147,8 @@ export const secretApprovalRequestServiceFactory = ({
);
if (
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerId !== membership.id &&
!policy.approvers.find((approverId) => approverId === membership.id)
secretApprovalRequest.committerUserId !== actorId &&
!policy.approvers.find((approverUserId) => approverUserId === actorId)
) {
throw new UnauthorizedError({ message: "User has no access" });
}
@@ -178,7 +173,7 @@ export const secretApprovalRequestServiceFactory = ({
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const { policy } = secretApprovalRequest;
const { membership, hasRole } = await permissionService.getProjectPermission(
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
actorId,
secretApprovalRequest.projectId,
@@ -187,8 +182,8 @@ export const secretApprovalRequestServiceFactory = ({
);
if (
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerId !== membership.id &&
!policy.approvers.find((approverId) => approverId === membership.id)
secretApprovalRequest.committerUserId !== actorId &&
!policy.approvers.find((approverUserId) => approverUserId === actorId)
) {
throw new UnauthorizedError({ message: "User has no access" });
}
@@ -196,7 +191,7 @@ export const secretApprovalRequestServiceFactory = ({
const review = await secretApprovalRequestReviewerDAL.findOne(
{
requestId: secretApprovalRequest.id,
member: membership.id
memberUserId: actorId
},
tx
);
@@ -205,7 +200,7 @@ export const secretApprovalRequestServiceFactory = ({
{
status,
requestId: secretApprovalRequest.id,
member: membership.id
memberUserId: actorId
},
tx
);
@@ -228,7 +223,7 @@ export const secretApprovalRequestServiceFactory = ({
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const { policy } = secretApprovalRequest;
const { membership, hasRole } = await permissionService.getProjectPermission(
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
actorId,
secretApprovalRequest.projectId,
@@ -237,8 +232,8 @@ export const secretApprovalRequestServiceFactory = ({
);
if (
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerId !== membership.id &&
!policy.approvers.find((approverId) => approverId === membership.id)
secretApprovalRequest.committerUserId !== actorId &&
!policy.approvers.find((approverUserId) => approverUserId === actorId)
) {
throw new UnauthorizedError({ message: "User has no access" });
}
@@ -251,8 +246,9 @@ export const secretApprovalRequestServiceFactory = ({
const updatedRequest = await secretApprovalRequestDAL.updateById(secretApprovalRequest.id, {
status,
statusChangeBy: membership.id
statusChangeByUserId: actorId
});
return { ...secretApprovalRequest, ...updatedRequest };
};
@@ -268,7 +264,7 @@ export const secretApprovalRequestServiceFactory = ({
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const { policy, folderId, projectId } = secretApprovalRequest;
const { membership, hasRole } = await permissionService.getProjectPermission(
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
actorId,
projectId,
@@ -278,8 +274,8 @@ export const secretApprovalRequestServiceFactory = ({
if (
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerId !== membership.id &&
!policy.approvers.find((approverId) => approverId === membership.id)
secretApprovalRequest.committerUserId !== actorId &&
!policy.approvers.find((approverUserId) => approverUserId === actorId)
) {
throw new UnauthorizedError({ message: "User has no access" });
}
@@ -290,7 +286,7 @@ export const secretApprovalRequestServiceFactory = ({
const hasMinApproval =
secretApprovalRequest.policy.approvals <=
secretApprovalRequest.policy.approvers.filter(
(approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
(approverUserId) => reviewers[approverUserId.toString()] === ApprovalStatus.APPROVED
).length;
if (!hasMinApproval) throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
@@ -445,7 +441,7 @@ export const secretApprovalRequestServiceFactory = ({
conflicts: JSON.stringify(conflicts),
hasMerged: true,
status: RequestState.Closed,
statusChangeBy: membership.id
statusChangeByUserId: actorId
},
tx
);
@@ -480,7 +476,7 @@ export const secretApprovalRequestServiceFactory = ({
}: TGenerateSecretApprovalRequestDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const { permission, membership } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
@@ -634,7 +630,7 @@ export const secretApprovalRequestServiceFactory = ({
policyId: policy.id,
status: "open",
hasMerged: false,
committerId: membership.id
committerUserId: actorId
},
tx
);

View File

@@ -614,3 +614,29 @@ export const INTEGRATION = {
integrationId: "The ID of the integration object."
}
};
export const AUDIT_LOG_STREAMS = {
CREATE: {
url: "The HTTP URL to push logs to.",
headers: {
desc: "The HTTP headers attached for the external prrovider requests.",
key: "The HTTP header key name.",
value: "The HTTP header value."
}
},
UPDATE: {
id: "The ID of the audit log stream to update.",
url: "The HTTP URL to push logs to.",
headers: {
desc: "The HTTP headers attached for the external prrovider requests.",
key: "The HTTP header key name.",
value: "The HTTP header value."
}
},
DELETE: {
id: "The ID of the audit log stream to delete."
},
GET_BY_ID: {
id: "The ID of the audit log stream to get details."
}
};

View File

@@ -119,6 +119,7 @@ const envSchema = z
})
.transform((data) => ({
...data,
isCloud: Boolean(data.LICENSE_SERVER_KEY),
isSmtpConfigured: Boolean(data.SMTP_HOST),
isRedisConfigured: Boolean(data.REDIS_URL),
isDevelopmentMode: data.NODE_ENV === "development",

View File

@@ -17,7 +17,7 @@ export type TOrgPermission = {
actorId: string;
orgId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string | undefined;
actorOrgId: string;
};
export type TProjectPermission = {

View File

@@ -1 +1,2 @@
export { isDisposableEmail } from "./validate-email";
export { validateLocalIps } from "./validate-url";

View File

@@ -0,0 +1,18 @@
import { getConfig } from "../config/env";
import { BadRequestError } from "../errors";
export const validateLocalIps = (url: string) => {
const validUrl = new URL(url);
const appCfg = getConfig();
// on cloud local ips are not allowed
if (
appCfg.isCloud &&
(validUrl.host === "host.docker.internal" ||
validUrl.host.match(/^10\.\d+\.\d+\.\d+/) ||
validUrl.host.match(/^192\.168\.\d+\.\d+/))
)
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
if (validUrl.host === "localhost" || validUrl.host === "127.0.0.1")
throw new BadRequestError({ message: "Localhost not allowed" });
};

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,9 +2,17 @@ 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";
import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-dal";
import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
@@ -14,6 +22,7 @@ import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secre
import { groupDALFactory } from "@app/ee/services/group/group-dal";
import { groupServiceFactory } from "@app/ee/services/group/group-service";
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { groupProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/group-project-user-additional-privilege/group-project-user-additional-privilege-dal";
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
@@ -193,6 +202,7 @@ export const registerRoutes = async (
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
const auditLogDAL = auditLogDALFactory(db);
const auditLogStreamDAL = auditLogStreamDALFactory(db);
const trustedIpDAL = trustedIpDALFactory(db);
const telemetryDAL = telemetryDALFactory(db);
@@ -202,6 +212,14 @@ 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 groupProjectUserAdditionalPrivilegeDAL = groupProjectUserAdditionalPrivilegeDALFactory(db);
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
@@ -215,10 +233,10 @@ export const registerRoutes = async (
const gitAppInstallSessionDAL = gitAppInstallSessionDALFactory(db);
const gitAppOrgDAL = gitAppDALFactory(db);
const groupDAL = groupDALFactory(db);
const userGroupMembershipDAL = userGroupMembershipDALFactory(db);
const groupDAL = groupDALFactory(db, userGroupMembershipDAL);
const groupProjectDAL = groupProjectDALFactory(db);
const groupProjectMembershipRoleDAL = groupProjectMembershipRoleDALFactory(db);
const userGroupMembershipDAL = userGroupMembershipDALFactory(db);
const secretScanningDAL = secretScanningDALFactory(db);
const licenseDAL = licenseDALFactory(db);
const dynamicSecretDAL = dynamicSecretDALFactory(db);
@@ -243,16 +261,24 @@ export const registerRoutes = async (
auditLogDAL,
queueService,
projectDAL,
licenseService
licenseService,
auditLogStreamDAL
});
const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue });
const auditLogStreamService = auditLogStreamServiceFactory({
licenseService,
permissionService,
auditLogStreamDAL
});
const sapService = secretApprovalPolicyServiceFactory({
projectMembershipDAL,
projectEnvDAL,
secretApprovalPolicyApproverDAL: sapApproverDAL,
userDAL,
permissionService,
secretApprovalPolicyDAL
});
const samlService = samlConfigServiceFactory({
permissionService,
orgBotDAL,
@@ -266,10 +292,13 @@ export const registerRoutes = async (
groupDAL,
groupProjectDAL,
orgDAL,
secretApprovalPolicyDAL,
userGroupMembershipDAL,
projectDAL,
projectBotDAL,
projectKeyDAL,
secretApprovalRequestDAL,
accessApprovalRequestDAL,
permissionService,
licenseService
});
@@ -277,7 +306,10 @@ export const registerRoutes = async (
groupDAL,
groupProjectDAL,
groupProjectMembershipRoleDAL,
secretApprovalPolicyDAL,
secretApprovalRequestDAL,
userGroupMembershipDAL,
accessApprovalRequestDAL,
projectDAL,
projectKeyDAL,
projectBotDAL,
@@ -292,7 +324,10 @@ export const registerRoutes = async (
projectDAL,
projectMembershipDAL,
groupDAL,
secretApprovalPolicyDAL,
groupProjectDAL,
secretApprovalRequestDAL,
accessApprovalRequestDAL,
userGroupMembershipDAL,
projectKeyDAL,
projectBotDAL,
@@ -305,7 +340,10 @@ export const registerRoutes = async (
ldapGroupMapDAL,
orgDAL,
orgBotDAL,
secretApprovalPolicyDAL,
groupDAL,
secretApprovalRequestDAL,
accessApprovalRequestDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
@@ -397,6 +435,7 @@ export const registerRoutes = async (
projectUserMembershipRoleDAL,
projectDAL,
permissionService,
groupProjectDAL,
projectBotDAL,
orgDAL,
userDAL,
@@ -571,6 +610,31 @@ export const registerRoutes = async (
secretVersionTagDAL,
secretQueueService
});
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
permissionService,
projectEnvDAL,
userDAL,
projectDAL
});
const accessApprovalRequestService = accessApprovalRequestServiceFactory({
projectDAL,
permissionService,
accessApprovalRequestReviewerDAL,
additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL,
groupAdditionalPrivilegeDAL: groupProjectUserAdditionalPrivilegeDAL,
projectMembershipDAL,
accessApprovalPolicyDAL,
accessApprovalRequestDAL,
projectEnvDAL,
userDAL,
smtpService,
accessApprovalPolicyApproverDAL
});
const secretRotationQueue = secretRotationQueueFactory({
telemetryService,
secretRotationDAL,
@@ -707,6 +771,8 @@ export const registerRoutes = async (
identityProject: identityProjectService,
identityUa: identityUaService,
secretApprovalPolicy: sapService,
accessApprovalPolicy: accessApprovalPolicyService,
accessApprovalRequest: accessApprovalRequestService,
secretApprovalRequest: sarService,
secretRotation: secretRotationService,
dynamicSecret: dynamicSecretService,
@@ -715,6 +781,7 @@ export const registerRoutes = async (
saml: samlService,
ldap: ldapService,
auditLog: auditLogService,
auditLogStream: auditLogStreamService,
secretScanning: secretScanningService,
license: licenseService,
trustedIp: trustedIpService,

View File

@@ -69,3 +69,10 @@ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
keyEncoding: true,
algorithm: true
});
export const SanitizedAuditLogStreamSchema = z.object({
id: z.string(),
url: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});

View File

@@ -68,9 +68,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
params: z.object({
workspaceId: z.string().trim()
}),
querystring: z.object({
includeGroupMembers: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
}),
response: {
200: z.object({
users: ProjectMembershipsSchema.extend({
isGroupMember: z.boolean(),
user: UsersSchema.pick({
email: true,
username: true,
@@ -104,6 +111,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
includeGroupMembers: req.query.includeGroupMembers,
projectId: req.params.workspaceId,
actorOrgId: req.permission.orgId
});

View File

@@ -76,6 +76,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
.object({
id: z.string(),
name: z.string(),
slug: z.string(),
organization: z.string(),
environments: z
.object({

View File

@@ -915,7 +915,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: approval.committerId,
committedByUser: approval.committerUserId,
secretApprovalRequestId: approval.id,
secretApprovalRequestSlug: approval.slug
}
@@ -1099,7 +1099,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: approval.committerId,
committedByUser: approval.committerUserId,
secretApprovalRequestId: approval.id,
secretApprovalRequestSlug: approval.slug
}
@@ -1230,14 +1230,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
]
}
});
await server.services.auditLog.createAuditLog({
projectId: req.body.workspaceId,
...req.auditLogInfo,
event: {
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: approval.committerId,
committedByUser: approval.committerUserId,
secretApprovalRequestId: approval.id,
secretApprovalRequestSlug: approval.slug
}
@@ -1363,7 +1362,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: approval.committerId,
committedByUser: approval.committerUserId,
secretApprovalRequestId: approval.id,
secretApprovalRequestSlug: approval.slug
}
@@ -1490,7 +1489,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: approval.committerId,
committedByUser: approval.committerUserId,
secretApprovalRequestId: approval.id,
secretApprovalRequestSlug: approval.slug
}
@@ -1604,7 +1603,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: approval.committerId,
committedByUser: approval.committerUserId,
secretApprovalRequestId: approval.id,
secretApprovalRequestSlug: approval.slug
}

View File

@@ -82,7 +82,7 @@ export const authSignupServiceFactory = ({
await smtpService.sendMail({
template: SmtpTemplates.EmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [email],
recipients: [user.email as string],
substitutions: {
code: token
}

View File

@@ -1,7 +1,7 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, sqlNestRelationships } from "@app/lib/knex";
@@ -10,6 +10,100 @@ export type TGroupProjectDALFactory = ReturnType<typeof groupProjectDALFactory>;
export const groupProjectDALFactory = (db: TDbClient) => {
const groupProjectOrm = ormify(db, TableName.GroupProjectMembership);
// The GroupProjectMembership table has a reference to the project (projectId) AND the group (groupId).
// We need to join the GroupProjectMembership table with the Groups table to get the group name and slug.
// We also need to join the GroupProjectMembershipRole table to get the role of the group in the project.
const findAllProjectGroupMembers = async (projectId: string) => {
const docs = await db(TableName.UserGroupMembership)
// Join the GroupProjectMembership table with the Groups table to get the group name and slug.
.join(
TableName.GroupProjectMembership,
`${TableName.UserGroupMembership}.groupId`,
`${TableName.GroupProjectMembership}.groupId` // this gives us access to the project id in the group membership
)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.join<TUserEncryptionKeys>(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id`
)
.join(
TableName.GroupProjectMembershipRole,
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.GroupProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.select(
db.ref("id").withSchema(TableName.GroupProjectMembership),
db.ref("isGhost").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("role").withSchema(TableName.GroupProjectMembershipRole),
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("membershipRoleId"),
db.ref("customRoleId").withSchema(TableName.GroupProjectMembershipRole),
db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("temporaryMode").withSchema(TableName.GroupProjectMembershipRole),
db.ref("isTemporary").withSchema(TableName.GroupProjectMembershipRole),
db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole),
db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole)
)
.where({ isGhost: false });
const members = sqlNestRelationships({
data: docs,
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId }) => ({
isGroupMember: true,
id,
userId,
projectId,
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }
}),
key: "id",
childrenMapper: [
{
label: "roles" as const,
key: "membershipRoleId",
mapper: ({
role,
customRoleId,
customRoleName,
customRoleSlug,
membershipRoleId,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
}) => ({
id: membershipRoleId,
role,
customRoleId,
customRoleName,
customRoleSlug,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
})
}
]
});
return members;
};
const findByProjectId = async (projectId: string, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.GroupProjectMembership)
@@ -95,5 +189,5 @@ export const groupProjectDALFactory = (db: TDbClient) => {
}
};
return { ...groupProjectOrm, findByProjectId };
return { ...groupProjectOrm, findByProjectId, findAllProjectGroupMembers };
};

View File

@@ -2,8 +2,11 @@ import { ForbiddenError } from "@casl/ability";
import ms from "ms";
import { ProjectMembershipRole, SecretKeyEncoding } from "@app/db/schemas";
import { TAccessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
@@ -39,6 +42,9 @@ type TGroupProjectServiceFactoryDep = {
projectBotDAL: TProjectBotDALFactory;
groupDAL: Pick<TGroupDALFactory, "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
};
export type TGroupProjectServiceFactory = ReturnType<typeof groupProjectServiceFactory>;
@@ -48,6 +54,9 @@ export const groupProjectServiceFactory = ({
groupProjectDAL,
groupProjectMembershipRoleDAL,
userGroupMembershipDAL,
secretApprovalRequestDAL,
secretApprovalPolicyDAL,
accessApprovalRequestDAL,
projectDAL,
projectKeyDAL,
projectBotDAL,
@@ -277,7 +286,8 @@ export const groupProjectServiceFactory = ({
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
if (!groupProjectMembership) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
if (!groupProjectMembership.id)
throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
const { permission } = await permissionService.getProjectPermission(
actor,
@@ -289,8 +299,34 @@ export const groupProjectServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => {
// This is group members that do not have individual access to the project (A.K.A members that don't have a normal project membership)
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id, tx);
// Delete all access approvals by the group members
await accessApprovalRequestDAL.delete(
{
groupMembershipId: groupProjectMembership.id,
$in: {
requestedByUserId: groupMembers.map((member) => member.user.id)
}
},
tx
);
const secretApprovalPolicies = await secretApprovalPolicyDAL.findByProjectIds([project.id], tx);
// Delete any secret approvals by the group members
await secretApprovalRequestDAL.delete(
{
$in: {
policyId: secretApprovalPolicies.map((policy) => policy.id),
committerUserId: groupMembers.map((member) => member.user.id)
}
},
tx
);
if (groupMembers.length) {
await projectKeyDAL.delete(
{

View File

@@ -1,12 +1,74 @@
import { Knex } from "knex";
import { Tables } from "knex/types/tables";
import { TDbClient } from "@app/db";
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
export type TProjectMembershipDALFactory = ReturnType<typeof projectMembershipDALFactory>;
export const projectMembershipDALFactory = (db: TDbClient) => {
const projectMemberOrm = ormify(db, TableName.ProjectMembership);
const projectMembershipOrm = ormify(db, TableName.ProjectMembership);
const accessApprovalRequestOrm = ormify(db, TableName.AccessApprovalRequest);
const secretApprovalRequestOrm = ormify(db, TableName.SecretApprovalRequest);
const deleteMany = async (filter: TFindFilter<Tables[TableName.ProjectMembership]["base"]>, tx?: Knex) => {
const handleDeletion = async (processedTx: Knex) => {
// Find all memberships
const memberships = await projectMembershipOrm.find(filter, {
tx: processedTx
});
// Delete all access approvals in this project from the users attached to these memberships
await accessApprovalRequestOrm.delete(
{
$in: {
projectMembershipId: memberships.map((membership) => membership.id)
}
},
processedTx
);
for await (const membership of memberships) {
const allPoliciesInProject = await (tx || db)(TableName.SecretApprovalRequest)
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id`
)
.where({ [`${TableName.Environment}.projectId` as "projectId"]: membership.projectId })
.where({ [`${TableName.SecretApprovalRequest}.committerUserId` as "committerUserId"]: membership.userId })
.select(db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"));
await secretApprovalRequestOrm.delete(
{
$in: {
policyId: allPoliciesInProject.map((policy) => policy.policyId)
},
committerUserId: membership.userId
},
processedTx
);
// Delete the actual project memberships
await projectMembershipOrm.delete(
{
id: membership.id
},
processedTx
);
}
return memberships;
};
if (tx) {
return handleDeletion(tx);
}
return db.transaction(handleDeletion);
};
// special query
const findAllProjectMembers = async (projectId: string) => {
@@ -54,6 +116,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
const members = sqlNestRelationships({
data: docs,
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId }) => ({
isGroupMember: false,
id,
userId,
projectId,
@@ -152,8 +215,9 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
};
return {
...projectMemberOrm,
...projectMembershipOrm,
findAllProjectMembers,
delete: deleteMany,
findProjectGhostUser,
findMembershipsByUsername,
findProjectMembershipsByUserId

View File

@@ -19,6 +19,7 @@ import { groupBy } from "@app/lib/fn";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
import { ActorType } from "../auth/auth-type";
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
@@ -52,6 +53,7 @@ type TProjectMembershipServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
groupProjectDAL: TGroupProjectDALFactory;
};
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
@@ -61,6 +63,7 @@ export const projectMembershipServiceFactory = ({
projectMembershipDAL,
projectUserMembershipRoleDAL,
smtpService,
groupProjectDAL,
projectRoleDAL,
projectBotDAL,
orgDAL,
@@ -74,6 +77,7 @@ export const projectMembershipServiceFactory = ({
actorId,
actor,
actorOrgId,
includeGroupMembers,
actorAuthMethod,
projectId
}: TGetProjectMembershipDTO) => {
@@ -86,7 +90,20 @@ export const projectMembershipServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
return projectMembershipDAL.findAllProjectMembers(projectId);
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
if (includeGroupMembers) {
const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId);
const allMembers = [...projectMembers, ...groupMembers];
// Ensure the userId is unique
const membersIds = new Set(allMembers.map((entity) => entity.user.id));
const uniqueMembers = allMembers.filter((entity) => membersIds.has(entity.user.id));
return uniqueMembers;
}
return projectMembers;
};
const addUsersToProject = async ({

View File

@@ -1,6 +1,8 @@
import { TProjectPermission } from "@app/lib/types";
export type TGetProjectMembershipDTO = TProjectPermission;
export type TGetProjectMembershipDTO = {
includeGroupMembers?: boolean;
} & TProjectPermission;
export enum ProjectUserMembershipTemporaryMode {
Relative = "relative"
}

View File

@@ -20,6 +20,7 @@ export enum SmtpTemplates {
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

@@ -10,7 +10,7 @@ import {
TUserEncryptionKeysUpdate
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TUserDALFactory = ReturnType<typeof userDALFactory>;
@@ -63,6 +63,99 @@ export const userDALFactory = (db: TDbClient) => {
}
};
const findUsersByProjectId = async (projectId: string, userIds: string[]) => {
try {
const projectMembershipQuery = await db(TableName.ProjectMembership)
.where({ projectId })
.whereIn("userId", userIds)
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.select(selectAllTableCols(TableName.Users))
.select(db.ref("id").withSchema(TableName.ProjectMembership).as("projectMembershipId"));
const groupMembershipQuery = await db(TableName.UserGroupMembership)
.whereIn("userId", userIds)
.join(
TableName.GroupProjectMembership,
`${TableName.UserGroupMembership}.groupId`,
`${TableName.GroupProjectMembership}.groupId` // this gives us access to the project id in the group membership
)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.select(selectAllTableCols(TableName.Users))
.select(db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupProjectMembershipId"));
const projectMembershipUsers = projectMembershipQuery.map((user) => ({
...user,
projectMembershipId: user.projectMembershipId,
userGroupMembershipId: null
}));
const groupMembershipUsers = groupMembershipQuery.map((user) => ({
...user,
projectMembershipId: null,
groupProjectMembershipId: user.groupProjectMembershipId
}));
// return [...projectMembershipUsers, ...groupMembershipUsers];
// There may be duplicates in the results since a user can have both a project membership, and access through a group, so we need to filter out potential duplicates.
// We should prioritize project memberships over group memberships.
const memberships = [...projectMembershipUsers, ...groupMembershipUsers];
const uniqueMemberships = memberships.filter((user, index) => {
const firstIndex = memberships.findIndex((u) => u.id === user.id);
return firstIndex === index;
});
return uniqueMemberships;
} catch (error) {
throw new DatabaseError({ error, name: "Find users by project id" });
}
};
// if its a group membership, it should have a isGroupMembership flag
const findUserByProjectId = async (projectId: string, userId: string) => {
try {
const projectMembership = await db(TableName.ProjectMembership)
.where({ projectId, userId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.select(selectAllTableCols(TableName.Users))
.select(db.ref("id").withSchema(TableName.ProjectMembership).as("projectMembershipId"))
.first();
const groupProjectMembership = await db(TableName.UserGroupMembership)
.where({ userId })
.join(
TableName.GroupProjectMembership,
`${TableName.UserGroupMembership}.groupId`,
`${TableName.GroupProjectMembership}.groupId` // this gives us access to the project id in the group membership
)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.select(selectAllTableCols(TableName.Users))
.select(db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupProjectMembershipId"))
.first();
if (projectMembership) {
return {
...projectMembership,
projectMembershipId: projectMembership.projectMembershipId,
groupProjectMembershipId: null
};
}
if (groupProjectMembership) {
return {
...groupProjectMembership,
projectMembershipId: null,
groupProjectMembershipId: groupProjectMembership.groupProjectMembershipId
};
}
} catch (error) {
throw new DatabaseError({ error, name: "Find user by project id" });
}
};
const findUserByProjectMembershipId = async (projectMembershipId: string) => {
try {
return await db(TableName.ProjectMembership)
@@ -74,6 +167,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("*");
@@ -135,11 +239,14 @@ export const userDALFactory = (db: TDbClient) => {
return {
...userOrm,
findUserByUsername,
findUsersByProjectId,
findUserByProjectId,
findUserEncKeyByUsername,
findUserEncKeyByUserIdsBatch,
findUserEncKeyByUserId,
updateUserEncryptionByUserId,
findUserByProjectMembershipId,
findUsersByProjectMembershipIds,
upsertUserEncryptionKey,
createUserEncryption,
findOneUserAction,

View File

@@ -24,16 +24,16 @@ resolvers hostdns
timeout retry 1s
hold valid 5s
frontend master
frontend postgres_master
bind *:5433
default_backend master_backend
default_backend postgres_master_backend
frontend replicas
frontend postgres_replicas
bind *:5434
default_backend replica_backend
default_backend postgres_replica_backend
backend master_backend
backend postgres_master_backend
option httpchk GET /master
http-check expect status 200
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
@@ -41,7 +41,7 @@ backend master_backend
server postgres-2 postgres-2:5432 check port 8008 resolvers hostdns
server postgres-3 postgres-3:5432 check port 8008 resolvers hostdns
backend replica_backend
backend postgres_replica_backend
option httpchk GET /replica
http-check expect status 200
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
@@ -50,11 +50,11 @@ backend replica_backend
server postgres-3 postgres-3:5432 check port 8008 resolvers hostdns
frontend redis_frontend
frontend redis_master_frontend
bind *:6379
default_backend redis_backend
default_backend redis_master_backend
backend redis_backend
backend redis_master_backend
option tcp-check
tcp-check send AUTH\ 123456\r\n
tcp-check expect string +OK

View File

@@ -5,8 +5,8 @@ services:
image: haproxy:latest
ports:
- '7001:7000'
- '5002:5433'
- '5003:5434'
- '5002:5433' # Postgres master
- '5003:5434' # Postgres read
- '6379:6379'
- '8080:8080'
networks:
@@ -15,22 +15,18 @@ services:
- source: haproxy-config
target: /usr/local/etc/haproxy/haproxy.cfg
deploy:
placement:
constraints:
- node.labels.name == node1
mode: global
infisical:
container_name: infisical-backend
image: infisical/infisical:latest-postgres
image: infisical/infisical:v0.60.1-postgres
env_file: .env
ports:
- 80:8080
environment:
- NODE_ENV=production
networks:
- infisical
secrets:
- env_file
deploy:
replicas: 5
etcd1:
image: ghcr.io/zalando/spilo-16:3.2-p2

View File

@@ -0,0 +1,82 @@
---
title: "Audit Log Streams"
description: "Learn how to stream Infisical Audit Logs to external logging providers."
---
<Info>
Audit log streams is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact team@infisical.com to purchase an enterprise license to use it.
</Info>
Infisical Audit Log Streaming enables you to transmit your organization's Audit Logs to external logging providers for monitoring and analysis.
The logs are formatted in JSON, requiring your logging provider to support JSON-based log parsing.
## Overview
<Steps>
<Step title="Navigate to Organization Settings in your sidebar." />
<Step title="Select Audit Log Streams Tab.">
![stream create](../../images/platform/audit-log-streams/stream-create.png)
</Step>
<Step title="Click on Create">
![stream create](../../images/platform/audit-log-streams/stream-inputs.png)
Provide the following values
<ParamField path="Endpoint URL" type="string" required>
The HTTPS endpoint URL of the logging provider that collects the JSON stream.
</ParamField>
<ParamField path="Headers" type="string" >
The HTTP headers for the logging provider for identification and authentication.
</ParamField>
</Step>
</Steps>
![stream listt](../../images/platform/audit-log-streams/stream-list.png)
Your Audit Logs are now ready to be streamed.
## Example Providers
### Better Stack
<Steps>
<Step title="Select Connect Source">
![better stack connect source](../../images/platform/audit-log-streams/betterstack-create-source.png)
</Step>
<Step title="Provide a name and select platform"/>
<Step title="Provide Audit Log Stream inputs">
![better stack connect](../../images/platform/audit-log-streams/betterstack-source-details.png)
1. Copy the **endpoint** from Better Stack to the **Endpoint URL** field.
3. Create a new header with key **Authorization** and set the value as **Bearer \<source token from betterstack\>**.
</Step>
</Steps>
### Datadog
<Steps>
<Step title="Navigate to API Keys section">
![api key create](../../images/platform/audit-log-streams/datadog-api-sidebar.png)
</Step>
<Step title="Select New Key and provide a key name">
![api key form](../../images/platform/audit-log-streams/data-create-api-key.png)
![api key form](../../images/platform/audit-log-streams/data-dog-api-key.png)
</Step>
<Step title="Find your Datadog region specific logging endpoint.">
![datadog url](../../images/platform/audit-log-streams/datadog-logging-endpoint.png)
1. Navigate to the [Datadog Send Logs API documentation](https://docs.datadoghq.com/api/latest/logs/?code-lang=curl&site=us5#send-logs).
2. Pick your Datadog account region.
3. Obtain your Datadog logging endpoint URL.
</Step>
<Step title="Provide audit log stream inputs">
![datadog api key details](../../images/platform/audit-log-streams/datadog-source-details.png)
1. Copy the **logging endpoint** from Datadog to the **Endpoint URL** field.
2. Copy the **API Key** from previous step
3. Create a new header with key **DD-API-KEY** and set the value as **API Key**.
</Step>
</Steps>

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 KiB

View File

@@ -143,7 +143,8 @@
"documentation/platform/dynamic-secrets/aws-iam"
]
},
"documentation/platform/groups"
"documentation/platform/groups",
"documentation/platform/audit-log-streams"
]
},
{
@@ -190,7 +191,6 @@
"group": "Self-host Infisical",
"pages": [
"self-hosting/overview",
"self-hosting/configuration/requirements",
{
"group": "Installation methods",
"pages": [
@@ -201,6 +201,7 @@
]
},
"self-hosting/configuration/envars",
"self-hosting/configuration/requirements",
{
"group": "Guides",
"pages": [
@@ -479,6 +480,9 @@
"api-reference/endpoints/secrets/read",
"api-reference/endpoints/secrets/update",
"api-reference/endpoints/secrets/delete",
"api-reference/endpoints/secrets/create-many",
"api-reference/endpoints/secrets/update-many",
"api-reference/endpoints/secrets/delete-many",
"api-reference/endpoints/secrets/attach-tags",
"api-reference/endpoints/secrets/detach-tags"
]

View File

@@ -1,5 +1,5 @@
---
title: "Requirements"
title: "Hardware requirements"
description: "Find out the minimal requirements for operating Infisical."
---

View File

@@ -63,426 +63,9 @@ For the sake of simplicity, the example in this guide only contains one manager
It's important to note that while the cluster can tolerate the failure of one node in a three-node setup, it's recommended to have a minimum of three nodes to ensure high availability.
With two nodes, the failure of a single node can result in a loss of quorum and potential downtime.
## Docker Deployment Stack
## Docker Deployment Stack Overview
<Tabs>
<Tab title="Docker Swarm stack">
```yaml infisical-stack.yaml
version: "3"
services:
haproxy:
image: haproxy:latest
ports:
- '7001:7000'
- '5002:5433'
- '5003:5434'
- '6379:6379'
- '8080:8080'
networks:
- infisical
configs:
- source: haproxy-config
target: /usr/local/etc/haproxy/haproxy.cfg
deploy:
placement:
constraints:
- node.labels.name == node1
infisical:
container_name: infisical-backend
image: infisical/infisical:v0.60.0-postgres
env_file: .env
ports:
- 80:8080
environment:
- NODE_ENV=production
networks:
- infisical
secrets:
- env_file
etcd1:
image: ghcr.io/zalando/spilo-16:3.2-p2
networks:
- infisical
environment:
ETCD_UNSUPPORTED_ARCH: arm64
container_name: demo-etcd1
deploy:
placement:
constraints:
- node.labels.name == node1
hostname: etcd1
command: |
etcd --name etcd1
--listen-client-urls http://0.0.0.0:2379
--listen-peer-urls=http://0.0.0.0:2380
--advertise-client-urls http://etcd1:2379
--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
--initial-advertise-peer-urls=http://etcd1:2380
--initial-cluster-state=new
etcd2:
image: ghcr.io/zalando/spilo-16:3.2-p2
networks:
- infisical
environment:
ETCD_UNSUPPORTED_ARCH: arm64
container_name: demo-etcd2
hostname: etcd2
deploy:
placement:
constraints:
- node.labels.name == node2
command: |
etcd --name etcd2
--listen-client-urls http://0.0.0.0:2379
--listen-peer-urls=http://0.0.0.0:2380
--advertise-client-urls http://etcd2:2379
--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
--initial-advertise-peer-urls=http://etcd2:2380
--initial-cluster-state=new
etcd3:
image: ghcr.io/zalando/spilo-16:3.2-p2
networks:
- infisical
environment:
ETCD_UNSUPPORTED_ARCH: arm64
container_name: demo-etcd3
hostname: etcd3
deploy:
placement:
constraints:
- node.labels.name == node3
command: |
etcd --name etcd3
--listen-client-urls http://0.0.0.0:2379
--listen-peer-urls=http://0.0.0.0:2380
--advertise-client-urls http://etcd3:2379
--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
--initial-advertise-peer-urls=http://etcd3:2380
--initial-cluster-state=new
spolo1:
image: ghcr.io/zalando/spilo-16:3.2-p2
container_name: postgres-1
networks:
- infisical
hostname: postgres-1
environment:
ETCD_HOSTS: etcd1:2379,etcd2:2379,etcd3:2379
PGPASSWORD_SUPERUSER: "postgres"
PGUSER_SUPERUSER: "postgres"
SCOPE: infisical
volumes:
- postgres_data1:/home/postgres/pgdata
deploy:
placement:
constraints:
- node.labels.name == node1
spolo2:
image: ghcr.io/zalando/spilo-16:3.2-p2
container_name: postgres-2
networks:
- infisical
hostname: postgres-2
environment:
ETCD_HOSTS: etcd1:2379,etcd2:2379,etcd3:2379
PGPASSWORD_SUPERUSER: "postgres"
PGUSER_SUPERUSER: "postgres"
SCOPE: infisical
volumes:
- postgres_data2:/home/postgres/pgdata
deploy:
placement:
constraints:
- node.labels.name == node2
spolo3:
image: ghcr.io/zalando/spilo-16:3.2-p2
container_name: postgres-3
networks:
- infisical
hostname: postgres-3
environment:
ETCD_HOSTS: etcd1:2379,etcd2:2379,etcd3:2379
PGPASSWORD_SUPERUSER: "postgres"
PGUSER_SUPERUSER: "postgres"
SCOPE: infisical
volumes:
- postgres_data3:/home/postgres/pgdata
deploy:
placement:
constraints:
- node.labels.name == node3
redis_replica0:
image: bitnami/redis:6.2.10
environment:
- REDIS_REPLICATION_MODE=master
- REDIS_PASSWORD=123456
networks:
- infisical
deploy:
placement:
constraints:
- node.labels.name == node1
redis_replica1:
image: bitnami/redis:6.2.10
environment:
- REDIS_REPLICATION_MODE=slave
- REDIS_MASTER_HOST=redis_replica0
- REDIS_MASTER_PORT_NUMBER=6379
- REDIS_MASTER_PASSWORD=123456
- REDIS_PASSWORD=123456
networks:
- infisical
deploy:
placement:
constraints:
- node.labels.name == node2
redis_replica2:
image: bitnami/redis:6.2.10
environment:
- REDIS_REPLICATION_MODE=slave
- REDIS_MASTER_HOST=redis_replica0
- REDIS_MASTER_PORT_NUMBER=6379
- REDIS_MASTER_PASSWORD=123456
- REDIS_PASSWORD=123456
networks:
- infisical
deploy:
placement:
constraints:
- node.labels.name == node3
redis_sentinel1:
image: bitnami/redis-sentinel:6.2.10
environment:
- REDIS_SENTINEL_QUORUM=2
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=5000
- REDIS_SENTINEL_FAILOVER_TIMEOUT=60000
- REDIS_SENTINEL_PORT_NUMBER=26379
- REDIS_MASTER_HOST=redis_replica1
- REDIS_MASTER_PORT_NUMBER=6379
- REDIS_MASTER_PASSWORD=123456
networks:
- infisical
deploy:
placement:
constraints:
- node.labels.name == node1
redis_sentinel2:
image: bitnami/redis-sentinel:6.2.10
environment:
- REDIS_SENTINEL_QUORUM=2
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=5000
- REDIS_SENTINEL_FAILOVER_TIMEOUT=60000
- REDIS_SENTINEL_PORT_NUMBER=26379
- REDIS_MASTER_HOST=redis_replica1
- REDIS_MASTER_PORT_NUMBER=6379
- REDIS_MASTER_PASSWORD=123456
networks:
- infisical
deploy:
placement:
constraints:
- node.labels.name == node2
redis_sentinel3:
image: bitnami/redis-sentinel:6.2.10
environment:
- REDIS_SENTINEL_QUORUM=2
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=5000
- REDIS_SENTINEL_FAILOVER_TIMEOUT=60000
- REDIS_SENTINEL_PORT_NUMBER=26379
- REDIS_MASTER_HOST=redis_replica1
- REDIS_MASTER_PORT_NUMBER=6379
- REDIS_MASTER_PASSWORD=123456
networks:
- infisical
deploy:
placement:
constraints:
- node.labels.name == node3
networks:
infisical:
volumes:
postgres_data1:
postgres_data2:
postgres_data3:
postgres_data4:
redis0:
redis1:
redis2:
configs:
haproxy-config:
file: ./haproxy.cfg
secrets:
env_file:
file: .env
```
</Tab>
<Tab title="HA Proxy config">
```text haproxy.cfg
global
maxconn 10000
log stdout format raw local0
defaults
log global
mode tcp
retries 3
timeout client 30m
timeout connect 10s
timeout server 30m
timeout check 5s
listen stats
mode http
bind *:7000
stats enable
stats uri /
resolvers hostdns
nameserver dns 127.0.0.11:53
resolve_retries 3
timeout resolve 1s
timeout retry 1s
hold valid 5s
frontend master
bind *:5433
default_backend master_backend
frontend replicas
bind *:5434
default_backend replica_backend
backend master_backend
option httpchk GET /master
http-check expect status 200
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
server postgres-1 postgres-1:5432 check port 8008 resolvers hostdns
server postgres-2 postgres-2:5432 check port 8008 resolvers hostdns
server postgres-3 postgres-3:5432 check port 8008 resolvers hostdns
backend replica_backend
option httpchk GET /replica
http-check expect status 200
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
server postgres-1 postgres-1:5432 check port 8008 resolvers hostdns
server postgres-2 postgres-2:5432 check port 8008 resolvers hostdns
server postgres-3 postgres-3:5432 check port 8008 resolvers hostdns
frontend redis_frontend
bind *:6379
default_backend redis_backend
backend redis_backend
option tcp-check
tcp-check send AUTH\ 123456\r\n
tcp-check expect string +OK
tcp-check send PING\r\n
tcp-check expect string +PONG
tcp-check send info\ replication\r\n
tcp-check expect string role:master
tcp-check send QUIT\r\n
tcp-check expect string +OK
server redis_master redis_replica0:6379 check inter 1s
server redis_replica1 redis_replica1:6379 check inter 1s
server redis_replica2 redis_replica2:6379 check inter 1s
frontend infisical_frontend
bind *:8080
default_backend infisical_backend
backend infisical_backend
option httpchk GET /api/status
http-check expect status 200
server infisical infisical:8080 check inter 1s
```
</Tab>
<Tab title=".example-env">
```env .env
# Keys
# Required key for platform encryption/decryption ops
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# JWT
# Required secrets to sign JWT tokens
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
DB_CONNECTION_URI=postgres://infisical:infisical@haproxy:5433/infisical?sslmode=no-verify
# Redis
REDIS_URL=redis://:123456@haproxy:6379
# Website URL
# Required
SITE_URL=http://localhost:8080
# Mail/SMTP
SMTP_HOST=
SMTP_PORT=
SMTP_NAME=
SMTP_USERNAME=
SMTP_PASSWORD=
# Integration
# Optional only if integration is used
CLIENT_ID_HEROKU=
CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB=
CLIENT_ID_GITLAB=
CLIENT_ID_BITBUCKET=
CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB=
CLIENT_SECRET_GITLAB=
CLIENT_SECRET_BITBUCKET=
CLIENT_SLUG_VERCEL=
# Sentry (optional) for monitoring errors
SENTRY_DSN=
# Infisical Cloud-specific configs
# Ignore - Not applicable for self-hosted version
POSTHOG_HOST=
POSTHOG_PROJECT_API_KEY=
# SSO-specific variables
CLIENT_ID_GOOGLE_LOGIN=
CLIENT_SECRET_GOOGLE_LOGIN=
CLIENT_ID_GITHUB_LOGIN=
CLIENT_SECRET_GITHUB_LOGIN=
CLIENT_ID_GITLAB_LOGIN=
CLIENT_SECRET_GITLAB_LOGIN=
```
</Tab>
</Tabs>
The provided Docker stack YAML file defines the services and their configurations for deploying Infisical with high availability. The main components of this stack are as follows.
The [Docker stack file](https://github.com/Infisical/infisical/tree/main/docker-swarm) used in this guide defines the services and their configurations for deploying Infisical in a highly available manner. The main components of this stack are as follows.
1. **HAProxy**: The HAProxy service is configured to expose ports for accessing PostgreSQL (5433 for the master, 5434 for replicas), Redis master (6379), and the Infisical backend (8080). It uses a config file (`haproxy.cfg`) to define the load balancing and health check rules.
@@ -496,42 +79,34 @@ The provided Docker stack YAML file defines the services and their configuration
6. **Redis Sentinel**: Three Redis Sentinel instances (redis_sentinel1, redis_sentinel2, redis_sentinel3) are deployed, one on each node, to monitor and manage the Redis instances. They are connected to the `infisical` network.
## HAProxy Configuration
## Deployment instructions
The HAProxy configuration file (`haproxy.cfg`) defines the load balancing and health check rules for the PostgreSQL and Redis instances.
1. **Stats**: This section enables the HAProxy statistics dashboard, accessible at port 7000.
2. **Resolvers**: This section defines the DNS resolver for service discovery, using the Docker embedded DNS server.
3. **Frontend**: There are separate frontend sections for the PostgreSQL master (port 5433), PostgreSQL replicas (port 5434), Redis (port 6379), and the Infisical backend (port 8080). Each frontend binds to the respective port and defines the default backend.
4. **Backend**: The backend sections define the servers and health check rules for each service.
- For PostgreSQL, there are separate backends for the master and replicas. The health check is performed using an HTTP request to the `/master` or `/replica` endpoint, expecting a 200 status code.
- For Redis, the backend uses a TCP health check with authentication and expects the role to be `master` for the Redis master instance.
- For the Infisical backend, the health check is performed using an HTTP request to the `/api/status` endpoint, expecting a 200 status code.
## Setting Up Docker Nodes
1. Initialize Docker Swarm on one of the VMs by running the following command:
```
docker swarm init --advertise-addr <MANAGER_NODE_IP>
<Steps>
<Step title="Initialize Docker Swarm on one of the VMs by running the following command">
```
docker swarm init
```
Replace `<MANAGER_NODE_IP>` with the IP address of the VM that will serve as the manager node. Remember to copy the join token returned by the this init command.
<Tip>
For the sake of simplicity, we only use one manager node in this example deployment. However, in production settings, we recommended you have at least 3 manager nodes.
</Tip>
</Step>
2. On the other VMs, join the Docker Swarm by running the command provided by the manager node:
```
<Step title="On the other VMs, join the Docker Swarm by running the command provided by the manager node">
```
docker swarm join --token <JOIN_TOKEN> <MANAGER_NODE_IP>:2377
```
Replace `<JOIN_TOKEN>` with the token provided by the manager node during initialization.
3. Label the nodes with `node.labels.name` to specify their roles. For example:
</Step>
```
<Step title="Label the nodes with `node.labels.name` to specify their roles.">
Labels on nodes will help us select where stateful components such as Postgres and Redis are deployed on. To label nodes, follow the steps below.
```
docker node update --label-add name=node1 <NODE1_ID>
docker node update --label-add name=node2 <NODE2_ID>
docker node update --label-add name=node3 <NODE3_ID>
@@ -540,32 +115,102 @@ The HAProxy configuration file (`haproxy.cfg`) defines the load balancing and he
Replace `<NODE1_ID>`, `<NODE2_ID>`, and `<NODE3_ID>` with the respective node IDs.
To view the list of nodes and their ids, run the following on the manager node `docker node ls`.
## Deploying the Docker Stack
</Step>
1. Copy the provided Docker stack YAML file and the HAProxy configuration file to the manager node.
<Step title="Copy deployment assets to manager node">
Copy the Docker stack YAML file, HAProxy configuration file and example `.env` file to the manager node. Ensure that all 3 files are placed in the same file directory.
- [Docker stack file](https://github.com/Infisical/infisical/blob/main/docker-swarm/stack.yaml) (rename to infisical-stack.yaml)
- [HA configuration file](https://github.com/Infisical/infisical/blob/main/docker-swarm/haproxy.cfg) (rename to haproxy.cfg)
- [Example .env file](https://github.com/Infisical/infisical/blob/main/docker-swarm/.env-example) (rename to .env)
</Step>
2. Deploy the stack using the following command:
<Step title="Deploy stack">
```
docker stack deploy -c infisical-stack.yaml infisical
```
</Step>
This command deploys the stack with the specified configuration.
3. Run the [schema migration](/self-hosting/configuration/schema-migrations) to initialize the database.
To connect to the Postgres database, use the following default credentials: username: `postgres` and password: `postgres`.
<Step title="Check service status">
```plain
$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
4kzq3ub8qgn9 infisical_etcd1 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2
tqx9t82bn8d9 infisical_etcd2 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2
t8vbkrasy8fz infisical_etcd3 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2
77iei42fcf6q infisical_haproxy global 4/4 haproxy:latest *:5002-5003->5433-5434/tcp, *:6379->6379/tcp, *:7001->7000/tcp, *:8080->8080/tcp
jaewzqy8md56 infisical_infisical replicated 5/5 infisical/infisical:v0.60.1-postgres
58w4zablfbtb infisical_redis_replica0 replicated 1/1 bitnami/redis:6.2.10
w4yag2whq0un infisical_redis_replica1 replicated 1/1 bitnami/redis:6.2.10
w03mriy0jave infisical_redis_replica2 replicated 1/1 bitnami/redis:6.2.10
ppo6rk47hc9t infisical_redis_sentinel1 replicated 1/1 bitnami/redis-sentinel:6.2.10
ub29vd0lnq7f infisical_redis_sentinel2 replicated 1/1 bitnami/redis-sentinel:6.2.10
szg3yky7yji2 infisical_redis_sentinel3 replicated 1/1 bitnami/redis-sentinel:6.2.10
eqtocpf5tiy0 infisical_spolo1 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2
3lznscvk7k5t infisical_spolo2 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2
v04ml7rz2j5q infisical_spolo3 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2
```
## Scaling and Resilience
<Note>
You'll notice that service `infisical_infisical` will not be in running state.
This is expected as the database does not yet have the desired schemas.
Once the database schema migrations have been successfully applied, this issue should be resolved.
</Note>
</Step>
To further scale and make the system more resilient, you can add more nodes to the Docker Swarm and update the stack configuration accordingly:
<Step title="Run schema migrations">
Run the schema migration to initialize the database. Follow the [guide here](/self-hosting/configuration/schema-migrations) to learn how.
1. Add new VMs and join them to the Docker Swarm as worker nodes.
To connect to the Postgres database, use the following default credentials defined in the Docker swarm: username: `postgres`, password: `postgres` and database: `postgres`.
</Step>
2. Update the Docker stack YAML file to include the new nodes in the `deploy` section of the relevant services, specifying the appropriate `node.labels.name` constraints.
<Step title="View service status">
![HA Proxy stats](/images/self-hosting/deployment-options/docker-swarm/ha-proxy-ha.png)
To view the health of services in your Infisical cluster, visit port `<NODE-IP>:7001` of any node in your Docker swarm.
This port will expose the HA Proxy stats.
3. Update the HAProxy configuration file (`haproxy.cfg`) to include the new nodes in the backend sections for PostgreSQL and Redis.
Run the following command to view the IPs of the nodes in your docker swarm.
4. Redeploy the updated stack using the `docker stack deploy` command.
```plain
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
0jnegl4gpo235l66nglcwc07t localhost Ready Active 26.0.2
no1a7zwj88057k73m196ulkq6 * localhost Ready Active Leader 26.0.2
wcb2x27w3tq7ht4v1h7ke49qk localhost Ready Active 26.0.2
zov5q7uop7wpxc2ndz712v9oa localhost Ready Active 26.0.2
```
Note that the database containers (PostgreSQL) are stateful and cannot be simply replicated. Instead, one database instance is deployed per node to ensure data consistency and avoid conflicts.
<Info>
The stats page may take 1-2 minutes to become accessible.
</Info>
</Step>
<Check>Once all services are running as expected, you may visit the IP address of the node where the HA Proxy was deployed. This should take you to the Infisical installation wizard.</Check>
<Step title="Initialize Infisical">
![self host sign up](/images/self-hosting/applicable-to-all/selfhost-signup.png)
Once all expected services are up and running, visit `<NODE-IP>:8080` of any node in the swarm. This will take you to the Infisical configuration page.
</Step>
</Steps>
## FAQ
<Accordion title="How do I scale Infisical cluster further?" defaultOpen="true">
To further scale and make the system more resilient, you can add more nodes to the Docker Swarm and update the stack configuration accordingly:
1. Add new VMs and join them to the Docker Swarm as worker nodes.
2. Update the Docker stack YAML file to include the new nodes in the `deploy` section of the relevant services, specifying the appropriate `node.labels.name` constraints.
3. Update the HAProxy configuration file (`haproxy.cfg`) to include the new nodes in the backend sections for PostgreSQL and Redis.
4. Redeploy the updated stack using the `docker stack deploy` command.
Note that the database containers (PostgreSQL) are stateful and cannot be simply replicated. Instead, one database instance is deployed per node to ensure data consistency and avoid conflicts.
</Accordion>
<Accordion title="How do I configure backups for Postgres and Redis?">
Native tooling for scheduled backups of Postgres and Redis is currently in development.
In the meantime, we recommend using a variety of open-source tools available for this purpose.
For Postgres, [Spilo](https://github.com/zalando/spilo) provides built-in support for scheduled data dumps.
You can explore other third party tools for managing db backups, one such tool is [docker-db-backup](https://github.com/tiredofit/docker-db-backup).
</Accordion>

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>
);
};

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