Compare commits

...

185 Commits

Author SHA1 Message Date
Daniel Hougaard
787f8318fe updated locks 2024-10-03 23:50:53 +04:00
Daniel Hougaard
9a27873af5 requested changes 2024-10-03 23:50:53 +04:00
Daniel Hougaard
0abab57d83 fix: variable naming 2024-10-03 23:50:53 +04:00
Daniel Hougaard
d5662dfef4 feat: allow creation of multiple project envs 2024-10-03 23:50:53 +04:00
Daniel Hougaard
ee2ee48b47 Merge pull request #2528 from Infisical/meet/fix-mustache-import-error
fix: change mustache import
2024-10-03 23:30:18 +04:00
Daniel Hougaard
896d977b95 fixed typescript 2024-10-03 23:12:10 +04:00
Meet
d1966b60a8 fix: ldif module import 2024-10-04 00:19:25 +05:30
Daniel Hougaard
e3cbcf5853 Merge pull request #2526 from Infisical/daniel/integration-not-found-error
fix(api): integration not found error
2024-10-03 18:35:35 +04:00
Daniel Hougaard
bdf1f7c601 Update integration-service.ts 2024-10-03 18:30:17 +04:00
Daniel Hougaard
24b23d4f90 Merge pull request #2482 from Infisical/daniel/shorter-share-url
feat(secret-sharing): server-side encryption
2024-10-03 17:48:12 +04:00
Meet Shah
09c1a5f778 Merge pull request #2516 from Infisical/meet/eng-1610-ldap-like-engine-for-infisical
feat: add LDAP support for dynamic secrets
2024-10-03 16:59:55 +05:30
Meet
73a9cf01f3 feat: add better error message 2024-10-03 16:44:57 +05:30
Meet
97e860cf21 feat: add better error on invalid LDIF 2024-10-03 16:37:21 +05:30
Meet
25f694bbdb feat: Improve docs and add docs button 2024-10-03 09:56:27 +05:30
Maidul Islam
fd254fbeec Merge pull request #2484 from Infisical/daniel/fix-operator-crd-update
fix(k8-operator): updating CRD does not reflect in operator
2024-10-02 17:33:52 -04:00
Meet
859c556425 feat: Add documentation and refactor 2024-10-02 22:55:48 +05:30
Daniel Hougaard
a3cad030e5 Merge pull request #2522 from Infisical/daniel/integration-router-fixes
fix: made all update fields optional
2024-10-02 20:27:53 +04:00
Scott Wilson
342e9f99d3 Merge pull request #2519 from scott-ray-wilson/folder-navigation-filter-behavior
Improvement: Store and Clear Filters on Secret Dashboard Navigation
2024-10-02 09:21:14 -07:00
Daniel Hougaard
8ed04d0b75 fix: made all update fields optional 2024-10-02 20:09:31 +04:00
Meet
5b5a8ff03f chore: switch to bad request error 2024-10-02 21:20:42 +05:30
Meet
e0199084ad fix: refactor and handle modify 2024-10-02 20:51:02 +05:30
Scott Wilson
67a6deed72 Merge pull request #2521 from akhilmhdh/fix/create-identity
feat: added a default empty array for create-identity
2024-10-02 07:40:25 -07:00
=
355113e15d fix: changed least privilege check for identity for action array consideration 2024-10-02 19:52:27 +05:30
=
40c589eced fix: update not getting the tag in identity modal 2024-10-02 19:21:44 +05:30
=
ec4f175f73 feat: added a default empty array for create-identity 2024-10-02 19:06:02 +05:30
Tuan Dang
2273c21eb2 Clean PR 2024-10-02 09:10:22 -04:00
Daniel Hougaard
97c2b15e29 fix: secret sharing view count 2024-10-02 15:20:06 +04:00
Daniel Hougaard
2f90ee067b Merge pull request #2520 from Infisical/daniel/better-k8-auth-logs
fix(k8-auth): better errors
2024-10-02 14:27:37 +04:00
Daniel Hougaard
7b64288019 Update identity-kubernetes-auth-service.ts 2024-10-02 13:39:15 +04:00
Sheen
e6e1ed7ca9 Merge pull request #2512 from Infisical/feat/enforce-oidc-sso
feat: enforce oidc sso
2024-10-02 11:42:31 +08:00
Sheen Capadngan
73838190fd Merge remote-tracking branch 'origin/main' into feat/enforce-oidc-sso 2024-10-02 11:01:03 +08:00
Maidul Islam
d32fad87d1 Merge pull request #2485 from akhilmhdh/feat/permission-ui
New project permission ui
2024-10-01 15:24:55 -04:00
=
67db9679fa feat: removed not needed tooltip 2024-10-02 00:39:45 +05:30
=
3edd48a8b3 feat: updated plus button 2024-10-02 00:39:45 +05:30
=
a4091bfcdd feat: removed console in test 2024-10-02 00:39:44 +05:30
=
24483631a0 feat: removed discard icon 2024-10-02 00:39:44 +05:30
=
0f74a1a011 feat: updated layout and fixed item not getting removed 2024-10-02 00:39:44 +05:30
=
62d6e3763b feat: added validation to check dedupe operators, loading indicator, string required rhs 2024-10-02 00:39:44 +05:30
=
39ea7a032f feat: added empty state for empty policy 2024-10-02 00:39:44 +05:30
=
3ac125f9c7 feat: fixed test, resolved another edgecase in dashboard and added label to conditions in secrets 2024-10-02 00:39:44 +05:30
=
7667a7e665 feat: resolved review comments: metadata overflow, save not working on first policy etc 2024-10-02 00:39:44 +05:30
=
d7499fc5c5 feat: removed console from overview 2024-10-02 00:39:43 +05:30
=
f6885b239b feat: small text changes in kms permission 2024-10-02 00:39:43 +05:30
=
4928322cdb feat: added saml parsing attributes and injecting to metadata of a user in org scoped 2024-10-02 00:39:43 +05:30
=
77e191d63e feat: implemented ui and api for managing user,identity metadata 2024-10-02 00:39:43 +05:30
=
15c98a1d2e feat: added template based permission 2024-10-02 00:39:43 +05:30
=
ed757bdeff fix: broken import due to merge conflict fix 2024-10-02 00:39:43 +05:30
=
65241ad8bf feat: updated backend permission request definition 2024-10-02 00:39:43 +05:30
=
6a7760f33f feat: updated ui for new permission 2024-10-02 00:39:42 +05:30
Sheen Capadngan
fdc62e21ef misc: addressed review comments 2024-10-02 02:10:46 +08:00
Sheen Capadngan
32f866f834 Merge remote-tracking branch 'origin/main' into feat/enforce-oidc-sso 2024-10-02 02:06:39 +08:00
Scott Wilson
fbf52850e8 feature: clear filters when navigating down and restore filters when navigating up folders in secrets dashboard 2024-10-01 09:26:25 -07:00
Maidul Islam
ab9b207f96 Merge pull request #2477 from meetcshah19/meet/eng-1519-allow-users-to-change-auth-method-in-the-ui-easily
feat: allow users to replace auth methods
2024-09-30 23:38:02 -04:00
Maidul Islam
5532b9cfea Merge pull request #2518 from akhilmhdh/fix/ui-select-long-text
feat: increase select width in org access control page and added overflow bounding for select
2024-09-30 22:47:55 -04:00
Maidul Islam
449d3f0304 Merge pull request #2490 from Infisical/meet/eng-1588-auto-migration-from-envkey
feat: add migration service to import from envkey
2024-09-30 21:48:53 -04:00
Daniel Hougaard
f0210c2607 feat: fixed UI and added permissions check to backend 2024-10-01 05:17:46 +04:00
Scott Wilson
ad88aaf17f fix: address changes 2024-09-30 16:53:42 -07:00
Daniel Hougaard
0485b56e8d fix: improvements 2024-10-01 03:51:55 +04:00
Daniel Hougaard
b65842f5c1 fix: requested changes 2024-10-01 00:16:18 +04:00
Meet
22b6e0afcd chore: refactor 2024-10-01 01:34:24 +05:30
Meet
b0e536e576 fix: improve UI and lint fix 2024-10-01 01:34:24 +05:30
Meet
54e4314e88 feat: add documentation 2024-10-01 01:34:24 +05:30
Meet
d00b1847cc feat: add UI for migration from EnvKey 2024-10-01 01:34:24 +05:30
Meet
be02617855 feat: add migration service to import from envkey 2024-10-01 01:34:18 +05:30
=
b5065f13c9 feat: increase select width in org access control page and added overflow bounding for select 2024-10-01 00:35:11 +05:30
Maidul Islam
659b6d5d19 Merge pull request #2515 from scott-ray-wilson/region-select
Feature: Add Data Region Select
2024-09-30 14:56:47 -04:00
Daniel Hougaard
9c33251c44 Update secret-sharing-service.ts 2024-09-30 22:51:42 +04:00
Daniel Hougaard
1a0896475c fix: added new identifier field for non-uuid IDs 2024-09-30 22:51:42 +04:00
Daniel Hougaard
7e820745a4 Update 20240930134623_secret-sharing-string-id.ts 2024-09-30 22:51:02 +04:00
Daniel Hougaard
fa63c150dd requested changes 2024-09-30 22:51:02 +04:00
Daniel Hougaard
1a2495a95c fix: improved root kms encryption methods 2024-09-30 22:51:02 +04:00
Daniel Hougaard
d79099946a feat(secret-sharing): server-side encryption 2024-09-30 22:51:02 +04:00
Meet
27afad583b fix: missed file 2024-10-01 00:03:47 +05:30
Maidul Islam
acde0867a0 Merge pull request #2517 from Infisical/revert-2505-revert-2494-daniel/api-errors
feat(api): better errors and documentation
2024-09-30 14:21:59 -04:00
Daniel Hougaard
d44f99bac2 Merge branch 'revert-2505-revert-2494-daniel/api-errors' of https://github.com/Infisical/infisical into revert-2505-revert-2494-daniel/api-errors 2024-09-30 22:16:32 +04:00
Daniel Hougaard
2b35e20b1d chore: rolled back bot not found errors 2024-09-30 22:16:00 +04:00
Scott Wilson
da15957c3f Merge pull request #2507 from scott-ray-wilson/integration-sync-retry-fix
Fix: Integration Sync Retry on Error Patch
2024-09-30 11:12:54 -07:00
Meet Shah
208fc3452d Merge pull request #2504 from meetcshah19/meet/add-column-exists-check
fix: check if column exists in migration
2024-09-30 23:42:22 +05:30
Maidul Islam
ba1db870a4 Merge pull request #2502 from Infisical/daniel/error-fixes
fix(api): error improvements
2024-09-30 13:51:03 -04:00
Daniel Hougaard
7885a3b0ff requested changes 2024-09-30 21:45:11 +04:00
Daniel Hougaard
66485f0464 fix: error improvements 2024-09-30 21:31:47 +04:00
Scott Wilson
0741058c1d Merge pull request #2498 from scott-ray-wilson/various-ui-improvements
Fix: Various UI Improvements, Fixes and Backend Refactoring
2024-09-30 10:19:25 -07:00
Maidul Islam
3a6e79c575 Revert "Revert "feat(api): better errors and documentation"" 2024-09-30 12:58:57 -04:00
Scott Wilson
70aa73482e fix: only display region select for cloud 2024-09-30 09:58:49 -07:00
Scott Wilson
2fa30bdd0e improvement: add info about migrating regions 2024-09-30 07:08:33 -07:00
Scott Wilson
b28fe30bba chore: add region select component 2024-09-30 07:05:23 -07:00
Scott Wilson
9ba39e99c6 feature: add region select to login/signup and improve login layout 2024-09-30 07:03:02 -07:00
Meet
0e6aed7497 feat: add LDAP support for dynamic secrets 2024-09-30 19:32:24 +05:30
Sheen
7e11fbe7a3 Merge pull request #2501 from Infisical/misc/added-proper-notif-for-changes-with-policies
misc: added proper notifs for paths with policies in overview
2024-09-30 21:15:18 +08:00
Sheen Capadngan
23abab987f feat: enforce oidc sso 2024-09-30 20:59:48 +08:00
Scott Wilson
a44b3efeb7 fix: allow errors to propogate in integration sync to facilitate retries unless final attempt 2024-09-27 17:02:20 -07:00
Meet
1992a09ac2 chore: lint fix 2024-09-28 03:20:02 +05:30
Maidul Islam
efa54e0c46 Merge pull request #2506 from Infisical/maidul-wdjhwedj
remove health checks for rds and redis
2024-09-27 17:31:19 -04:00
Maidul Islam
bde2d5e0a6 Merge pull request #2505 from Infisical/revert-2494-daniel/api-errors
Revert "feat(api): better errors and documentation"
2024-09-27 17:26:01 -04:00
Maidul Islam
4090c894fc Revert "feat(api): better errors and documentation" 2024-09-27 17:25:11 -04:00
Maidul Islam
221bde01f8 remove health checks for rds and redis 2024-09-27 17:24:09 -04:00
Meet
b191a3c2f4 fix: check if column exists in migration 2024-09-28 02:35:10 +05:30
Daniel Hougaard
032197ee9f Update access-approval-policy-fns.ts 2024-09-27 22:03:46 +04:00
Daniel Hougaard
d5a4eb609a fix: error improvements 2024-09-27 21:22:14 +04:00
Scott Wilson
e7f1980b80 improvement: switch slug to use badge 2024-09-27 09:46:16 -07:00
Daniel Hougaard
d430293c66 Merge pull request #2494 from Infisical/daniel/api-errors
feat(api): better errors and documentation
2024-09-27 20:25:10 +04:00
Daniel Hougaard
180d2692cd Re-trigger tests 2024-09-27 20:17:17 +04:00
Daniel Hougaard
433e58655a Update add-errors-to-response-schemas.ts 2024-09-27 20:12:08 +04:00
Daniel Hougaard
5ffb6b7232 fixed tests 2024-09-27 20:02:43 +04:00
Daniel Hougaard
55ca9149d5 Re-trigger tests 2024-09-27 20:02:43 +04:00
Daniel Hougaard
4ea57ca9a0 requested changes 2024-09-27 20:02:43 +04:00
Daniel Hougaard
7ac4b0b79f feat(api-docs): add error responses to API documentation 2024-09-27 20:02:43 +04:00
Daniel Hougaard
2d51ed317f feat(api): improve errors and error handling 2024-09-27 20:02:43 +04:00
Maidul Islam
02c51b05b6 Update login.mdx to remove sentence 2024-09-27 10:33:36 -04:00
Scott Wilson
cd09f03f0b chore: swap to boolean cast instead of !! 2024-09-27 07:19:57 -07:00
Sheen Capadngan
bc475e0f08 misc: added proper notifs for paths with policies in overview 2024-09-27 22:18:47 +08:00
Maidul Islam
441b008709 Merge pull request #2500 from Infisical/fix/addressed-modal-close-unresponsive
fix: address modal close unresponsive
2024-09-27 10:15:27 -04:00
Daniel Hougaard
4d81a0251e Merge pull request #2478 from Infisical/misc/approval-policy-tf-resource-prereq-1
misc: approval policy modifications for TF resource
2024-09-27 16:42:04 +04:00
Sheen Capadngan
59da513481 fix: address modal close unresponsive 2024-09-27 20:30:28 +08:00
Akhil Mohan
c17047a193 Merge pull request #2499 from akhilmhdh/doc/auth-method-fix
docs: added oidc method in login command method argument and changed order to make auth section first
2024-09-27 15:45:03 +05:30
=
f50a881273 docs: added oidc method in login command method argument and changed order to make auth section first 2024-09-27 15:32:24 +05:30
Scott Wilson
afd6dd5257 improvement: improve query param boolean handling for dashboard queries and move dashboard router to v1 2024-09-26 17:50:57 -07:00
Scott Wilson
3a43d7c5d5 improvement: add tooltip to secret table resource count and match secret icon color 2024-09-26 16:40:33 -07:00
Scott Wilson
65375886bd fix: handle overflow on dropdown content 2024-09-26 16:22:41 -07:00
Scott Wilson
8495107849 improvement: display slug for aws regions 2024-09-26 16:14:23 -07:00
Scott Wilson
c011d99b8b Merge pull request #2493 from scott-ray-wilson/secrets-overview-fix
Fix: Secrets Overview Endpoint Filter Secrets for Read Permissive Environments
2024-09-26 11:32:37 -07:00
Maidul Islam
adc3542750 Merge pull request #2495 from akhilmhdh/chore/disable-audit-log-in-cloud
feat: disabled audit log for cloud due to maintainence mode
2024-09-26 13:25:04 -04:00
=
82e3241f1b feat: disabled audit log for cloud due to maintainence mode 2024-09-26 22:32:16 +05:30
Sheen
2bca46886a Merge pull request #2466 from Infisical/misc/addressed-invalid-redirect-condition-signup-page
misc: addressed invalid redirect condition in signup invite page
2024-09-27 00:54:58 +08:00
Scott Wilson
971987c786 fix: display all envs in secrets overview header 2024-09-26 09:32:15 -07:00
Scott Wilson
cd71a13bb7 fix: refactor secrets overview endpoint to filter envs for secrets with read permissions 2024-09-26 09:24:29 -07:00
Maidul Islam
98290fe31b remove audit logs 2024-09-26 12:23:11 -04:00
Akhil Mohan
9f15fb1474 Merge pull request #2491 from akhilmhdh/feat/error-dashboard
fix: resolved permission not defined for custom org role
2024-09-26 21:36:50 +05:30
=
301a867f8b refactor: remove console 2024-09-26 21:13:31 +05:30
Maidul Islam
658a044e85 Merge pull request #2492 from Infisical/maidul-gdfvdfkw
hide audit log filter in prod
2024-09-26 11:42:37 -04:00
Maidul Islam
2c1e29445d hide audit log filter in prod 2024-09-26 11:34:30 -04:00
=
3f4c4f7418 fix: resolved permission not defined for custom org role 2024-09-26 20:43:08 +05:30
Maidul Islam
592cc13b1f Merge pull request #2488 from akhilmhdh/feat/fix-ui-paginated-secret
fix: dashboard not showing when root accessn not provided
2024-09-26 10:01:33 -04:00
Maidul Islam
e70c2f3d10 Merge pull request #2489 from akhilmhdh/feat/error-dashboard
feat: added error feedback on secret items saving for debugging
2024-09-26 07:35:37 -04:00
=
bac865eab1 feat: added error feedback on secret items saving for debugging 2024-09-26 16:42:31 +05:30
=
3d8fbc0a58 fix: dashboard not showing when root accessn not provided 2024-09-26 15:13:07 +05:30
Daniel Hougaard
1fcfab7efa feat: remove finalizers 2024-09-26 02:40:30 +04:00
Daniel Hougaard
499334eef1 fixed finalizers 2024-09-26 02:35:16 +04:00
Daniel Hougaard
9fd76b8729 chore: updated helm 2024-09-25 18:29:55 +04:00
Daniel Hougaard
80d450e980 fix(k8-operator): updating CRD does not reflect in operator 2024-09-25 18:26:50 +04:00
Maidul Islam
a1f2629366 Merge pull request #2481 from Infisical/doc/add-groups-endpoints-to-api-reference
doc: add groups endpoints to api reference documentation
2024-09-25 09:50:40 -04:00
Sheen Capadngan
bf8e1f2bfd misc: added missing filter 2024-09-25 21:36:28 +08:00
Sheen Capadngan
f7d10ceeda Merge remote-tracking branch 'origin/main' into misc/approval-policy-tf-resource-prereq-1 2024-09-25 21:15:46 +08:00
Meet Shah
095883a94e Merge pull request #2483 from Infisical/meet/fix-group-members-fetch
check user group membership correctly
2024-09-25 18:24:14 +05:30
Meet
51638b7c71 fix: check user group membership correctly 2024-09-25 18:02:32 +05:30
Sheen Capadngan
adaddad370 misc: added rate limiting 2024-09-25 18:46:44 +08:00
Sheen Capadngan
cf6ff58f16 misc: access approval prerequisites 2024-09-25 18:38:06 +08:00
Sheen Capadngan
3e3f42a8f7 doc: add groups endpoints to api reference documentation 2024-09-25 15:31:54 +08:00
Sheen Capadngan
974e21d856 fix: addressed bugs 2024-09-25 14:30:22 +08:00
Daniel Hougaard
da86338bfe Merge pull request #2480 from Infisical/daniel/fix-better-not-found-error
fix: throw not found when entity is not found
2024-09-24 21:08:42 +04:00
Daniel Hougaard
3a9a6767a0 fix: throw not found when entity is not found 2024-09-24 21:01:09 +04:00
Vlad Matsiiako
fe8a1e6ce6 Merge pull request #2476 from Infisical/daniel/fix-missing-vars-count
fix(dashboard): fix imports missing secrets counter
2024-09-24 09:46:31 -07:00
Maidul Islam
55aa3f7b58 Merge pull request #2479 from Infisical/misc/audit-log-page-warning-and-auto-select
misc: added maintenance notice to audit log page
2024-09-24 12:41:49 -04:00
Sheen Capadngan
59f3581370 misc: made it specific for cloud 2024-09-25 00:31:13 +08:00
Sheen Capadngan
ccae63936c misc: added maintenance notice to audit log page and handled project auto-select 2024-09-25 00:27:36 +08:00
Sheen Capadngan
6733349af0 misc: updated secret approval policy api to support TF usecase 2024-09-25 00:07:11 +08:00
Meet
f63c6b725b feat: allow users to replace auth methods 2024-09-24 21:07:43 +05:30
Daniel Hougaard
50b51f1810 Merge pull request #2475 from Infisical/daniel/prefix-secret-folders
fix(folders-api): prefix paths
2024-09-24 17:30:47 +04:00
Daniel Hougaard
fc39b3b0dd fix(dashboard): fix imports missing secrets counter 2024-09-24 17:24:38 +04:00
Daniel Hougaard
5964976e47 fix(folders-api): prefix paths 2024-09-24 15:49:27 +04:00
Daniel Hougaard
677a87150b Merge pull request #2474 from meetcshah19/meet/fix-group-fetch
fix: group fetch using project id
2024-09-24 01:01:58 +04:00
Meet
2469c8d0c6 fix: group listing using project id 2024-09-24 02:24:37 +05:30
Maidul Islam
dafb89d1dd Merge pull request #2473 from scott-ray-wilson/project-upgrade-banner-revision
Improvement: Project Upgrade Banner Revisions
2024-09-23 15:48:02 -04:00
Scott Wilson
8da01445e5 improvement: revise project upgrade banner to refer to secret engine version, state that upgrading is free and use lighter text for improved legibility 2024-09-23 12:36:10 -07:00
Maidul Islam
6b2273d314 update message 2024-09-23 15:32:11 -04:00
Maidul Islam
b886e66ee9 Remove service token notice 2024-09-23 15:25:36 -04:00
Scott Wilson
3afcb19727 Merge pull request #2464 from scott-ray-wilson/entra-mfa-docs
Docs: Microsoft Entra ID / Azure AD MFA
2024-09-23 12:10:38 -07:00
Meet Shah
06d2480f30 Merge pull request #2472 from meetcshah19/meet/fix-create-policy-ui
fix: group selection on create policy
2024-09-23 23:02:22 +05:30
Meet
fd7d8ddf2d fix: group selection on create policy 2024-09-23 20:59:05 +05:30
Maidul Islam
1dc0f4e5b8 Merge pull request #2431 from Infisical/misc/terraform-project-group-prereq
misc: setup prerequisites for terraform project group
2024-09-23 11:21:46 -04:00
Maidul Islam
fa64a88c24 Merge pull request #2470 from akhilmhdh/fix/inline-reference-permission
feat: added validation check for secret references made in v2 engine
2024-09-23 10:07:07 -04:00
Meet Shah
385ec05e57 Merge pull request #2458 from meetcshah19/meet/eng-1443-add-groups-as-eligible-approvers
feat: allow access approvals with user groups
2024-09-23 19:14:52 +05:30
Meet
3a38e1e413 chore: refactor 2024-09-23 19:04:57 +05:30
=
7f04e9e97d feat: added validation check for secret references made in v2 engine 2024-09-23 16:29:01 +05:30
Meet
fcbc7fcece chore: fix test 2024-09-23 10:53:58 +05:30
Meet
c2252c65a4 chore: lint fix 2024-09-23 10:30:49 +05:30
Meet
e150673de4 chore: Refactor and remove new tables 2024-09-23 10:26:58 +05:30
Sheen Capadngan
14c89c9be5 misc: addressed invalid redirect condition in signup invite page 2024-09-22 20:32:55 +08:00
Scott Wilson
ebea74b607 fix: address capitalization 2024-09-21 19:41:58 -07:00
Scott Wilson
5bbe5421bf docs: add images 2024-09-20 17:32:14 -07:00
Scott Wilson
279289989f docs: add entra / azure mfa docs 2024-09-20 17:31:32 -07:00
Meet
12ecefa832 chore: remove logs 2024-09-20 09:31:18 +05:30
Meet
dd9a00679d chore: fix type 2024-09-20 09:03:43 +05:30
Meet
081502848d feat: allow secret approvals with user groups 2024-09-20 08:51:48 +05:30
Meet
009be0ded8 feat: allow access approvals with user groups 2024-09-20 01:24:30 +05:30
329 changed files with 10447 additions and 4837 deletions

View File

@@ -1,6 +1,7 @@
import { seedData1 } from "@app/db/seed-data"; import { seedData1 } from "@app/db/seed-data";
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
const createPolicy = async (dto: { name: string; secretPath: string; approvers: string[]; approvals: number }) => { const createPolicy = async (dto: { name: string; secretPath: string; approvers: {type: ApproverType.User, id: string}[]; approvals: number }) => {
const res = await testServer.inject({ const res = await testServer.inject({
method: "POST", method: "POST",
url: `/api/v1/secret-approvals`, url: `/api/v1/secret-approvals`,
@@ -26,7 +27,7 @@ describe("Secret approval policy router", async () => {
const policy = await createPolicy({ const policy = await createPolicy({
secretPath: "/", secretPath: "/",
approvals: 1, approvals: 1,
approvers: [seedData1.id], approvers: [{id:seedData1.id, type: ApproverType.User}],
name: "test-policy" name: "test-policy"
}); });

View File

@@ -510,7 +510,7 @@ describe("Service token fail cases", async () => {
authorization: `Bearer ${serviceToken}` authorization: `Bearer ${serviceToken}`
} }
}); });
expect(fetchSecrets.statusCode).toBe(401); expect(fetchSecrets.statusCode).toBe(403);
expect(fetchSecrets.json().error).toBe("PermissionDenied"); expect(fetchSecrets.json().error).toBe("PermissionDenied");
await deleteServiceToken(); await deleteServiceToken();
}); });
@@ -532,7 +532,7 @@ describe("Service token fail cases", async () => {
authorization: `Bearer ${serviceToken}` authorization: `Bearer ${serviceToken}`
} }
}); });
expect(fetchSecrets.statusCode).toBe(401); expect(fetchSecrets.statusCode).toBe(403);
expect(fetchSecrets.json().error).toBe("PermissionDenied"); expect(fetchSecrets.json().error).toBe("PermissionDenied");
await deleteServiceToken(); await deleteServiceToken();
}); });
@@ -557,7 +557,7 @@ describe("Service token fail cases", async () => {
authorization: `Bearer ${serviceToken}` authorization: `Bearer ${serviceToken}`
} }
}); });
expect(writeSecrets.statusCode).toBe(401); expect(writeSecrets.statusCode).toBe(403);
expect(writeSecrets.json().error).toBe("PermissionDenied"); expect(writeSecrets.json().error).toBe("PermissionDenied");
// but read access should still work fine // but read access should still work fine

View File

@@ -1075,7 +1075,7 @@ describe("Secret V3 Raw Router Without E2EE enabled", async () => {
}, },
body: createSecretReqBody body: createSecretReqBody
}); });
expect(createSecRes.statusCode).toBe(400); expect(createSecRes.statusCode).toBe(404);
}); });
test("Update secret raw", async () => { test("Update secret raw", async () => {
@@ -1093,7 +1093,7 @@ describe("Secret V3 Raw Router Without E2EE enabled", async () => {
}, },
body: updateSecretReqBody body: updateSecretReqBody
}); });
expect(updateSecRes.statusCode).toBe(400); expect(updateSecRes.statusCode).toBe(404);
}); });
test("Delete secret raw", async () => { test("Delete secret raw", async () => {
@@ -1110,6 +1110,6 @@ describe("Secret V3 Raw Router Without E2EE enabled", async () => {
}, },
body: deletedSecretReqBody body: deletedSecretReqBody
}); });
expect(deletedSecRes.statusCode).toBe(400); expect(deletedSecRes.statusCode).toBe(404);
}); });
}); });

View File

@@ -61,10 +61,12 @@
"jwks-rsa": "^3.1.0", "jwks-rsa": "^3.1.0",
"knex": "^3.0.1", "knex": "^3.0.1",
"ldapjs": "^3.0.7", "ldapjs": "^3.0.7",
"ldif": "^0.5.1",
"libsodium-wrappers": "^0.7.13", "libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"mongodb": "^6.8.1", "mongodb": "^6.8.1",
"ms": "^2.1.3", "ms": "^2.1.3",
"mustache": "^4.2.0",
"mysql2": "^3.9.8", "mysql2": "^3.9.8",
"nanoid": "^3.3.4", "nanoid": "^3.3.4",
"nodemailer": "^6.9.9", "nodemailer": "^6.9.9",
@@ -85,6 +87,7 @@
"safe-regex": "^2.1.1", "safe-regex": "^2.1.1",
"scim-patch": "^0.8.3", "scim-patch": "^0.8.3",
"scim2-parse-filter": "^0.2.10", "scim2-parse-filter": "^0.2.10",
"sjcl": "^1.0.8",
"smee-client": "^2.0.0", "smee-client": "^2.0.0",
"tedious": "^18.2.1", "tedious": "^18.2.1",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
@@ -108,6 +111,7 @@
"@types/jsrp": "^0.2.6", "@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13", "@types/libsodium-wrappers": "^0.7.13",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/mustache": "^4.2.5",
"@types/node": "^20.9.5", "@types/node": "^20.9.5",
"@types/nodemailer": "^6.4.14", "@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12", "@types/passport-github": "^1.1.12",
@@ -117,6 +121,7 @@
"@types/prompt-sync": "^4.2.3", "@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6", "@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6", "@types/safe-regex": "^1.1.6",
"@types/sjcl": "^1.0.34",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0", "@typescript-eslint/parser": "^6.20.0",
@@ -7074,6 +7079,13 @@
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
}, },
"node_modules/@types/mustache": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz",
"integrity": "sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.9.5", "version": "20.9.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz",
@@ -7296,6 +7308,13 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/sjcl": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/@types/sjcl/-/sjcl-1.0.34.tgz",
"integrity": "sha512-bQHEeK5DTQRunIfQeUMgtpPsNNCcZyQ9MJuAfW1I7iN0LDunTc78Fu17STbLMd7KiEY/g2zHVApippa70h6HoQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/uuid": { "node_modules/@types/uuid": {
"version": "9.0.7", "version": "9.0.7",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz",
@@ -13008,6 +13027,12 @@
"verror": "^1.10.1" "verror": "^1.10.1"
} }
}, },
"node_modules/ldif": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/ldif/-/ldif-0.5.1.tgz",
"integrity": "sha512-8s46m/r2lSFO2+DqMxqWiJ10iiL4tuR5LC/KndV+E5//OAOzOx5s3HS5O34PJ5+kyaCA+K2oCaEPaDRfXUnQow==",
"license": "MIT"
},
"node_modules/leven": { "node_modules/leven": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
@@ -13704,6 +13729,15 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/mustache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
"license": "MIT",
"bin": {
"mustache": "bin/mustache"
}
},
"node_modules/mylas": { "node_modules/mylas": {
"version": "2.1.13", "version": "2.1.13",
"resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz", "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz",
@@ -16397,6 +16431,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/sjcl": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz",
"integrity": "sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ==",
"license": "(BSD-2-Clause OR GPL-2.0-only)",
"engines": {
"node": "*"
}
},
"node_modules/slash": { "node_modules/slash": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -17874,12 +17917,14 @@
"node_modules/tweetnacl": { "node_modules/tweetnacl": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"license": "Unlicense"
}, },
"node_modules/tweetnacl-util": { "node_modules/tweetnacl-util": {
"version": "0.15.1", "version": "0.15.1",
"resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz",
"integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==",
"license": "Unlicense"
}, },
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",

View File

@@ -71,6 +71,7 @@
"@types/jsrp": "^0.2.6", "@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13", "@types/libsodium-wrappers": "^0.7.13",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/mustache": "^4.2.5",
"@types/node": "^20.9.5", "@types/node": "^20.9.5",
"@types/nodemailer": "^6.4.14", "@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12", "@types/passport-github": "^1.1.12",
@@ -80,6 +81,7 @@
"@types/prompt-sync": "^4.2.3", "@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6", "@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6", "@types/safe-regex": "^1.1.6",
"@types/sjcl": "^1.0.34",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0", "@typescript-eslint/parser": "^6.20.0",
@@ -158,10 +160,12 @@
"jwks-rsa": "^3.1.0", "jwks-rsa": "^3.1.0",
"knex": "^3.0.1", "knex": "^3.0.1",
"ldapjs": "^3.0.7", "ldapjs": "^3.0.7",
"ldif": "^0.5.1",
"libsodium-wrappers": "^0.7.13", "libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"mongodb": "^6.8.1", "mongodb": "^6.8.1",
"ms": "^2.1.3", "ms": "^2.1.3",
"mustache": "^4.2.0",
"mysql2": "^3.9.8", "mysql2": "^3.9.8",
"nanoid": "^3.3.4", "nanoid": "^3.3.4",
"nodemailer": "^6.9.9", "nodemailer": "^6.9.9",
@@ -182,6 +186,7 @@
"safe-regex": "^2.1.1", "safe-regex": "^2.1.1",
"scim-patch": "^0.8.3", "scim-patch": "^0.8.3",
"scim2-parse-filter": "^0.2.10", "scim2-parse-filter": "^0.2.10",
"sjcl": "^1.0.8",
"smee-client": "^2.0.0", "smee-client": "^2.0.0",
"tedious": "^18.2.1", "tedious": "^18.2.1",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",

View File

@@ -38,6 +38,7 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
@@ -181,6 +182,7 @@ declare module "fastify" {
orgAdmin: TOrgAdminServiceFactory; orgAdmin: TOrgAdminServiceFactory;
slack: TSlackServiceFactory; slack: TSlackServiceFactory;
workflowIntegration: TWorkflowIntegrationServiceFactory; workflowIntegration: TWorkflowIntegrationServiceFactory;
migration: TExternalMigrationServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer // everywhere else access using service layer

View File

@@ -101,6 +101,9 @@ import {
TIdentityKubernetesAuths, TIdentityKubernetesAuths,
TIdentityKubernetesAuthsInsert, TIdentityKubernetesAuthsInsert,
TIdentityKubernetesAuthsUpdate, TIdentityKubernetesAuthsUpdate,
TIdentityMetadata,
TIdentityMetadataInsert,
TIdentityMetadataUpdate,
TIdentityOidcAuths, TIdentityOidcAuths,
TIdentityOidcAuthsInsert, TIdentityOidcAuthsInsert,
TIdentityOidcAuthsUpdate, TIdentityOidcAuthsUpdate,
@@ -546,6 +549,11 @@ declare module "knex/types/tables" {
TIdentityUniversalAuthsInsert, TIdentityUniversalAuthsInsert,
TIdentityUniversalAuthsUpdate TIdentityUniversalAuthsUpdate
>; >;
[TableName.IdentityMetadata]: KnexOriginal.CompositeTableType<
TIdentityMetadata,
TIdentityMetadataInsert,
TIdentityMetadataUpdate
>;
[TableName.IdentityKubernetesAuth]: KnexOriginal.CompositeTableType< [TableName.IdentityKubernetesAuth]: KnexOriginal.CompositeTableType<
TIdentityKubernetesAuths, TIdentityKubernetesAuths,
TIdentityKubernetesAuthsInsert, TIdentityKubernetesAuthsInsert,

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

@@ -0,0 +1,4 @@
declare module "ldif" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Untyped, the function returns `any`.
function parse(input: string, ...args: any[]): any;
}

View File

@@ -0,0 +1,76 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasAccessApproverGroupId = await knex.schema.hasColumn(
TableName.AccessApprovalPolicyApprover,
"approverGroupId"
);
const hasAccessApproverUserId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverUserId");
const hasSecretApproverGroupId = await knex.schema.hasColumn(
TableName.SecretApprovalPolicyApprover,
"approverGroupId"
);
const hasSecretApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId");
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
// add column approverGroupId to AccessApprovalPolicyApprover
if (!hasAccessApproverGroupId) {
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
}
// make approverUserId nullable
if (hasAccessApproverUserId) {
table.uuid("approverUserId").nullable().alter();
}
});
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
// add column approverGroupId to SecretApprovalPolicyApprover
if (!hasSecretApproverGroupId) {
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
}
// make approverUserId nullable
if (hasSecretApproverUserId) {
table.uuid("approverUserId").nullable().alter();
}
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasAccessApproverGroupId = await knex.schema.hasColumn(
TableName.AccessApprovalPolicyApprover,
"approverGroupId"
);
const hasAccessApproverUserId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverUserId");
const hasSecretApproverGroupId = await knex.schema.hasColumn(
TableName.SecretApprovalPolicyApprover,
"approverGroupId"
);
const hasSecretApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId");
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
if (hasAccessApproverGroupId) {
table.dropColumn("approverGroupId");
}
// make approverUserId not nullable
if (hasAccessApproverUserId) {
table.uuid("approverUserId").notNullable().alter();
}
});
// remove
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
if (hasSecretApproverGroupId) {
table.dropColumn("approverGroupId");
}
// make approverUserId not nullable
if (hasSecretApproverUserId) {
table.uuid("approverUserId").notNullable().alter();
}
});
}
}

View File

@@ -0,0 +1,24 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.IdentityMetadata))) {
await knex.schema.createTable(TableName.IdentityMetadata, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.string("key").notNullable();
tb.string("value").notNullable();
tb.uuid("orgId").notNullable();
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
tb.uuid("userId");
tb.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
tb.uuid("identityId");
tb.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
tb.timestamps(true, true, true);
});
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityMetadata);
}

View File

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

View File

@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.OidcConfig, "lastUsed"))) {
await knex.schema.alterTable(TableName.OidcConfig, (tb) => {
tb.datetime("lastUsed");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.OidcConfig, "lastUsed")) {
await knex.schema.alterTable(TableName.OidcConfig, (tb) => {
tb.dropColumn("lastUsed");
});
}
}

View File

@@ -12,7 +12,8 @@ export const AccessApprovalPoliciesApproversSchema = z.object({
policyId: z.string().uuid(), policyId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
approverUserId: z.string().uuid() approverUserId: z.string().uuid().nullable().optional(),
approverGroupId: z.string().uuid().nullable().optional()
}); });
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>; export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;

View File

@@ -0,0 +1,23 @@
// 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 IdentityMetadataSchema = z.object({
id: z.string().uuid(),
key: z.string(),
value: z.string(),
orgId: z.string().uuid(),
userId: z.string().uuid().nullable().optional(),
identityId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TIdentityMetadata = z.infer<typeof IdentityMetadataSchema>;
export type TIdentityMetadataInsert = Omit<z.input<typeof IdentityMetadataSchema>, TImmutableDBKeys>;
export type TIdentityMetadataUpdate = Partial<Omit<z.input<typeof IdentityMetadataSchema>, TImmutableDBKeys>>;

View File

@@ -31,6 +31,7 @@ export * from "./identity-aws-auths";
export * from "./identity-azure-auths"; export * from "./identity-azure-auths";
export * from "./identity-gcp-auths"; export * from "./identity-gcp-auths";
export * from "./identity-kubernetes-auths"; export * from "./identity-kubernetes-auths";
export * from "./identity-metadata";
export * from "./identity-oidc-auths"; export * from "./identity-oidc-auths";
export * from "./identity-org-memberships"; export * from "./identity-org-memberships";
export * from "./identity-project-additional-privilege"; export * from "./identity-project-additional-privilege";

View File

@@ -70,6 +70,8 @@ export enum TableName {
IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role", IdentityProjectMembershipRole = "identity_project_membership_role",
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege", IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
// used by both identity and users
IdentityMetadata = "identity_metadata",
ScimToken = "scim_tokens", ScimToken = "scim_tokens",
AccessApprovalPolicy = "access_approval_policies", AccessApprovalPolicy = "access_approval_policies",
AccessApprovalPolicyApprover = "access_approval_policies_approvers", AccessApprovalPolicyApprover = "access_approval_policies_approvers",

View File

@@ -26,7 +26,8 @@ export const OidcConfigsSchema = z.object({
isActive: z.boolean(), isActive: z.boolean(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
orgId: z.string().uuid() orgId: z.string().uuid(),
lastUsed: z.date().nullable().optional()
}); });
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>; export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;

View File

@@ -12,7 +12,8 @@ export const SecretApprovalPoliciesApproversSchema = z.object({
policyId: z.string().uuid(), policyId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
approverUserId: z.string().uuid() approverUserId: z.string().uuid().nullable().optional(),
approverGroupId: z.string().uuid().nullable().optional()
}); });
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>; export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;

View File

@@ -5,14 +5,16 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const SecretSharingSchema = z.object({ export const SecretSharingSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
encryptedValue: z.string(), encryptedValue: z.string().nullable().optional(),
iv: z.string(), iv: z.string().nullable().optional(),
tag: z.string(), tag: z.string().nullable().optional(),
hashedHex: z.string(), hashedHex: z.string().nullable().optional(),
expiresAt: z.date(), expiresAt: z.date(),
userId: z.string().uuid().nullable().optional(), userId: z.string().uuid().nullable().optional(),
orgId: z.string().uuid().nullable().optional(), orgId: z.string().uuid().nullable().optional(),
@@ -22,7 +24,9 @@ export const SecretSharingSchema = z.object({
accessType: z.string().default("anyone"), accessType: z.string().default("anyone"),
name: z.string().nullable().optional(), name: z.string().nullable().optional(),
lastViewedAt: z.date().nullable().optional(), lastViewedAt: z.date().nullable().optional(),
password: z.string().nullable().optional() password: z.string().nullable().optional(),
encryptedSecret: zodBuffer.nullable().optional(),
identifier: z.string().nullable().optional()
}); });
export type TSecretSharing = z.infer<typeof SecretSharingSchema>; export type TSecretSharing = z.infer<typeof SecretSharingSchema>;

View File

@@ -1,7 +1,9 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
import { EnforcementLevel } from "@app/lib/types"; import { EnforcementLevel } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas"; import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -10,28 +12,32 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({ server.route({
url: "/", url: "/",
method: "POST", method: "POST",
config: {
rateLimit: writeLimit
},
schema: { schema: {
body: z body: z.object({
.object({ projectSlug: z.string().trim(),
projectSlug: z.string().trim(), name: z.string().optional(),
name: z.string().optional(), secretPath: z.string().trim().default("/"),
secretPath: z.string().trim().default("/"), environment: z.string(),
environment: z.string(), approvers: z
approvers: z.string().array().min(1), .discriminatedUnion("type", [
approvals: z.number().min(1).default(1), z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
}) ])
.refine((data) => data.approvals <= data.approvers.length, { .array()
path: ["approvals"], .min(1, { message: "At least one approver should be provided" }),
message: "The number of approvals should be lower than the number of approvers." approvals: z.number().min(1).default(1),
}), enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
}),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({ const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({
actor: req.permission.type, actor: req.permission.type,
@@ -50,6 +56,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({ server.route({
url: "/", url: "/",
method: "GET", method: "GET",
config: {
rateLimit: readLimit
},
schema: { schema: {
querystring: z.object({ querystring: z.object({
projectSlug: z.string().trim() projectSlug: z.string().trim()
@@ -58,14 +67,15 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
200: z.object({ 200: z.object({
approvals: sapPubSchema approvals: sapPubSchema
.extend({ .extend({
userApprovers: z approvers: z
.object({ .object({ type: z.nativeEnum(ApproverType), id: z.string().nullable().optional() })
userId: z.string() .array()
}) .nullable()
.array(), .optional()
secretPath: z.string().optional().nullable()
}) })
.array() .array()
.nullable()
.optional()
}) })
} }
}, },
@@ -115,33 +125,37 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({ server.route({
url: "/:policyId", url: "/:policyId",
method: "PATCH", method: "PATCH",
config: {
rateLimit: writeLimit
},
schema: { schema: {
params: z.object({ params: z.object({
policyId: z.string() policyId: z.string()
}), }),
body: z body: z.object({
.object({ name: z.string().optional(),
name: z.string().optional(), secretPath: z
secretPath: z .string()
.string() .trim()
.trim() .optional()
.optional() .transform((val) => (val === "" ? "/" : val)),
.transform((val) => (val === "" ? "/" : val)), approvers: z
approvers: z.string().array().min(1), .discriminatedUnion("type", [
approvals: z.number().min(1).default(1), z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
}) ])
.refine((data) => data.approvals <= data.approvers.length, { .array()
path: ["approvals"], .min(1, { message: "At least one approver should be provided" }),
message: "The number of approvals should be lower than the number of approvers." approvals: z.number().min(1).optional(),
}), enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
}),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({ await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({
policyId: req.params.policyId, policyId: req.params.policyId,
@@ -157,6 +171,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({ server.route({
url: "/:policyId", url: "/:policyId",
method: "DELETE", method: "DELETE",
config: {
rateLimit: writeLimit
},
schema: { schema: {
params: z.object({ params: z.object({
policyId: z.string() policyId: z.string()
@@ -167,7 +184,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({ const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({
actor: req.permission.type, actor: req.permission.type,
@@ -179,4 +196,44 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
return { approval }; return { approval };
} }
}); });
server.route({
url: "/:policyId",
method: "GET",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
policyId: z.string()
}),
response: {
200: z.object({
approval: sapPubSchema.extend({
approvers: z
.object({
type: z.nativeEnum(ApproverType),
id: z.string().nullable().optional(),
name: z.string().nullable().optional()
})
.array()
.nullable()
.optional()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const approval = await server.services.accessApprovalPolicy.getAccessApprovalPolicyById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.params
});
return { approval };
}
});
}; };

View File

@@ -48,7 +48,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
params: z.object({ params: z.object({
id: z.string() id: z.string().trim().describe(GROUPS.GET_BY_ID.id)
}), }),
response: { response: {
200: GroupsSchema 200: GroupsSchema

View File

@@ -5,7 +5,7 @@ import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors"; import { UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -61,7 +61,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
handler: async (req) => { handler: async (req) => {
const { permissions, privilegePermission } = req.body; const { permissions, privilegePermission } = req.body;
if (!permissions && !privilegePermission) { if (!permissions && !privilegePermission) {
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" }); throw new UnauthorizedError({ message: "Permission or privilegePermission must be provided" });
} }
const permission = privilegePermission const permission = privilegePermission
@@ -140,7 +140,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
handler: async (req) => { handler: async (req) => {
const { permissions, privilegePermission } = req.body; const { permissions, privilegePermission } = req.body;
if (!permissions && !privilegePermission) { if (!permissions && !privilegePermission) {
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" }); throw new UnauthorizedError({ message: "Permission or privilegePermission must be provided" });
} }
const permission = privilegePermission const permission = privilegePermission
@@ -224,7 +224,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
handler: async (req) => { handler: async (req) => {
const { permissions, privilegePermission, ...updatedInfo } = req.body.privilegeDetails; const { permissions, privilegePermission, ...updatedInfo } = req.body.privilegeDetails;
if (!permissions && !privilegePermission) { if (!permissions && !privilegePermission) {
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" }); throw new UnauthorizedError({ message: "Permission or privilegePermission must be provided" });
} }
const permission = privilegePermission const permission = privilegePermission

View File

@@ -3,10 +3,11 @@ import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
import { ProjectPermissionSchema } from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs"; import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ProjectPermissionSchema, SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas"; import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {

View File

@@ -1,7 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { RateLimitSchema } from "@app/db/schemas"; import { RateLimitSchema } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -29,7 +29,7 @@ export const registerRateLimitRouter = async (server: FastifyZodProvider) => {
handler: async () => { handler: async () => {
const rateLimit = await server.services.rateLimit.getRateLimits(); const rateLimit = await server.services.rateLimit.getRateLimits();
if (!rateLimit) { if (!rateLimit) {
throw new BadRequestError({ throw new NotFoundError({
name: "Get Rate Limit Error", name: "Get Rate Limit Error",
message: "Rate limit configuration does not exist." message: "Rate limit configuration does not exist."
}); });

View File

@@ -61,7 +61,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
id: samlConfigId id: samlConfigId
}; };
} else { } else {
throw new BadRequestError({ message: "Missing sso identitier or org slug" }); throw new BadRequestError({ message: "Missing sso identifier or org slug" });
} }
const ssoConfig = await server.services.saml.getSaml(ssoLookupDetails); const ssoConfig = await server.services.saml.getSaml(ssoLookupDetails);
@@ -100,6 +100,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
async (req, profile, cb) => { async (req, profile, cb) => {
try { try {
if (!profile) throw new BadRequestError({ message: "Missing profile" }); if (!profile) throw new BadRequestError({ message: "Missing profile" });
const email = const email =
profile?.email ?? profile?.email ??
// entra sends data in this format // entra sends data in this format
@@ -123,6 +124,14 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
); );
} }
const userMetadata = Object.keys(profile.attributes || {})
.map((key) => {
// for the ones like in format: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email
const formatedKey = key.startsWith("http") ? key.split("/").at(-1) || "" : key;
return { key: formatedKey, value: String((profile.attributes as Record<string, string>)[key]) };
})
.filter((el) => el.key && !["email", "firstName", "lastName"].includes(el.key));
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({ const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
externalId: profile.nameID, externalId: profile.nameID,
email, email,
@@ -130,7 +139,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
lastName: lastName as string, lastName: lastName as string,
relayState: (req.body as { RelayState?: string }).RelayState, relayState: (req.body as { RelayState?: string }).RelayState,
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string, authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string,
metadata: userMetadata
}); });
cb(null, { isUserCompleted, providerAuthToken }); cb(null, { isUserCompleted, providerAuthToken });
} catch (error) { } catch (error) {

View File

@@ -1,6 +1,7 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { EnforcementLevel } from "@app/lib/types"; import { EnforcementLevel } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -16,32 +17,33 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z body: z.object({
.object({ workspaceId: z.string(),
workspaceId: z.string(), name: z.string().optional(),
name: z.string().optional(), environment: z.string(),
environment: z.string(), secretPath: z
secretPath: z .string()
.string() .optional()
.optional() .nullable()
.nullable() .default("/")
.default("/") .transform((val) => (val ? removeTrailingSlash(val) : val)),
.transform((val) => (val ? removeTrailingSlash(val) : val)), approvers: z
approvers: z.string().array().min(1), .discriminatedUnion("type", [
approvals: z.number().min(1).default(1), z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
}) ])
.refine((data) => data.approvals <= data.approvers.length, { .array()
path: ["approvals"], .min(1, { message: "At least one approver should be provided" }),
message: "The number of approvals should be lower than the number of approvers." approvals: z.number().min(1).default(1),
}), enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
}),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({ const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({
actor: req.permission.type, actor: req.permission.type,
@@ -67,30 +69,31 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
params: z.object({ params: z.object({
sapId: z.string() sapId: z.string()
}), }),
body: z body: z.object({
.object({ name: z.string().optional(),
name: z.string().optional(), approvers: z
approvers: z.string().array().min(1), .discriminatedUnion("type", [
approvals: z.number().min(1).default(1), z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
secretPath: z z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
.string() ])
.optional() .array()
.nullable() .min(1, { message: "At least one approver should be provided" }),
.transform((val) => (val ? removeTrailingSlash(val) : val)) approvals: z.number().min(1).default(1),
.transform((val) => (val === "" ? "/" : val)), secretPath: z
enforcementLevel: z.nativeEnum(EnforcementLevel).optional() .string()
}) .optional()
.refine((data) => data.approvals <= data.approvers.length, { .nullable()
path: ["approvals"], .transform((val) => (val ? removeTrailingSlash(val) : val))
message: "The number of approvals should be lower than the number of approvers." .transform((val) => (val === "" ? "/" : val)),
}), enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
}),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({ const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({
actor: req.permission.type, actor: req.permission.type,
@@ -120,7 +123,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({ const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({
actor: req.permission.type, actor: req.permission.type,
@@ -147,9 +150,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
200: z.object({ 200: z.object({
approvals: sapPubSchema approvals: sapPubSchema
.extend({ .extend({
userApprovers: z approvers: z
.object({ .object({
userId: z.string() id: z.string().nullable().optional(),
type: z.nativeEnum(ApproverType)
}) })
.array() .array()
}) })
@@ -170,6 +174,44 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
} }
}); });
server.route({
url: "/:sapId",
method: "GET",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
sapId: z.string()
}),
response: {
200: z.object({
approval: sapPubSchema.extend({
approvers: z
.object({
id: z.string().nullable().optional(),
type: z.nativeEnum(ApproverType),
name: z.string().nullable().optional()
})
.array()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.getSecretApprovalPolicyById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.params
});
return { approval };
}
});
server.route({ server.route({
url: "/board", url: "/board",
method: "GET", method: "GET",
@@ -186,7 +228,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
200: z.object({ 200: z.object({
policy: sapPubSchema policy: sapPubSchema
.extend({ .extend({
userApprovers: z.object({ userId: z.string() }).array() userApprovers: z.object({ userId: z.string().nullable().optional() }).array()
}) })
.optional() .optional()
}) })

View File

@@ -13,7 +13,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
const approvalRequestUser = z.object({ userId: z.string() }).merge( const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge(
UsersSchema.pick({ UsersSchema.pick({
email: true, email: true,
firstName: true, firstName: true,
@@ -46,7 +46,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
approvals: z.number(), approvals: z.number(),
approvers: z.string().array(), approvers: z
.object({
userId: z.string().nullable().optional()
})
.array(),
secretPath: z.string().optional().nullable(), secretPath: z.string().optional().nullable(),
enforcementLevel: z.string() enforcementLevel: z.string()
}), }),
@@ -54,7 +58,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(), commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
environment: z.string(), environment: z.string(),
reviewers: z.object({ userId: z.string(), status: z.string() }).array(), reviewers: z.object({ userId: z.string(), status: z.string() }).array(),
approvers: z.string().array() approvers: z
.object({
userId: z.string().nullable().optional()
})
.array()
}).array() }).array()
}) })
} }

View File

@@ -5,22 +5,38 @@ import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies } from
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
import { ApproverType } from "./access-approval-policy-types";
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>; export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
export const accessApprovalPolicyDALFactory = (db: TDbClient) => { export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy); const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy);
const accessApprovalPolicyFindQuery = async (tx: Knex, filter: TFindFilter<TAccessApprovalPolicies>) => { const accessApprovalPolicyFindQuery = async (
tx: Knex,
filter: TFindFilter<TAccessApprovalPolicies>,
customFilter?: {
policyId?: string;
}
) => {
const result = await tx(TableName.AccessApprovalPolicy) const result = await tx(TableName.AccessApprovalPolicy)
// eslint-disable-next-line // eslint-disable-next-line
.where(buildFindFilter(filter)) .where(buildFindFilter(filter))
.where((qb) => {
if (customFilter?.policyId) {
void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId);
}
})
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) .join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.leftJoin( .leftJoin(
TableName.AccessApprovalPolicyApprover, TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId` `${TableName.AccessApprovalPolicyApprover}.policyId`
) )
.leftJoin(TableName.Users, `${TableName.AccessApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
.select(tx.ref("username").withSchema(TableName.Users).as("approverUsername"))
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) .select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName")) .select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug")) .select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId")) .select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
@@ -30,10 +46,10 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
return result; return result;
}; };
const findById = async (id: string, tx?: Knex) => { const findById = async (policyId: string, tx?: Knex) => {
try { try {
const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), { const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), {
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id [`${TableName.AccessApprovalPolicy}.id` as "id"]: policyId
}); });
const formattedDoc = sqlNestRelationships({ const formattedDoc = sqlNestRelationships({
data: doc, data: doc,
@@ -50,9 +66,18 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
childrenMapper: [ childrenMapper: [
{ {
key: "approverUserId", key: "approverUserId",
label: "userApprovers" as const, label: "approvers" as const,
mapper: ({ approverUserId }) => ({ mapper: ({ approverUserId: id }) => ({
userId: approverUserId id,
type: "user"
})
},
{
key: "approverGroupId",
label: "approvers" as const,
mapper: ({ approverGroupId: id }) => ({
id,
type: "group"
}) })
} }
] ]
@@ -64,9 +89,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
} }
}; };
const find = async (filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, tx?: Knex) => { const find = async (
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
customFilter?: {
policyId?: string;
},
tx?: Knex
) => {
try { try {
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter); const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter);
const formattedDocs = sqlNestRelationships({ const formattedDocs = sqlNestRelationships({
data: docs, data: docs,
@@ -84,9 +115,19 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
childrenMapper: [ childrenMapper: [
{ {
key: "approverUserId", key: "approverUserId",
label: "userApprovers" as const, label: "approvers" as const,
mapper: ({ approverUserId }) => ({ mapper: ({ approverUserId: id, approverUsername }) => ({
userId: approverUserId id,
type: ApproverType.User,
name: approverUsername
})
},
{
key: "approverGroupId",
label: "approvers" as const,
mapper: ({ approverGroupId: id }) => ({
id,
type: ApproverType.Group
}) })
} }
] ]

View File

@@ -1,12 +1,11 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { BadRequestError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TVerifyApprovers } from "./access-approval-policy-types"; import { TIsApproversValid } from "./access-approval-policy-types";
export const verifyApprovers = async ({ export const isApproversValid = async ({
userIds, userIds,
projectId, projectId,
orgId, orgId,
@@ -14,9 +13,9 @@ export const verifyApprovers = async ({
actorAuthMethod, actorAuthMethod,
secretPath, secretPath,
permissionService permissionService
}: TVerifyApprovers) => { }: TIsApproversValid) => {
for await (const userId of userIds) { try {
try { for await (const userId of userIds) {
const { permission: approverPermission } = await permissionService.getProjectPermission( const { permission: approverPermission } = await permissionService.getProjectPermission(
ActorType.USER, ActorType.USER,
userId, userId,
@@ -29,8 +28,9 @@ export const verifyApprovers = async ({
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath }) 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" });
} }
} catch {
return false;
} }
return true;
}; };

View File

@@ -2,17 +2,21 @@ import { ForbiddenError } from "@casl/ability";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TGroupDALFactory } from "../group/group-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
import { verifyApprovers } from "./access-approval-policy-fns"; import { isApproversValid } from "./access-approval-policy-fns";
import { import {
ApproverType,
TCreateAccessApprovalPolicy, TCreateAccessApprovalPolicy,
TDeleteAccessApprovalPolicy, TDeleteAccessApprovalPolicy,
TGetAccessApprovalPolicyByIdDTO,
TGetAccessPolicyCountByEnvironmentDTO, TGetAccessPolicyCountByEnvironmentDTO,
TListAccessApprovalPoliciesDTO, TListAccessApprovalPoliciesDTO,
TUpdateAccessApprovalPolicy TUpdateAccessApprovalPolicy
@@ -25,6 +29,8 @@ type TSecretApprovalPolicyServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory; accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
groupDAL: TGroupDALFactory;
userDAL: Pick<TUserDALFactory, "find">;
}; };
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>; export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
@@ -32,9 +38,11 @@ export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprov
export const accessApprovalPolicyServiceFactory = ({ export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
groupDAL,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
projectDAL projectDAL,
userDAL
}: TSecretApprovalPolicyServiceFactoryDep) => { }: TSecretApprovalPolicyServiceFactoryDep) => {
const createAccessApprovalPolicy = async ({ const createAccessApprovalPolicy = async ({
name, name,
@@ -50,9 +58,23 @@ export const accessApprovalPolicyServiceFactory = ({
enforcementLevel enforcementLevel
}: TCreateAccessApprovalPolicy) => { }: TCreateAccessApprovalPolicy) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
if (approvals > approvers.length) // If there is a group approver people might be added to the group later to meet the approvers quota
const groupApprovers = approvers
.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id) as string[];
const userApprovers = approvers
.filter((approver) => approver.type === ApproverType.User)
.map((approver) => approver.id)
.filter(Boolean) as string[];
const userApproverNames = approvers
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
.filter(Boolean) as string[];
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -67,18 +89,65 @@ export const accessApprovalPolicyServiceFactory = ({
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id }); const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
if (!env) throw new BadRequestError({ message: "Environment not found" }); if (!env) throw new NotFoundError({ message: "Environment not found" });
await verifyApprovers({ let approverUserIds = userApprovers;
if (userApproverNames.length) {
const approverUsers = await userDAL.find({
$in: {
username: userApproverNames
}
});
const approverNamesFromDb = approverUsers.map((user) => user.username);
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
if (invalidUsernames.length) {
throw new BadRequestError({
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
});
}
approverUserIds = approverUserIds.concat(approverUsers.map((user) => user.id));
}
const usersPromises: Promise<
{
id: string;
email: string | null | undefined;
username: string;
firstName: string | null | undefined;
lastName: string | null | undefined;
isPartOfGroup: boolean;
}[]
>[] = [];
const verifyAllApprovers = [...approverUserIds];
for (const groupId of groupApprovers) {
usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }));
}
const verifyGroupApprovers = (await Promise.all(usersPromises))
.flat()
.filter((user) => user.isPartOfGroup)
.map((user) => user.id);
verifyAllApprovers.push(...verifyGroupApprovers);
const approversValid = await isApproversValid({
projectId: project.id, projectId: project.id,
orgId: actorOrgId, orgId: actorOrgId,
envSlug: environment, envSlug: environment,
secretPath, secretPath,
actorAuthMethod, actorAuthMethod,
permissionService, permissionService,
userIds: approvers userIds: verifyAllApprovers
}); });
if (!approversValid) {
throw new BadRequestError({
message: "One or more approvers doesn't have access to be specified secret path"
});
}
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => { const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.create( const doc = await accessApprovalPolicyDAL.create(
{ {
@@ -90,13 +159,26 @@ export const accessApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
await accessApprovalPolicyApproverDAL.insertMany( if (approverUserIds.length) {
approvers.map((userId) => ({ await accessApprovalPolicyApproverDAL.insertMany(
approverUserId: userId, approverUserIds.map((userId) => ({
policyId: doc.id approverUserId: userId,
})), policyId: doc.id
tx })),
); tx
);
}
if (groupApprovers) {
await accessApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((groupId) => ({
approverGroupId: groupId,
policyId: doc.id
})),
tx
);
}
return doc; return doc;
}); });
return { ...accessApproval, environment: env, projectId: project.id }; return { ...accessApproval, environment: env, projectId: project.id };
@@ -110,7 +192,7 @@ export const accessApprovalPolicyServiceFactory = ({
projectSlug projectSlug
}: TListAccessApprovalPoliciesDTO) => { }: TListAccessApprovalPoliciesDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
// Anyone in the project should be able to get the policies. // Anyone in the project should be able to get the policies.
/* const { permission } = */ await permissionService.getProjectPermission( /* const { permission } = */ await permissionService.getProjectPermission(
@@ -138,8 +220,30 @@ export const accessApprovalPolicyServiceFactory = ({
approvals, approvals,
enforcementLevel enforcementLevel
}: TUpdateAccessApprovalPolicy) => { }: TUpdateAccessApprovalPolicy) => {
const groupApprovers = approvers
.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id) as string[];
const userApprovers = approvers
.filter((approver) => approver.type === ApproverType.User)
.map((approver) => approver.id)
.filter(Boolean) as string[];
const userApproverNames = approvers
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
.filter(Boolean) as string[];
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId); const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); const currentAppovals = approvals || accessApprovalPolicy.approvals;
if (
groupApprovers?.length === 0 &&
userApprovers &&
currentAppovals > userApprovers.length + userApproverNames.length
) {
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
}
if (!accessApprovalPolicy) throw new NotFoundError({ message: "Secret approval policy not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -161,26 +265,100 @@ export const accessApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
if (approvers) {
await verifyApprovers({ await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
if (userApprovers.length || userApproverNames.length) {
let userApproverIds = userApprovers;
if (userApproverNames.length) {
const approverUsers = await userDAL.find({
$in: {
username: userApproverNames
}
});
const approverNamesFromDb = approverUsers.map((user) => user.username);
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
if (invalidUsernames.length) {
throw new BadRequestError({
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
});
}
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
}
const approversValid = await isApproversValid({
projectId: accessApprovalPolicy.projectId, projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId, orgId: actorOrgId,
envSlug: accessApprovalPolicy.environment.slug, envSlug: accessApprovalPolicy.environment.slug,
secretPath: doc.secretPath!, secretPath: doc.secretPath!,
actorAuthMethod, actorAuthMethod,
permissionService, permissionService,
userIds: approvers userIds: userApproverIds
}); });
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx); if (!approversValid) {
throw new BadRequestError({
message: "One or more approvers doesn't have access to be specified secret path"
});
}
await accessApprovalPolicyApproverDAL.insertMany( await accessApprovalPolicyApproverDAL.insertMany(
approvers.map((userId) => ({ userApproverIds.map((userId) => ({
approverUserId: userId, approverUserId: userId,
policyId: doc.id policyId: doc.id
})), })),
tx tx
); );
} }
if (groupApprovers) {
const usersPromises: Promise<
{
id: string;
email: string | null | undefined;
username: string;
firstName: string | null | undefined;
lastName: string | null | undefined;
isPartOfGroup: boolean;
}[]
>[] = [];
for (const groupId of groupApprovers) {
usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }));
}
const verifyGroupApprovers = (await Promise.all(usersPromises))
.flat()
.filter((user) => user.isPartOfGroup)
.map((user) => user.id);
const approversValid = await isApproversValid({
projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId,
envSlug: accessApprovalPolicy.environment.slug,
secretPath: doc.secretPath!,
actorAuthMethod,
permissionService,
userIds: verifyGroupApprovers
});
if (!approversValid) {
throw new BadRequestError({
message: "One or more approvers doesn't have access to be specified secret path"
});
}
await accessApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((groupId) => ({
approverGroupId: groupId,
policyId: doc.id
})),
tx
);
}
return doc; return doc;
}); });
return { return {
@@ -198,7 +376,7 @@ export const accessApprovalPolicyServiceFactory = ({
actorOrgId actorOrgId
}: TDeleteAccessApprovalPolicy) => { }: TDeleteAccessApprovalPolicy) => {
const policy = await accessApprovalPolicyDAL.findById(policyId); const policy = await accessApprovalPolicyDAL.findById(policyId);
if (!policy) throw new BadRequestError({ message: "Secret approval policy not found" }); if (!policy) throw new NotFoundError({ message: "Secret approval policy not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -226,7 +404,7 @@ export const accessApprovalPolicyServiceFactory = ({
}: TGetAccessPolicyCountByEnvironmentDTO) => { }: TGetAccessPolicyCountByEnvironmentDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const { membership } = await permissionService.getProjectPermission( const { membership } = await permissionService.getProjectPermission(
actor, actor,
@@ -235,22 +413,53 @@ export const accessApprovalPolicyServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (!membership) throw new BadRequestError({ message: "User not found in project" }); if (!membership) {
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
}
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
if (!environment) throw new BadRequestError({ message: "Environment not found" }); if (!environment) throw new NotFoundError({ message: "Environment not found" });
const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id }); const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id });
if (!policies) throw new BadRequestError({ message: "No policies found" }); if (!policies) throw new NotFoundError({ message: "No policies found" });
return { count: policies.length }; return { count: policies.length };
}; };
const getAccessApprovalPolicyById = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
policyId
}: TGetAccessApprovalPolicyByIdDTO) => {
const [policy] = await accessApprovalPolicyDAL.find({}, { policyId });
if (!policy) {
throw new NotFoundError({
message: "Cannot find access approval policy"
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
policy.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
return policy;
};
return { return {
getAccessPolicyCountByEnvSlug, getAccessPolicyCountByEnvSlug,
createAccessApprovalPolicy, createAccessApprovalPolicy,
deleteAccessApprovalPolicy, deleteAccessApprovalPolicy,
updateAccessApprovalPolicy, updateAccessApprovalPolicy,
getAccessApprovalPolicyByProjectSlug getAccessApprovalPolicyByProjectSlug,
getAccessApprovalPolicyById
}; };
}; };

View File

@@ -3,7 +3,7 @@ import { ActorAuthMethod } from "@app/services/auth/auth-type";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
export type TVerifyApprovers = { export type TIsApproversValid = {
userIds: string[]; userIds: string[];
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
envSlug: string; envSlug: string;
@@ -13,11 +13,16 @@ export type TVerifyApprovers = {
orgId: string; orgId: string;
}; };
export enum ApproverType {
Group = "group",
User = "user"
}
export type TCreateAccessApprovalPolicy = { export type TCreateAccessApprovalPolicy = {
approvals: number; approvals: number;
secretPath: string; secretPath: string;
environment: string; environment: string;
approvers: string[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
projectSlug: string; projectSlug: string;
name: string; name: string;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
@@ -26,7 +31,7 @@ export type TCreateAccessApprovalPolicy = {
export type TUpdateAccessApprovalPolicy = { export type TUpdateAccessApprovalPolicy = {
policyId: string; policyId: string;
approvals?: number; approvals?: number;
approvers?: string[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
secretPath?: string; secretPath?: string;
name?: string; name?: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
@@ -41,6 +46,10 @@ export type TGetAccessPolicyCountByEnvironmentDTO = {
projectSlug: string; projectSlug: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetAccessApprovalPolicyByIdDTO = {
policyId: string;
} & Omit<TProjectPermission, "projectId">;
export type TListAccessApprovalPoliciesDTO = { export type TListAccessApprovalPoliciesDTO = {
projectSlug: string; projectSlug: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;

View File

@@ -39,6 +39,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId` `${TableName.AccessApprovalPolicyApprover}.policyId`
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.join<TUsers>( .join<TUsers>(
db(TableName.Users).as("requestedByUser"), db(TableName.Users).as("requestedByUser"),
@@ -59,6 +65,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
) )
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) .select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"))
.select( .select(
db.ref("projectId").withSchema(TableName.Environment), db.ref("projectId").withSchema(TableName.Environment),
@@ -142,7 +149,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
label: "reviewers" as const, label: "reviewers" as const,
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined) mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
}, },
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId } { key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId },
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({ approverGroupUserId }) => approverGroupUserId
}
] ]
}); });
@@ -172,17 +184,28 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`requestedByUser.id` `requestedByUser.id`
) )
.join( .leftJoin(
TableName.AccessApprovalPolicyApprover, TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId` `${TableName.AccessApprovalPolicyApprover}.policyId`
) )
.join<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalPolicyApproverUser"), db(TableName.Users).as("accessApprovalPolicyApproverUser"),
`${TableName.AccessApprovalPolicyApprover}.approverUserId`, `${TableName.AccessApprovalPolicyApprover}.approverUserId`,
"accessApprovalPolicyApproverUser.id" "accessApprovalPolicyApproverUser.id"
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalPolicyGroupApproverUser"),
`${TableName.UserGroupMembership}.userId`,
"accessApprovalPolicyGroupApproverUser.id"
)
.leftJoin( .leftJoin(
TableName.AccessApprovalRequestReviewer, TableName.AccessApprovalRequestReviewer,
@@ -200,10 +223,15 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.AccessApprovalRequest)) .select(selectAllTableCols(TableName.AccessApprovalRequest))
.select( .select(
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover), tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
tx.ref("userId").withSchema(TableName.UserGroupMembership),
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"), tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("email").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
tx.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"), tx.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"),
tx.ref("username").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
tx.ref("firstName").withSchema("accessApprovalPolicyApproverUser").as("approverFirstName"), tx.ref("firstName").withSchema("accessApprovalPolicyApproverUser").as("approverFirstName"),
tx.ref("firstName").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupFirstName"),
tx.ref("lastName").withSchema("accessApprovalPolicyApproverUser").as("approverLastName"), tx.ref("lastName").withSchema("accessApprovalPolicyApproverUser").as("approverLastName"),
tx.ref("lastName").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupLastName"),
tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"), tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"), tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"), tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
@@ -282,6 +310,23 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
lastName, lastName,
username username
}) })
},
{
key: "userId",
label: "approvers" as const,
mapper: ({
userId,
approverGroupEmail: email,
approverGroupUsername: username,
approverGroupLastName: lastName,
approverFirstName: firstName
}) => ({
userId,
email,
firstName,
lastName,
username
})
} }
] ]
}); });

View File

@@ -1,6 +1,6 @@
import { PackRule, unpackRules } from "@casl/ability/extra"; import { PackRule, unpackRules } from "@casl/ability/extra";
import { UnauthorizedError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TVerifyPermission } from "./access-approval-request-types"; import { TVerifyPermission } from "./access-approval-request-types";
@@ -19,7 +19,7 @@ export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) =
); );
if (!permission || !permission.length) { if (!permission || !permission.length) {
throw new UnauthorizedError({ message: "No permission provided" }); throw new BadRequestError({ message: "No permission provided" });
} }
const requestedPermissions: string[] = []; const requestedPermissions: string[] = [];
@@ -39,10 +39,10 @@ export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) =
const permissionEnv = firstPermission.conditions?.environment; const permissionEnv = firstPermission.conditions?.environment;
if (!permissionEnv || typeof permissionEnv !== "string") { if (!permissionEnv || typeof permissionEnv !== "string") {
throw new UnauthorizedError({ message: "Permission environment is not a string" }); throw new BadRequestError({ message: "Permission environment is not a string" });
} }
if (!permissionSecretPath || typeof permissionSecretPath !== "string") { if (!permissionSecretPath || typeof permissionSecretPath !== "string") {
throw new UnauthorizedError({ message: "Permission path is not a string" }); throw new BadRequestError({ message: "Permission path is not a string" });
} }
return { return {

View File

@@ -3,7 +3,7 @@ import ms from "ms";
import { ProjectMembershipRole } from "@app/db/schemas"; import { ProjectMembershipRole } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -17,7 +17,8 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns"; import { isApproversValid } from "../access-approval-policy/access-approval-policy-fns";
import { TGroupDALFactory } from "../group/group-dal";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; 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 { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
@@ -57,6 +58,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
TAccessApprovalRequestReviewerDALFactory, TAccessApprovalRequestReviewerDALFactory,
"create" | "find" | "findOne" | "transaction" "create" | "find" | "findOne" | "transaction"
>; >;
groupDAL: Pick<TGroupDALFactory, "findAllGroupPossibleMembers">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick< userDAL: Pick<
@@ -70,6 +72,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>; export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
export const accessApprovalRequestServiceFactory = ({ export const accessApprovalRequestServiceFactory = ({
groupDAL,
projectDAL, projectDAL,
projectEnvDAL, projectEnvDAL,
permissionService, permissionService,
@@ -96,7 +99,7 @@ export const accessApprovalRequestServiceFactory = ({
}: TCreateAccessApprovalRequestDTO) => { }: TCreateAccessApprovalRequestDTO) => {
const cfg = getConfig(); const cfg = getConfig();
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new UnauthorizedError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
// Anyone can create an access approval request. // Anyone can create an access approval request.
const { membership } = await permissionService.getProjectPermission( const { membership } = await permissionService.getProjectPermission(
@@ -106,31 +109,56 @@ export const accessApprovalRequestServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); if (!membership) {
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
}
const requestedByUser = await userDAL.findById(actorId); const requestedByUser = await userDAL.findById(actorId);
if (!requestedByUser) throw new UnauthorizedError({ message: "User not found" }); if (!requestedByUser) throw new ForbiddenRequestError({ message: "User not found" });
await projectDAL.checkProjectUpgradeStatus(project.id); await projectDAL.checkProjectUpgradeStatus(project.id);
const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({ permissions: requestedPermissions }); const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({ permissions: requestedPermissions });
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
if (!environment) throw new UnauthorizedError({ message: "Environment not found" }); if (!environment) throw new NotFoundError({ message: "Environment not found" });
const policy = await accessApprovalPolicyDAL.findOne({ const policy = await accessApprovalPolicyDAL.findOne({
envId: environment.id, envId: environment.id,
secretPath secretPath
}); });
if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." }); if (!policy) throw new NotFoundError({ message: "No policy matching criteria was found." });
const approverIds: string[] = [];
const approverGroupIds: string[] = [];
const approvers = await accessApprovalPolicyApproverDAL.find({ const approvers = await accessApprovalPolicyApproverDAL.find({
policyId: policy.id policyId: policy.id
}); });
approvers.forEach((approver) => {
if (approver.approverUserId) {
approverIds.push(approver.approverUserId);
} else if (approver.approverGroupId) {
approverGroupIds.push(approver.approverGroupId);
}
});
const groupUsers = (
await Promise.all(
approverGroupIds.map((groupApproverId) =>
groupDAL.findAllGroupPossibleMembers({
orgId: actorOrgId,
groupId: groupApproverId
})
)
)
).flat();
approverIds.push(...groupUsers.filter((user) => user.isPartOfGroup).map((user) => user.id));
const approverUsers = await userDAL.find({ const approverUsers = await userDAL.find({
$in: { $in: {
id: approvers.map((approver) => approver.approverUserId) id: [...new Set(approverIds)]
} }
}); });
@@ -236,7 +264,7 @@ export const accessApprovalRequestServiceFactory = ({
actorAuthMethod actorAuthMethod
}: TListApprovalRequestsDTO) => { }: TListApprovalRequestsDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new UnauthorizedError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const { membership } = await permissionService.getProjectPermission( const { membership } = await permissionService.getProjectPermission(
actor, actor,
@@ -245,7 +273,9 @@ export const accessApprovalRequestServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); if (!membership) {
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
}
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id }); const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id)); let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
@@ -270,7 +300,7 @@ export const accessApprovalRequestServiceFactory = ({
actorOrgId actorOrgId
}: TReviewAccessRequestDTO) => { }: TReviewAccessRequestDTO) => {
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId); const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
if (!accessApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (!accessApprovalRequest) throw new NotFoundError({ message: "Secret approval request not found" });
const { policy } = accessApprovalRequest; const { policy } = accessApprovalRequest;
const { membership, hasRole } = await permissionService.getProjectPermission( const { membership, hasRole } = await permissionService.getProjectPermission(
@@ -281,19 +311,21 @@ export const accessApprovalRequestServiceFactory = ({
actorOrgId actorOrgId
); );
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); if (!membership) {
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
}
if ( if (
!hasRole(ProjectMembershipRole.Admin) && !hasRole(ProjectMembershipRole.Admin) &&
accessApprovalRequest.requestedByUserId !== actorId && // The request wasn't made by the current user accessApprovalRequest.requestedByUserId !== actorId && // The request wasn't made by the current user
!policy.approvers.find((approver) => approver.userId === actorId) // The request isn't performed by an assigned approver !policy.approvers.find((approver) => approver.userId === actorId) // The request isn't performed by an assigned approver
) { ) {
throw new UnauthorizedError({ message: "You are not authorized to approve this request" }); throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" });
} }
const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id); const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id);
await verifyApprovers({ const approversValid = await isApproversValid({
projectId: accessApprovalRequest.projectId, projectId: accessApprovalRequest.projectId,
orgId: actorOrgId, orgId: actorOrgId,
envSlug: accessApprovalRequest.environment, envSlug: accessApprovalRequest.environment,
@@ -303,6 +335,10 @@ export const accessApprovalRequestServiceFactory = ({
userIds: [reviewerProjectMembership.userId] userIds: [reviewerProjectMembership.userId]
}); });
if (!approversValid) {
throw new ForbiddenRequestError({ message: "You don't have access to approve this request" });
}
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id }); const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) { if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" }); throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
@@ -385,7 +421,7 @@ export const accessApprovalRequestServiceFactory = ({
const getCount = async ({ projectSlug, actor, actorAuthMethod, actorId, actorOrgId }: TGetAccessRequestCountDTO) => { const getCount = async ({ projectSlug, actor, actorAuthMethod, actorId, actorOrgId }: TGetAccessRequestCountDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new UnauthorizedError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const { membership } = await permissionService.getProjectPermission( const { membership } = await permissionService.getProjectPermission(
actor, actor,
@@ -394,7 +430,9 @@ export const accessApprovalRequestServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (!membership) throw new BadRequestError({ message: "User not found in project" }); if (!membership) {
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
}
const count = await accessApprovalRequestDAL.getCount({ projectId: project.id }); const count = await accessApprovalRequestDAL.getCount({ projectId: project.id });

View File

@@ -5,7 +5,7 @@ import { SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue"; import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue";
@@ -43,14 +43,15 @@ export const auditLogStreamServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod actorAuthMethod
}: TCreateAuditLogStreamDTO) => { }: TCreateAuditLogStreamDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
const appCfg = getConfig(); const appCfg = getConfig();
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan.auditLogStreams) if (!plan.auditLogStreams) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to create audit log streams due to plan restriction. Upgrade plan to create group." message: "Failed to create audit log streams due to plan restriction. Upgrade plan to create group."
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
@@ -120,7 +121,7 @@ export const auditLogStreamServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod actorAuthMethod
}: TUpdateAuditLogStreamDTO) => { }: TUpdateAuditLogStreamDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan.auditLogStreams) if (!plan.auditLogStreams)
@@ -129,7 +130,7 @@ export const auditLogStreamServiceFactory = ({
}); });
const logStream = await auditLogStreamDAL.findById(id); const logStream = await auditLogStreamDAL.findById(id);
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" }); if (!logStream) throw new NotFoundError({ message: "Audit log stream not found" });
const { orgId } = logStream; const { orgId } = logStream;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
@@ -178,10 +179,10 @@ export const auditLogStreamServiceFactory = ({
}; };
const deleteById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TDeleteAuditLogStreamDTO) => { const deleteById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TDeleteAuditLogStreamDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
const logStream = await auditLogStreamDAL.findById(id); const logStream = await auditLogStreamDAL.findById(id);
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" }); if (!logStream) throw new NotFoundError({ message: "Audit log stream not found" });
const { orgId } = logStream; const { orgId } = logStream;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
@@ -193,7 +194,7 @@ export const auditLogStreamServiceFactory = ({
const getById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetDetailsAuditLogStreamDTO) => { const getById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetDetailsAuditLogStreamDTO) => {
const logStream = await auditLogStreamDAL.findById(id); const logStream = await auditLogStreamDAL.findById(id);
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" }); if (!logStream) throw new NotFoundError({ message: "Audit log stream not found" });
const { orgId } = logStream; const { orgId } = logStream;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);

View File

@@ -2,10 +2,9 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
// import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -19,7 +18,6 @@ type TCertificateAuthorityCrlServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
// licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>; export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
@@ -66,7 +64,7 @@ export const certificateAuthorityCrlServiceFactory = ({
*/ */
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => { const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!ca) throw new NotFoundError({ message: "CA not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -81,13 +79,6 @@ export const certificateAuthorityCrlServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
// const plan = await licenseService.getPlan(actorOrgId);
// if (!plan.caCrl)
// throw new BadRequestError({
// message:
// "Failed to get CA certificate revocation lists (CRLs) due to plan restriction. Upgrade plan to get the CA CRL."
// });
const caCrls = await certificateAuthorityCrlDAL.find({ caId: ca.id }, { sort: [["createdAt", "desc"]] }); const caCrls = await certificateAuthorityCrlDAL.find({ caId: ca.id }, { sort: [["createdAt", "desc"]] });
const keyId = await getProjectKmsCertificateKeyId({ const keyId = await getProjectKmsCertificateKeyId({

View File

@@ -7,7 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -61,7 +61,7 @@ export const dynamicSecretLeaseServiceFactory = ({
}: TCreateDynamicSecretLeaseDTO) => { }: TCreateDynamicSecretLeaseDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const projectId = project.id; const projectId = project.id;
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -84,10 +84,10 @@ export const dynamicSecretLeaseServiceFactory = ({
} }
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" }); if (!dynamicSecretCfg) throw new NotFoundError({ message: "Dynamic secret not found" });
const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id); const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id);
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT) if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
@@ -134,7 +134,7 @@ export const dynamicSecretLeaseServiceFactory = ({
leaseId leaseId
}: TRenewDynamicSecretLeaseDTO) => { }: TRenewDynamicSecretLeaseDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const projectId = project.id; const projectId = project.id;
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -157,10 +157,10 @@ export const dynamicSecretLeaseServiceFactory = ({
} }
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" }); if (!dynamicSecretLease) throw new NotFoundError({ message: "Dynamic secret lease not found" });
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
@@ -208,7 +208,7 @@ export const dynamicSecretLeaseServiceFactory = ({
isForced isForced
}: TDeleteDynamicSecretLeaseDTO) => { }: TDeleteDynamicSecretLeaseDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const projectId = project.id; const projectId = project.id;
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -224,10 +224,10 @@ export const dynamicSecretLeaseServiceFactory = ({
); );
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" }); if (!dynamicSecretLease) throw new NotFoundError({ message: "Dynamic secret lease not found" });
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
@@ -273,7 +273,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorAuthMethod actorAuthMethod
}: TListDynamicSecretLeasesDTO) => { }: TListDynamicSecretLeasesDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const projectId = project.id; const projectId = project.id;
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -289,10 +289,10 @@ export const dynamicSecretLeaseServiceFactory = ({
); );
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" }); if (!dynamicSecretCfg) throw new NotFoundError({ message: "Dynamic secret not found" });
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id }); const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
return dynamicSecretLeases; return dynamicSecretLeases;
@@ -309,7 +309,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorAuthMethod actorAuthMethod
}: TDetailsDynamicSecretLeaseDTO) => { }: TDetailsDynamicSecretLeaseDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const projectId = project.id; const projectId = project.id;
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -325,10 +325,10 @@ export const dynamicSecretLeaseServiceFactory = ({
); );
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" }); if (!dynamicSecretLease) throw new NotFoundError({ message: "Dynamic secret lease not found" });
return dynamicSecretLease; return dynamicSecretLease;
}; };

View File

@@ -5,7 +5,7 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -66,7 +66,7 @@ export const dynamicSecretServiceFactory = ({
actorAuthMethod actorAuthMethod
}: TCreateDynamicSecretDTO) => { }: TCreateDynamicSecretDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const projectId = project.id; const projectId = project.id;
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -89,7 +89,7 @@ export const dynamicSecretServiceFactory = ({
} }
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (existingDynamicSecret) if (existingDynamicSecret)
@@ -134,7 +134,7 @@ export const dynamicSecretServiceFactory = ({
actorAuthMethod actorAuthMethod
}: TUpdateDynamicSecretDTO) => { }: TUpdateDynamicSecretDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const projectId = project.id; const projectId = project.id;
@@ -158,10 +158,10 @@ export const dynamicSecretServiceFactory = ({
} }
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" }); if (!dynamicSecretCfg) throw new NotFoundError({ message: "Dynamic secret not found" });
if (newName) { if (newName) {
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id }); const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
@@ -213,7 +213,7 @@ export const dynamicSecretServiceFactory = ({
isForced isForced
}: TDeleteDynamicSecretDTO) => { }: TDeleteDynamicSecretDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const projectId = project.id; const projectId = project.id;
@@ -230,7 +230,7 @@ export const dynamicSecretServiceFactory = ({
); );
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" }); if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
@@ -271,7 +271,7 @@ export const dynamicSecretServiceFactory = ({
actor actor
}: TDetailsDynamicSecretDTO) => { }: TDetailsDynamicSecretDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const projectId = project.id; const projectId = project.id;
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -287,10 +287,10 @@ export const dynamicSecretServiceFactory = ({
); );
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" }); if (!dynamicSecretCfg) throw new NotFoundError({ message: "Dynamic secret not found" });
const decryptedStoredInput = JSON.parse( const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({ infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
@@ -313,26 +313,29 @@ export const dynamicSecretServiceFactory = ({
projectId, projectId,
path, path,
environmentSlugs, environmentSlugs,
search search,
isInternal
}: TListDynamicSecretsMultiEnvDTO) => { }: TListDynamicSecretsMultiEnvDTO) => {
const { permission } = await permissionService.getProjectPermission( if (!isInternal) {
actor, const { permission } = await permissionService.getProjectPermission(
actorId, actor,
projectId, actorId,
actorAuthMethod, projectId,
actorOrgId actorAuthMethod,
); actorOrgId
);
// verify user has access to each env in request // verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) => environmentSlugs.forEach((environmentSlug) =>
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
) )
); );
}
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path); const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
if (!folders.length) throw new BadRequestError({ message: "Folders not found" }); if (!folders.length) throw new NotFoundError({ message: "Folders not found" });
const dynamicSecretCfg = await dynamicSecretDAL.find( const dynamicSecretCfg = await dynamicSecretDAL.find(
{ $in: { folderId: folders.map((folder) => folder.id) }, $search: search ? { name: `%${search}%` } : undefined }, { $in: { folderId: folders.map((folder) => folder.id) }, $search: search ? { name: `%${search}%` } : undefined },
@@ -366,7 +369,7 @@ export const dynamicSecretServiceFactory = ({
); );
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.find( const dynamicSecretCfg = await dynamicSecretDAL.find(
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined }, { folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
@@ -395,7 +398,7 @@ export const dynamicSecretServiceFactory = ({
if (!projectId) { if (!projectId) {
if (!projectSlug) throw new BadRequestError({ message: "Project ID or slug required" }); if (!projectSlug) throw new BadRequestError({ message: "Project ID or slug required" });
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
projectId = project.id; projectId = project.id;
} }
@@ -412,7 +415,7 @@ export const dynamicSecretServiceFactory = ({
); );
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.find( const dynamicSecretCfg = await dynamicSecretDAL.find(
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined }, { folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
@@ -434,26 +437,29 @@ export const dynamicSecretServiceFactory = ({
path, path,
environmentSlugs, environmentSlugs,
projectId, projectId,
isInternal,
...params ...params
}: TListDynamicSecretsMultiEnvDTO) => { }: TListDynamicSecretsMultiEnvDTO) => {
const { permission } = await permissionService.getProjectPermission( if (!isInternal) {
actor, const { permission } = await permissionService.getProjectPermission(
actorId, actor,
projectId, actorId,
actorAuthMethod, projectId,
actorOrgId actorAuthMethod,
); actorOrgId
);
// verify user has access to each env in request // verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) => environmentSlugs.forEach((environmentSlug) =>
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
) )
); );
}
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path); const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
if (!folders.length) throw new BadRequestError({ message: "Folders not found" }); if (!folders.length) throw new NotFoundError({ message: "Folders not found" });
const dynamicSecretCfg = await dynamicSecretDAL.listDynamicSecretsByFolderIds({ const dynamicSecretCfg = await dynamicSecretDAL.listDynamicSecretsByFolderIds({
folderIds: folders.map((folder) => folder.id), folderIds: folders.map((folder) => folder.id),

View File

@@ -63,7 +63,7 @@ export type TListDynamicSecretsDTO = {
export type TListDynamicSecretsMultiEnvDTO = Omit< export type TListDynamicSecretsMultiEnvDTO = Omit<
TListDynamicSecretsDTO, TListDynamicSecretsDTO,
"projectId" | "environmentSlug" | "projectSlug" "projectId" | "environmentSlug" | "projectSlug"
> & { projectId: string; environmentSlugs: string[] }; > & { projectId: string; environmentSlugs: string[]; isInternal?: boolean };
export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & { export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & {
projectId: string; projectId: string;

View File

@@ -3,6 +3,7 @@ import { AwsIamProvider } from "./aws-iam";
import { AzureEntraIDProvider } from "./azure-entra-id"; import { AzureEntraIDProvider } from "./azure-entra-id";
import { CassandraProvider } from "./cassandra"; import { CassandraProvider } from "./cassandra";
import { ElasticSearchProvider } from "./elastic-search"; import { ElasticSearchProvider } from "./elastic-search";
import { LdapProvider } from "./ldap";
import { DynamicSecretProviders } from "./models"; import { DynamicSecretProviders } from "./models";
import { MongoAtlasProvider } from "./mongo-atlas"; import { MongoAtlasProvider } from "./mongo-atlas";
import { MongoDBProvider } from "./mongo-db"; import { MongoDBProvider } from "./mongo-db";
@@ -20,5 +21,6 @@ export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.MongoDB]: MongoDBProvider(), [DynamicSecretProviders.MongoDB]: MongoDBProvider(),
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(), [DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(), [DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider() [DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
[DynamicSecretProviders.Ldap]: LdapProvider()
}); });

View File

@@ -0,0 +1,234 @@
import ldapjs from "ldapjs";
import ldif from "ldif";
import mustache from "mustache";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { LdapSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 64)();
};
const encodePassword = (password?: string) => {
const quotedPassword = `"${password}"`;
const utf16lePassword = Buffer.from(quotedPassword, "utf16le");
const base64Password = utf16lePassword.toString("base64");
return base64Password;
};
const generateUsername = () => {
return alphaNumericNanoId(20);
};
const generateLDIF = ({
username,
password,
ldifTemplate
}: {
username: string;
password?: string;
ldifTemplate: string;
}): string => {
const data = {
Username: username,
Password: password,
EncodedPassword: encodePassword(password)
};
const renderedLdif = mustache.render(ldifTemplate, data);
return renderedLdif;
};
export const LdapProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await LdapSchema.parseAsync(inputs);
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof LdapSchema>): Promise<ldapjs.Client> => {
return new Promise((resolve, reject) => {
const client = ldapjs.createClient({
url: providerInputs.url,
tlsOptions: {
ca: providerInputs.ca ? providerInputs.ca : null,
rejectUnauthorized: !!providerInputs.ca
},
reconnect: true,
bindDN: providerInputs.binddn,
bindCredentials: providerInputs.bindpass
});
client.on("error", (err: Error) => {
client.unbind();
reject(new BadRequestError({ message: err.message }));
});
client.bind(providerInputs.binddn, providerInputs.bindpass, (err) => {
if (err) {
client.unbind();
reject(new BadRequestError({ message: err.message }));
} else {
resolve(client);
}
});
});
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
return client.connected;
};
const executeLdif = async (client: ldapjs.Client, ldif_file: string) => {
type TEntry = {
dn: string;
type: string;
changes: {
operation?: string;
attribute: {
attribute: string;
};
value: {
value: string;
};
values: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Untyped, can be any for ldapjs.Change.modification.values
value: any;
}[];
}[];
};
let parsedEntries: TEntry[];
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
parsedEntries = ldif.parse(ldif_file).entries as TEntry[];
} catch (err) {
throw new BadRequestError({
message: "Invalid LDIF format, refer to the documentation at Dynamic secrets > LDAP > LDIF Entries."
});
}
const dnArray: string[] = [];
for await (const entry of parsedEntries) {
const { dn } = entry;
let responseDn: string;
if (entry.type === "add") {
const attributes: Record<string, string | string[]> = {};
entry.changes.forEach((change) => {
const attrName = change.attribute.attribute;
const attrValue = change.value.value;
attributes[attrName] = Array.isArray(attrValue) ? attrValue : [attrValue];
});
responseDn = await new Promise((resolve, reject) => {
client.add(dn, attributes, (err) => {
if (err) {
reject(new BadRequestError({ message: err.message }));
} else {
resolve(dn);
}
});
});
} else if (entry.type === "modify") {
const changes: ldapjs.Change[] = [];
entry.changes.forEach((change) => {
changes.push(
new ldapjs.Change({
operation: change.operation || "replace",
modification: {
type: change.attribute.attribute,
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
values: change.values.map((value) => value.value)
}
})
);
});
responseDn = await new Promise((resolve, reject) => {
client.modify(dn, changes, (err) => {
if (err) {
reject(new BadRequestError({ message: err.message }));
} else {
resolve(dn);
}
});
});
} else if (entry.type === "delete") {
responseDn = await new Promise((resolve, reject) => {
client.del(dn, (err) => {
if (err) {
reject(new BadRequestError({ message: err.message }));
} else {
resolve(dn);
}
});
});
} else {
client.unbind();
throw new BadRequestError({ message: `Unsupported operation type ${entry.type}` });
}
dnArray.push(responseDn);
}
client.unbind();
return dnArray;
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
try {
const dnArray = await executeLdif(client, generatedLdif);
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
} catch (err) {
if (providerInputs.rollbackLdif) {
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
await executeLdif(client, rollbackLdif);
}
throw new BadRequestError({ message: (err as Error).message });
}
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const revocationLdif = generateLDIF({ username: entityId, ldifTemplate: providerInputs.revocationLdif });
await executeLdif(connection, revocationLdif);
return { entityId };
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
return { entityId };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@@ -174,6 +174,17 @@ export const AzureEntraIDSchema = z.object({
clientSecret: z.string().trim().min(1) clientSecret: z.string().trim().min(1)
}); });
export const LdapSchema = z.object({
url: z.string().trim().min(1),
binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1),
ca: z.string().optional(),
creationLdif: z.string().min(1),
revocationLdif: z.string().min(1),
rollbackLdif: z.string().optional()
});
export enum DynamicSecretProviders { export enum DynamicSecretProviders {
SqlDatabase = "sql-database", SqlDatabase = "sql-database",
Cassandra = "cassandra", Cassandra = "cassandra",
@@ -184,7 +195,8 @@ export enum DynamicSecretProviders {
ElasticSearch = "elastic-search", ElasticSearch = "elastic-search",
MongoDB = "mongo-db", MongoDB = "mongo-db",
RabbitMq = "rabbit-mq", RabbitMq = "rabbit-mq",
AzureEntraID = "azure-entra-id" AzureEntraID = "azure-entra-id",
Ldap = "ldap"
} }
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@@ -197,7 +209,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }), z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }), z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }) z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema })
]); ]);
export type TDynamicProviderFns = { export type TDynamicProviderFns = {

View File

@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@@ -145,7 +145,7 @@ export const externalKmsServiceFactory = ({
const kmsSlug = slug ? slugify(slug) : undefined; const kmsSlug = slug ? slugify(slug) : undefined;
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); if (!externalKmsDoc) throw new NotFoundError({ message: "External kms not found" });
let sanitizedProviderInput = ""; let sanitizedProviderInput = "";
const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } = const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
@@ -220,7 +220,7 @@ export const externalKmsServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); if (!externalKmsDoc) throw new NotFoundError({ message: "External kms not found" });
const externalKms = await externalKmsDAL.transaction(async (tx) => { const externalKms = await externalKmsDAL.transaction(async (tx) => {
const kms = await kmsDAL.deleteById(kmsDoc.id, tx); const kms = await kmsDAL.deleteById(kmsDoc.id, tx);
@@ -258,7 +258,7 @@ export const externalKmsServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); if (!externalKmsDoc) throw new NotFoundError({ message: "External kms not found" });
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
@@ -298,7 +298,7 @@ export const externalKmsServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); if (!externalKmsDoc) throw new NotFoundError({ message: "External kms not found" });
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,

View File

@@ -60,7 +60,7 @@ export const groupDALFactory = (db: TDbClient) => {
}; };
// special query // special query
const findAllGroupMembers = async ({ const findAllGroupPossibleMembers = async ({
orgId, orgId,
groupId, groupId,
offset = 0, offset = 0,
@@ -125,7 +125,7 @@ export const groupDALFactory = (db: TDbClient) => {
return { return {
findGroups, findGroups,
findByOrgId, findByOrgId,
findAllGroupMembers, findAllGroupPossibleMembers,
...groupOrm ...groupOrm
}; };
}; };

View File

@@ -2,7 +2,7 @@ import { Knex } from "knex";
import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ScimRequestError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors";
import { import {
TAddUsersToGroup, TAddUsersToGroup,
@@ -73,24 +73,24 @@ const addAcceptedUsersToGroup = async ({
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx); const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
if (!ghostUser) { if (!ghostUser) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find sudo user" message: "Failed to find project owner"
}); });
} }
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx); const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx);
if (!ghostUserLatestKey) { if (!ghostUserLatestKey) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find sudo user latest key" message: "Failed to find project owner's latest key"
}); });
} }
const bot = await projectBotDAL.findOne({ projectId }, tx); const bot = await projectBotDAL.findOne({ projectId }, tx);
if (!bot) { if (!bot) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find bot" message: "Failed to find project bot"
}); });
} }
@@ -200,7 +200,7 @@ export const addUsersToGroupByUserIds = async ({
userIds.forEach((userId) => { userIds.forEach((userId) => {
if (!existingUserOrgMembershipsUserIdsSet.has(userId)) if (!existingUserOrgMembershipsUserIdsSet.has(userId))
throw new BadRequestError({ throw new ForbiddenRequestError({
message: `User with id ${userId} is not part of the organization` message: `User with id ${userId} is not part of the organization`
}); });
}); });
@@ -303,7 +303,7 @@ export const removeUsersFromGroupByUserIds = async ({
userIds.forEach((userId) => { userIds.forEach((userId) => {
if (!existingUserGroupMembershipsUserIdsSet.has(userId)) if (!existingUserGroupMembershipsUserIdsSet.has(userId))
throw new BadRequestError({ throw new ForbiddenRequestError({
message: `User(s) are not part of the group ${group.slug}` message: `User(s) are not part of the group ${group.slug}`
}); });
}); });
@@ -415,7 +415,7 @@ export const convertPendingGroupAdditionsToGroupMemberships = async ({
const usersUserIdsSet = new Set(users.map((u) => u.id)); const usersUserIdsSet = new Set(users.map((u) => u.id));
userIds.forEach((userId) => { userIds.forEach((userId) => {
if (!usersUserIdsSet.has(userId)) { if (!usersUserIdsSet.has(userId)) {
throw new BadRequestError({ throw new NotFoundError({
message: `Failed to find user with id ${userId}` message: `Failed to find user with id ${userId}`
}); });
} }

View File

@@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
@@ -30,7 +30,10 @@ import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
type TGroupServiceFactoryDep = { type TGroupServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">; userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupMembers" | "findById">; groupDAL: Pick<
TGroupDALFactory,
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById"
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">; orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
userGroupMembershipDAL: Pick< userGroupMembershipDAL: Pick<
@@ -59,7 +62,7 @@ export const groupServiceFactory = ({
licenseService licenseService
}: TGroupServiceFactoryDep) => { }: TGroupServiceFactoryDep) => {
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => { const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
@@ -82,7 +85,8 @@ export const groupServiceFactory = ({
); );
const isCustomRole = Boolean(customRole); const isCustomRole = Boolean(customRole);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged group" }); if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
const group = await groupDAL.create({ const group = await groupDAL.create({
name, name,
@@ -105,7 +109,7 @@ export const groupServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TUpdateGroupDTO) => { }: TUpdateGroupDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
@@ -124,7 +128,7 @@ export const groupServiceFactory = ({
const group = await groupDAL.findOne({ orgId: actorOrgId, id }); const group = await groupDAL.findOne({ orgId: actorOrgId, id });
if (!group) { if (!group) {
throw new BadRequestError({ message: `Failed to find group with ID ${id}` }); throw new NotFoundError({ message: `Failed to find group with ID ${id}` });
} }
let customRole: TOrgRoles | undefined; let customRole: TOrgRoles | undefined;
@@ -137,7 +141,7 @@ export const groupServiceFactory = ({
const isCustomRole = Boolean(customOrgRole); const isCustomRole = Boolean(customOrgRole);
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission); const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredNewRolePermission) if (!hasRequiredNewRolePermission)
throw new BadRequestError({ message: "Failed to create a more privileged group" }); throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
if (isCustomRole) customRole = customOrgRole; if (isCustomRole) customRole = customOrgRole;
} }
@@ -161,7 +165,7 @@ export const groupServiceFactory = ({
}; };
const deleteGroup = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => { const deleteGroup = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
@@ -188,9 +192,7 @@ export const groupServiceFactory = ({
}; };
const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => { const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => {
if (!actorOrgId) { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
throw new BadRequestError({ message: "Failed to read group without organization" });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
@@ -221,7 +223,7 @@ export const groupServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TListGroupUsersDTO) => { }: TListGroupUsersDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
@@ -238,11 +240,11 @@ export const groupServiceFactory = ({
}); });
if (!group) if (!group)
throw new BadRequestError({ throw new NotFoundError({
message: `Failed to find group with ID ${id}` message: `Failed to find group with ID ${id}`
}); });
const users = await groupDAL.findAllGroupMembers({ const users = await groupDAL.findAllGroupPossibleMembers({
orgId: group.orgId, orgId: group.orgId,
groupId: group.id, groupId: group.id,
offset, offset,
@@ -256,7 +258,7 @@ export const groupServiceFactory = ({
}; };
const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => { const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
@@ -274,7 +276,7 @@ export const groupServiceFactory = ({
}); });
if (!group) if (!group)
throw new BadRequestError({ throw new NotFoundError({
message: `Failed to find group with ID ${id}` message: `Failed to find group with ID ${id}`
}); });
@@ -286,7 +288,7 @@ export const groupServiceFactory = ({
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" }); throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
const user = await userDAL.findOne({ username }); const user = await userDAL.findOne({ username });
if (!user) throw new BadRequestError({ message: `Failed to find user with username ${username}` }); if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
const users = await addUsersToGroupByUserIds({ const users = await addUsersToGroupByUserIds({
group, group,
@@ -311,7 +313,7 @@ export const groupServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TRemoveUserFromGroupDTO) => { }: TRemoveUserFromGroupDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
@@ -329,7 +331,7 @@ export const groupServiceFactory = ({
}); });
if (!group) if (!group)
throw new BadRequestError({ throw new NotFoundError({
message: `Failed to find group with ID ${id}` message: `Failed to find group with ID ${id}`
}); });
@@ -341,7 +343,7 @@ export const groupServiceFactory = ({
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" }); throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
const user = await userDAL.findOne({ username }); const user = await userDAL.findOne({ username });
if (!user) throw new BadRequestError({ message: `Failed to find user with username ${username}` }); if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
const users = await removeUsersFromGroupByUserIds({ const users = await removeUsersFromGroupByUserIds({
group, group,

View File

@@ -4,7 +4,7 @@ import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -34,18 +34,12 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
// TODO(akhilmhdh): move this to more centralized // TODO(akhilmhdh): move this to more centralized
export const UnpackedPermissionSchema = z.object({ export const UnpackedPermissionSchema = z.object({
subject: z.union([z.string().min(1), z.string().array()]).optional(), subject: z
action: z.union([z.string().min(1), z.string().array()]), .union([z.string().min(1), z.string().array()])
conditions: z .transform((el) => (typeof el !== "string" ? el[0] : el))
.object({ .optional(),
environment: z.string().optional(), action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
secretPath: z conditions: z.unknown().optional()
.object({
$glob: z.string().min(1)
})
.optional()
})
.optional()
}); });
const unpackPermissions = (permissions: unknown) => const unpackPermissions = (permissions: unknown) =>
@@ -71,12 +65,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
...dto ...dto
}: TCreateIdentityPrivilegeDTO) => { }: TCreateIdentityPrivilegeDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const projectId = project.id; const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership) if (!identityProjectMembership)
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -143,12 +137,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod actorAuthMethod
}: TUpdateIdentityPrivilegeDTO) => { }: TUpdateIdentityPrivilegeDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const projectId = project.id; const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership) if (!identityProjectMembership)
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -173,7 +167,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
slug, slug,
projectMembershipId: identityProjectMembership.id projectMembershipId: identityProjectMembership.id
}); });
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" });
if (data?.slug) { if (data?.slug) {
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
slug: data.slug, slug: data.slug,
@@ -224,12 +218,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod actorAuthMethod
}: TDeleteIdentityPrivilegeDTO) => { }: TDeleteIdentityPrivilegeDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const projectId = project.id; const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership) if (!identityProjectMembership)
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -254,7 +248,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
slug, slug,
projectMembershipId: identityProjectMembership.id projectMembershipId: identityProjectMembership.id
}); });
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" });
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
return { return {
@@ -274,12 +268,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod actorAuthMethod
}: TGetIdentityPrivilegeDetailsDTO) => { }: TGetIdentityPrivilegeDetailsDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const projectId = project.id; const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership) if (!identityProjectMembership)
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -293,7 +287,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
slug, slug,
projectMembershipId: identityProjectMembership.id projectMembershipId: identityProjectMembership.id
}); });
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" });
return { return {
...identityPrivilege, ...identityPrivilege,
@@ -310,12 +304,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
projectSlug projectSlug
}: TListIdentityPrivilegesDTO) => { }: TListIdentityPrivilegesDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new NotFoundError({ message: "Project not found" });
const projectId = project.id; const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership) if (!identityProjectMembership)
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,

View File

@@ -21,7 +21,7 @@ import {
infisicalSymmetricDecrypt, infisicalSymmetricDecrypt,
infisicalSymmetricEncypt infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption"; } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TokenType } from "@app/services/auth-token/auth-token-types";
@@ -253,7 +253,7 @@ export const ldapConfigServiceFactory = ({
}; };
const orgBot = await orgBotDAL.findOne({ orgId }); const orgBot = await orgBotDAL.findOne({ orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); if (!orgBot) throw new NotFoundError({ message: "Organization bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({ const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey, ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV, iv: orgBot.symmetricKeyIV,
@@ -289,10 +289,10 @@ export const ldapConfigServiceFactory = ({
const getLdapCfg = async (filter: { orgId: string; isActive?: boolean; id?: string }) => { const getLdapCfg = async (filter: { orgId: string; isActive?: boolean; id?: string }) => {
const ldapConfig = await ldapConfigDAL.findOne(filter); const ldapConfig = await ldapConfigDAL.findOne(filter);
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" }); if (!ldapConfig) throw new NotFoundError({ message: "Failed to find organization LDAP data" });
const orgBot = await orgBotDAL.findOne({ orgId: ldapConfig.orgId }); const orgBot = await orgBotDAL.findOne({ orgId: ldapConfig.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); if (!orgBot) throw new NotFoundError({ message: "Organization bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({ const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey, ciphertext: orgBot.encryptedSymmetricKey,
@@ -375,7 +375,7 @@ export const ldapConfigServiceFactory = ({
const bootLdap = async (organizationSlug: string) => { const bootLdap = async (organizationSlug: string) => {
const organization = await orgDAL.findOne({ slug: organizationSlug }); const organization = await orgDAL.findOne({ slug: organizationSlug });
if (!organization) throw new BadRequestError({ message: "Org not found" }); if (!organization) throw new NotFoundError({ message: "Organization not found" });
const ldapConfig = await getLdapCfg({ const ldapConfig = await getLdapCfg({
orgId: organization.id, orgId: organization.id,
@@ -420,7 +420,7 @@ export const ldapConfigServiceFactory = ({
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.LDAP)) { if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.LDAP)) {
throw new BadRequestError({ throw new ForbiddenRequestError({
message: "Login with LDAP is disabled by administrator." message: "Login with LDAP is disabled by administrator."
}); });
} }
@@ -432,7 +432,7 @@ export const ldapConfigServiceFactory = ({
}); });
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) throw new BadRequestError({ message: "Org not found" }); if (!organization) throw new NotFoundError({ message: "Organization not found" });
if (userAlias) { if (userAlias) {
await userDAL.transaction(async (tx) => { await userDAL.transaction(async (tx) => {
@@ -700,7 +700,7 @@ export const ldapConfigServiceFactory = ({
orgId orgId
}); });
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" }); if (!ldapConfig) throw new NotFoundError({ message: "Failed to find organization LDAP data" });
const groupMaps = await ldapGroupMapDAL.findLdapGroupMapsByLdapConfigId(ldapConfigId); const groupMaps = await ldapGroupMapDAL.findLdapGroupMapsByLdapConfigId(ldapConfigId);
@@ -741,13 +741,13 @@ export const ldapConfigServiceFactory = ({
const groups = await searchGroups(ldapConfig, groupSearchFilter, ldapConfig.groupSearchBase); const groups = await searchGroups(ldapConfig, groupSearchFilter, ldapConfig.groupSearchBase);
if (!groups.some((g) => g.cn === ldapGroupCN)) { if (!groups.some((g) => g.cn === ldapGroupCN)) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find LDAP Group CN" message: "Failed to find LDAP Group CN"
}); });
} }
const group = await groupDAL.findOne({ slug: groupSlug, orgId }); const group = await groupDAL.findOne({ slug: groupSlug, orgId });
if (!group) throw new BadRequestError({ message: "Failed to find group" }); if (!group) throw new NotFoundError({ message: "Failed to find group" });
const groupMap = await ldapGroupMapDAL.create({ const groupMap = await ldapGroupMapDAL.create({
ldapConfigId, ldapConfigId,
@@ -781,7 +781,7 @@ export const ldapConfigServiceFactory = ({
orgId orgId
}); });
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" }); if (!ldapConfig) throw new NotFoundError({ message: "Failed to find organization LDAP data" });
const [deletedGroupMap] = await ldapGroupMapDAL.delete({ const [deletedGroupMap] = await ldapGroupMapDAL.delete({
ldapConfigId: ldapConfig.id, ldapConfigId: ldapConfig.id,

View File

@@ -10,7 +10,7 @@ import { Knex } from "knex";
import { TKeyStoreFactory } from "@app/keystore/keystore"; import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { verifyOfflineLicense } from "@app/lib/crypto"; import { verifyOfflineLicense } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
@@ -145,7 +145,7 @@ export const licenseServiceFactory = ({
if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet; if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet;
const org = await orgDAL.findOrgById(orgId); const org = await orgDAL.findOrgById(orgId);
if (!org) throw new BadRequestError({ message: "Org not found" }); if (!org) throw new NotFoundError({ message: "Organization not found" });
const { const {
data: { currentPlan } data: { currentPlan }
} = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>( } = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>(
@@ -204,7 +204,7 @@ export const licenseServiceFactory = ({
const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => { const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => {
if (instanceType === InstanceType.Cloud) { if (instanceType === InstanceType.Cloud) {
const org = await orgDAL.findOrgById(orgId); const org = await orgDAL.findOrgById(orgId);
if (!org) throw new BadRequestError({ message: "Org not found" }); if (!org) throw new NotFoundError({ message: "Organization not found" });
const quantity = await licenseDAL.countOfOrgMembers(orgId, tx); const quantity = await licenseDAL.countOfOrgMembers(orgId, tx);
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx); const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx);
@@ -266,8 +266,8 @@ export const licenseServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) { if (!organization) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization" message: "Organization not found"
}); });
} }
@@ -294,8 +294,8 @@ export const licenseServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) { if (!organization) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization" message: "Organization not found"
}); });
} }
@@ -340,8 +340,8 @@ export const licenseServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) { if (!organization) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization" message: "Organization not found"
}); });
} }
const { data } = await licenseServerCloudApi.request.get( const { data } = await licenseServerCloudApi.request.get(
@@ -357,8 +357,8 @@ export const licenseServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) { if (!organization) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization" message: "Organization not found"
}); });
} }
const { data } = await licenseServerCloudApi.request.get( const { data } = await licenseServerCloudApi.request.get(
@@ -373,8 +373,8 @@ export const licenseServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) { if (!organization) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization" message: "Organization not found"
}); });
} }
@@ -398,8 +398,8 @@ export const licenseServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) { if (!organization) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization" message: "Organization not found"
}); });
} }
const { data } = await licenseServerCloudApi.request.patch( const { data } = await licenseServerCloudApi.request.patch(
@@ -418,8 +418,8 @@ export const licenseServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) { if (!organization) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization" message: "Organization not found"
}); });
} }
@@ -445,8 +445,8 @@ export const licenseServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) { if (!organization) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization" message: "Organization not found"
}); });
} }
const { const {
@@ -474,8 +474,8 @@ export const licenseServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) { if (!organization) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization" message: "Organization not found"
}); });
} }
@@ -491,8 +491,8 @@ export const licenseServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) { if (!organization) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization" message: "Organization not found"
}); });
} }
const { const {
@@ -509,8 +509,8 @@ export const licenseServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) { if (!organization) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization" message: "Organization not found"
}); });
} }
@@ -530,8 +530,8 @@ export const licenseServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) { if (!organization) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization" message: "Organization not found"
}); });
} }
@@ -547,8 +547,8 @@ export const licenseServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) { if (!organization) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization" message: "Organization not found"
}); });
} }
@@ -564,8 +564,8 @@ export const licenseServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) { if (!organization) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization" message: "Organization not found"
}); });
} }

View File

@@ -1,5 +1,6 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>; export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
@@ -7,5 +8,22 @@ export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
export const oidcConfigDALFactory = (db: TDbClient) => { export const oidcConfigDALFactory = (db: TDbClient) => {
const oidcCfgOrm = ormify(db, TableName.OidcConfig); const oidcCfgOrm = ormify(db, TableName.OidcConfig);
return { ...oidcCfgOrm }; const findEnforceableOidcCfg = async (orgId: string) => {
try {
const oidcCfg = await db
.replicaNode()(TableName.OidcConfig)
.where({
orgId,
isActive: true
})
.whereNotNull("lastUsed")
.first();
return oidcCfg;
} catch (error) {
throw new DatabaseError({ error, name: "Find org by id" });
}
};
return { ...oidcCfgOrm, findEnforceableOidcCfg };
}; };

View File

@@ -17,7 +17,7 @@ import {
infisicalSymmetricDecrypt, infisicalSymmetricDecrypt,
infisicalSymmetricEncypt infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption"; } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TokenType } from "@app/services/auth-token/auth-token-types";
@@ -77,7 +77,7 @@ export const oidcConfigServiceFactory = ({
const getOidc = async (dto: TGetOidcCfgDTO) => { const getOidc = async (dto: TGetOidcCfgDTO) => {
const org = await orgDAL.findOne({ slug: dto.orgSlug }); const org = await orgDAL.findOne({ slug: dto.orgSlug });
if (!org) { if (!org) {
throw new BadRequestError({ throw new NotFoundError({
message: "Organization not found", message: "Organization not found",
name: "OrgNotFound" name: "OrgNotFound"
}); });
@@ -98,7 +98,7 @@ export const oidcConfigServiceFactory = ({
}); });
if (!oidcCfg) { if (!oidcCfg) {
throw new BadRequestError({ throw new NotFoundError({
message: "Failed to find organization OIDC configuration" message: "Failed to find organization OIDC configuration"
}); });
} }
@@ -106,7 +106,7 @@ export const oidcConfigServiceFactory = ({
// decrypt and return cfg // decrypt and return cfg
const orgBot = await orgBotDAL.findOne({ orgId: oidcCfg.orgId }); const orgBot = await orgBotDAL.findOne({ orgId: oidcCfg.orgId });
if (!orgBot) { if (!orgBot) {
throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); throw new NotFoundError({ message: "Organization bot not found", name: "OrgBotNotFound" });
} }
const key = infisicalSymmetricDecrypt({ const key = infisicalSymmetricDecrypt({
@@ -160,7 +160,7 @@ export const oidcConfigServiceFactory = ({
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) { if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) {
throw new BadRequestError({ throw new ForbiddenRequestError({
message: "Login with OIDC is disabled by administrator." message: "Login with OIDC is disabled by administrator."
}); });
} }
@@ -173,7 +173,7 @@ export const oidcConfigServiceFactory = ({
}); });
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) throw new BadRequestError({ message: "Org not found" }); if (!organization) throw new NotFoundError({ message: "Organization not found" });
let user: TUsers; let user: TUsers;
if (userAlias) { if (userAlias) {
@@ -314,6 +314,8 @@ export const oidcConfigServiceFactory = ({
} }
); );
await oidcConfigDAL.update({ orgId }, { lastUsed: new Date() });
if (user.email && !user.isEmailVerified) { if (user.email && !user.isEmailVerified) {
const token = await tokenService.createTokenForUser({ const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION, type: TokenType.TOKEN_EMAIL_VERIFICATION,
@@ -356,7 +358,7 @@ export const oidcConfigServiceFactory = ({
}); });
if (!org) { if (!org) {
throw new BadRequestError({ throw new NotFoundError({
message: "Organization not found" message: "Organization not found"
}); });
} }
@@ -378,7 +380,7 @@ export const oidcConfigServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
const orgBot = await orgBotDAL.findOne({ orgId: org.id }); const orgBot = await orgBotDAL.findOne({ orgId: org.id });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); if (!orgBot) throw new NotFoundError({ message: "Organization bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({ const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey, ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV, iv: orgBot.symmetricKeyIV,
@@ -395,7 +397,8 @@ export const oidcConfigServiceFactory = ({
tokenEndpoint, tokenEndpoint,
userinfoEndpoint, userinfoEndpoint,
jwksUri, jwksUri,
isActive isActive,
lastUsed: null
}; };
if (clientId !== undefined) { if (clientId !== undefined) {
@@ -418,6 +421,7 @@ export const oidcConfigServiceFactory = ({
} }
const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery); const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery);
await orgDAL.updateById(org.id, { authEnforced: false, scimEnabled: false });
return ssoConfig; return ssoConfig;
}; };
@@ -443,7 +447,7 @@ export const oidcConfigServiceFactory = ({
slug: orgSlug slug: orgSlug
}); });
if (!org) { if (!org) {
throw new BadRequestError({ throw new NotFoundError({
message: "Organization not found" message: "Organization not found"
}); });
} }
@@ -549,7 +553,7 @@ export const oidcConfigServiceFactory = ({
}); });
if (!org) { if (!org) {
throw new BadRequestError({ throw new NotFoundError({
message: "Organization not found." message: "Organization not found."
}); });
} }
@@ -560,7 +564,7 @@ export const oidcConfigServiceFactory = ({
}); });
if (!oidcCfg || !oidcCfg.isActive) { if (!oidcCfg || !oidcCfg.isActive) {
throw new BadRequestError({ throw new ForbiddenRequestError({
message: "Failed to authenticate with OIDC SSO" message: "Failed to authenticate with OIDC SSO"
}); });
} }
@@ -617,7 +621,7 @@ export const oidcConfigServiceFactory = ({
if (oidcCfg.allowedEmailDomains) { if (oidcCfg.allowedEmailDomains) {
const allowedDomains = oidcCfg.allowedEmailDomains.split(", "); const allowedDomains = oidcCfg.allowedEmailDomains.split(", ");
if (!allowedDomains.includes(claims.email.split("@")[1])) { if (!allowedDomains.includes(claims.email.split("@")[1])) {
throw new BadRequestError({ throw new ForbiddenRequestError({
message: "Email not allowed." message: "Email not allowed."
}); });
} }

View File

@@ -50,6 +50,7 @@ export const permissionDALFactory = (db: TDbClient) => {
.select( .select(
selectAllTableCols(TableName.OrgMembership), selectAllTableCols(TableName.OrgMembership),
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"), db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
db.ref("permissions").withSchema(TableName.OrgRoles),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("groupId").withSchema("userGroups"), db.ref("groupId").withSchema("userGroups"),
db.ref("groupOrgId").withSchema("userGroups"), db.ref("groupOrgId").withSchema("userGroups"),
@@ -167,8 +168,14 @@ export const permissionDALFactory = (db: TDbClient) => {
}) })
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId])) .join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder
.on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`)
.andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`);
})
.select( .select(
db.ref("id").withSchema(TableName.Users).as("userId"), db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("username").withSchema(TableName.Users).as("username"),
// groups specific // groups specific
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"), db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"), db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
@@ -256,6 +263,9 @@ export const permissionDALFactory = (db: TDbClient) => {
.withSchema(TableName.ProjectUserAdditionalPrivilege) .withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesTemporaryAccessEndTime"), .as("userAdditionalPrivilegesTemporaryAccessEndTime"),
// general // general
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project), db.ref("orgId").withSchema(TableName.Project),
db.ref("id").withSchema(TableName.Project).as("projectId") db.ref("id").withSchema(TableName.Project).as("projectId")
@@ -266,6 +276,7 @@ export const permissionDALFactory = (db: TDbClient) => {
key: "projectId", key: "projectId",
parentMapper: ({ parentMapper: ({
orgId, orgId,
username,
orgAuthEnforced, orgAuthEnforced,
membershipId, membershipId,
groupMembershipId, groupMembershipId,
@@ -278,6 +289,7 @@ export const permissionDALFactory = (db: TDbClient) => {
orgAuthEnforced, orgAuthEnforced,
userId, userId,
projectId, projectId,
username,
id: membershipId || groupMembershipId, id: membershipId || groupMembershipId,
createdAt: membershipCreatedAt || groupMembershipCreatedAt, createdAt: membershipCreatedAt || groupMembershipCreatedAt,
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
@@ -353,6 +365,15 @@ export const permissionDALFactory = (db: TDbClient) => {
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime, temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
isTemporary: userAdditionalPrivilegesIsTemporary isTemporary: userAdditionalPrivilegesIsTemporary
}) })
},
{
key: "metadataId",
label: "metadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
} }
] ]
}); });
@@ -398,6 +419,7 @@ export const permissionDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`, `${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
`${TableName.IdentityProjectMembership}.id` `${TableName.IdentityProjectMembership}.id`
) )
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`)
.leftJoin( .leftJoin(
TableName.ProjectRoles, TableName.ProjectRoles,
`${TableName.IdentityProjectMembershipRole}.customRoleId`, `${TableName.IdentityProjectMembershipRole}.customRoleId`,
@@ -414,11 +436,17 @@ export const permissionDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembership}.projectId`, `${TableName.IdentityProjectMembership}.projectId`,
`${TableName.Project}.id` `${TableName.Project}.id`
) )
.where("identityId", identityId) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder
.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`);
})
.where(`${TableName.IdentityProjectMembership}.identityId`, identityId)
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId) .where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole)) .select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
.select( .select(
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"), db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"), db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
@@ -442,15 +470,19 @@ export const permissionDALFactory = (db: TDbClient) => {
db db
.ref("temporaryAccessEndTime") .ref("temporaryAccessEndTime")
.withSchema(TableName.IdentityProjectAdditionalPrivilege) .withSchema(TableName.IdentityProjectAdditionalPrivilege)
.as("identityApTemporaryAccessEndTime") .as("identityApTemporaryAccessEndTime"),
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
); );
const permission = sqlNestRelationships({ const permission = sqlNestRelationships({
data: docs, data: docs,
key: "membershipId", key: "membershipId",
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId }) => ({ parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({
id: membershipId, id: membershipId,
identityId, identityId,
username: identityName,
projectId, projectId,
createdAt: membershipCreatedAt, createdAt: membershipCreatedAt,
updatedAt: membershipUpdatedAt, updatedAt: membershipUpdatedAt,
@@ -488,6 +520,15 @@ export const permissionDALFactory = (db: TDbClient) => {
temporaryAccessStartTime: identityApTemporaryAccessStartTime, temporaryAccessStartTime: identityApTemporaryAccessStartTime,
isTemporary: identityApIsTemporary isTemporary: identityApIsTemporary
}) })
},
{
key: "metadataId",
label: "metadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
} }
] ]
}); });

View File

@@ -1,5 +1,5 @@
import { TOrganizations } from "@app/db/schemas"; import { TOrganizations } from "@app/db/schemas";
import { UnauthorizedError } from "@app/lib/errors"; import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
@@ -14,14 +14,19 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
].includes(actorAuthMethod); ].includes(actorAuthMethod);
} }
function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) { function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"]) {
if (actorAuthMethod === undefined) { if (actorAuthMethod === undefined) {
throw new UnauthorizedError({ name: "No auth method defined" }); throw new UnauthorizedError({ name: "No auth method defined" });
} }
if (isSamlEnforced && actorAuthMethod !== null && !isAuthMethodSaml(actorAuthMethod)) { if (
throw new UnauthorizedError({ name: "Cannot access org-scoped resource" }); isOrgSsoEnforced &&
actorAuthMethod !== null &&
!isAuthMethodSaml(actorAuthMethod) &&
actorAuthMethod !== AuthMethod.OIDC
) {
throw new ForbiddenRequestError({ name: "Org auth enforced. Cannot access org-scoped resource" });
} }
} }
export { isAuthMethodSaml, validateOrgSAML }; export { isAuthMethodSaml, validateOrgSSO };

View File

@@ -0,0 +1,9 @@
export type TBuildProjectPermissionDTO = {
permissions?: unknown;
role: string;
}[];
export type TBuildOrgPermissionDTO = {
permissions?: unknown;
role: string;
}[];

View File

@@ -1,6 +1,7 @@
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability"; import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, unpackRules } from "@casl/ability/extra"; import { PackRule, unpackRules } from "@casl/ability/extra";
import { MongoQuery } from "@ucast/mongo2js"; import { MongoQuery } from "@ucast/mongo2js";
import handlebars from "handlebars";
import { import {
OrgMembershipRole, OrgMembershipRole,
@@ -10,7 +11,8 @@ import {
TProjectMemberships TProjectMemberships
} from "@app/db/schemas"; } from "@app/db/schemas";
import { conditionsMatcher } from "@app/lib/casl"; import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { objectify } from "@app/lib/fn";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -19,8 +21,8 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission"; import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
import { TPermissionDALFactory } from "./permission-dal"; import { TPermissionDALFactory } from "./permission-dal";
import { validateOrgSAML } from "./permission-fns"; import { validateOrgSSO } from "./permission-fns";
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-types"; import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types";
import { import {
buildServiceTokenProjectPermission, buildServiceTokenProjectPermission,
projectAdminPermissions, projectAdminPermissions,
@@ -62,7 +64,7 @@ export const permissionServiceFactory = ({
permissions as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[] permissions as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]
); );
default: default:
throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" }); throw new NotFoundError({ name: "OrgRoleInvalid", message: "Organization role not found" });
} }
}) })
.reduce((curr, prev) => prev.concat(curr), []); .reduce((curr, prev) => prev.concat(curr), []);
@@ -72,7 +74,7 @@ export const permissionServiceFactory = ({
}); });
}; };
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => { const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
const rules = projectUserRoles const rules = projectUserRoles
.map(({ role, permissions }) => { .map(({ role, permissions }) => {
switch (role) { switch (role) {
@@ -90,7 +92,7 @@ export const permissionServiceFactory = ({
); );
} }
default: default:
throw new BadRequestError({ throw new NotFoundError({
name: "ProjectRoleInvalid", name: "ProjectRoleInvalid",
message: "Project role not found" message: "Project role not found"
}); });
@@ -98,9 +100,7 @@ export const permissionServiceFactory = ({
}) })
.reduce((curr, prev) => prev.concat(curr), []); .reduce((curr, prev) => prev.concat(curr), []);
return createMongoAbility<ProjectPermissionSet>(rules, { return rules;
conditionsMatcher
});
}; };
/* /*
@@ -114,11 +114,11 @@ export const permissionServiceFactory = ({
) => { ) => {
// when token is scoped, ensure the passed org id is same as user org id // when token is scoped, ensure the passed org id is same as user org id
if (userOrgId && userOrgId !== orgId) if (userOrgId && userOrgId !== orgId)
throw new BadRequestError({ message: "Invalid user token. Scoped to different organization." }); throw new ForbiddenRequestError({ message: "Invalid user token. Scoped to different organization." });
const membership = await permissionDAL.getOrgPermission(userId, orgId); const membership = await permissionDAL.getOrgPermission(userId, orgId);
if (!membership) throw new UnauthorizedError({ name: "User not in org" }); if (!membership) throw new ForbiddenRequestError({ name: "You are not apart of this organization" });
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
throw new BadRequestError({ name: "Custom permission not found" }); throw new BadRequestError({ name: "Custom organization permission not found" });
} }
// If the org ID is API_KEY, the request is being made with an API Key. // If the org ID is API_KEY, the request is being made with an API Key.
@@ -127,10 +127,10 @@ export const permissionServiceFactory = ({
// Extra: This means that when users are using API keys to make requests, they can't use slug-based routes. // Extra: This means that when users are using API keys to make requests, they can't use slug-based routes.
// Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization. // Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization.
if (userOrgId !== "API_KEY" && membership.orgId !== userOrgId) { if (userOrgId !== "API_KEY" && membership.orgId !== userOrgId) {
throw new UnauthorizedError({ name: "You are not logged into this organization" }); throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
} }
validateOrgSAML(authMethod, membership.orgAuthEnforced); validateOrgSSO(authMethod, membership.orgAuthEnforced);
const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat( const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat(
membership?.groups?.map(({ role, customRolePermission }) => ({ membership?.groups?.map(({ role, customRolePermission }) => ({
@@ -143,9 +143,9 @@ export const permissionServiceFactory = ({
const getIdentityOrgPermission = async (identityId: string, orgId: string) => { const getIdentityOrgPermission = async (identityId: string, orgId: string) => {
const membership = await permissionDAL.getOrgIdentityPermission(identityId, orgId); const membership = await permissionDAL.getOrgIdentityPermission(identityId, orgId);
if (!membership) throw new UnauthorizedError({ name: "Identity not in org" }); if (!membership) throw new ForbiddenRequestError({ name: "Identity is not apart of this organization" });
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
throw new BadRequestError({ name: "Custom permission not found" }); throw new NotFoundError({ name: "Custom organization permission not found" });
} }
return { return {
permission: buildOrgPermission([{ role: membership.role, permissions: membership.permissions }]), permission: buildOrgPermission([{ role: membership.role, permissions: membership.permissions }]),
@@ -166,8 +166,8 @@ export const permissionServiceFactory = ({
case ActorType.IDENTITY: case ActorType.IDENTITY:
return getIdentityOrgPermission(id, orgId); return getIdentityOrgPermission(id, orgId);
default: default:
throw new UnauthorizedError({ throw new BadRequestError({
message: "Permission not defined", message: "Invalid actor provided",
name: "Get org permission" name: "Get org permission"
}); });
} }
@@ -179,7 +179,7 @@ export const permissionServiceFactory = ({
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole); const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
if (isCustomRole) { if (isCustomRole) {
const orgRole = await orgRoleDAL.findOne({ slug: role, orgId }); const orgRole = await orgRoleDAL.findOne({ slug: role, orgId });
if (!orgRole) throw new BadRequestError({ message: "Role not found" }); if (!orgRole) throw new NotFoundError({ message: "Specified role was not found" });
return { return {
permission: buildOrgPermission([{ role: OrgMembershipRole.Custom, permissions: orgRole.permissions }]), permission: buildOrgPermission([{ role: OrgMembershipRole.Custom, permissions: orgRole.permissions }]),
role: orgRole role: orgRole
@@ -196,12 +196,12 @@ export const permissionServiceFactory = ({
userOrgId?: string userOrgId?: string
): Promise<TProjectPermissionRT<ActorType.USER>> => { ): Promise<TProjectPermissionRT<ActorType.USER>> => {
const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId); const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId);
if (!userProjectPermission) throw new UnauthorizedError({ name: "User not in project" }); if (!userProjectPermission) throw new ForbiddenRequestError({ name: "User not a part of the specified project" });
if ( if (
userProjectPermission.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions) userProjectPermission.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions)
) { ) {
throw new BadRequestError({ name: "Custom permission not found" }); throw new NotFoundError({ name: "The permission was not found" });
} }
// If the org ID is API_KEY, the request is being made with an API Key. // If the org ID is API_KEY, the request is being made with an API Key.
@@ -210,10 +210,10 @@ export const permissionServiceFactory = ({
// Extra: This means that when users are using API keys to make requests, they can't use slug-based routes. // Extra: This means that when users are using API keys to make requests, they can't use slug-based routes.
// Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization. // Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization.
if (userOrgId !== "API_KEY" && userProjectPermission.orgId !== userOrgId) { if (userOrgId !== "API_KEY" && userProjectPermission.orgId !== userOrgId) {
throw new UnauthorizedError({ name: "You are not logged into this organization" }); throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
} }
validateOrgSAML(authMethod, userProjectPermission.orgAuthEnforced); validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced);
// join two permissions and pass to build the final permission set // join two permissions and pass to build the final permission set
const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
@@ -223,8 +223,32 @@ export const permissionServiceFactory = ({
permissions permissions
})) || []; })) || [];
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true });
const metadataKeyValuePair = objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
);
const interpolateRules = templatedRules(
{
identity: {
id: userProjectPermission.userId,
username: userProjectPermission.username,
metadata: metadataKeyValuePair
}
},
{ data: false }
);
const permission = createMongoAbility<ProjectPermissionSet>(
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
{
conditionsMatcher
}
);
return { return {
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)), permission,
membership: userProjectPermission, membership: userProjectPermission,
hasRole: (role: string) => hasRole: (role: string) =>
userProjectPermission.roles.findIndex( userProjectPermission.roles.findIndex(
@@ -239,18 +263,19 @@ export const permissionServiceFactory = ({
identityOrgId: string | undefined identityOrgId: string | undefined
): Promise<TProjectPermissionRT<ActorType.IDENTITY>> => { ): Promise<TProjectPermissionRT<ActorType.IDENTITY>> => {
const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId); const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId);
if (!identityProjectPermission) throw new UnauthorizedError({ name: "Identity not in project" }); if (!identityProjectPermission)
throw new ForbiddenRequestError({ name: "Identity is not a member of the specified project" });
if ( if (
identityProjectPermission.roles.some( identityProjectPermission.roles.some(
({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions ({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions
) )
) { ) {
throw new BadRequestError({ name: "Custom permission not found" }); throw new NotFoundError({ name: "Custom permission not found" });
} }
if (identityProjectPermission.orgId !== identityOrgId) { if (identityProjectPermission.orgId !== identityOrgId) {
throw new UnauthorizedError({ name: "You are not a member of this organization" }); throw new ForbiddenRequestError({ name: "Identity is not a member of the specified organization" });
} }
const rolePermissions = const rolePermissions =
@@ -261,8 +286,32 @@ export const permissionServiceFactory = ({
permissions permissions
})) || []; })) || [];
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true });
const metadataKeyValuePair = objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
);
const interpolateRules = templatedRules(
{
identity: {
id: identityProjectPermission.identityId,
username: identityProjectPermission.username,
metadata: metadataKeyValuePair
}
},
{ data: false }
);
const permission = createMongoAbility<ProjectPermissionSet>(
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
{
conditionsMatcher
}
);
return { return {
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)), permission,
membership: identityProjectPermission, membership: identityProjectPermission,
hasRole: (role: string) => hasRole: (role: string) =>
identityProjectPermission.roles.findIndex( identityProjectPermission.roles.findIndex(
@@ -277,25 +326,23 @@ export const permissionServiceFactory = ({
actorOrgId: string | undefined actorOrgId: string | undefined
) => { ) => {
const serviceToken = await serviceTokenDAL.findById(serviceTokenId); const serviceToken = await serviceTokenDAL.findById(serviceTokenId);
if (!serviceToken) throw new BadRequestError({ message: "Service token not found" }); if (!serviceToken) throw new NotFoundError({ message: "Service token not found" });
const serviceTokenProject = await projectDAL.findById(serviceToken.projectId); const serviceTokenProject = await projectDAL.findById(serviceToken.projectId);
if (!serviceTokenProject) throw new BadRequestError({ message: "Service token not linked to a project" }); if (!serviceTokenProject) throw new BadRequestError({ message: "Service token not linked to a project" });
if (serviceTokenProject.orgId !== actorOrgId) { if (serviceTokenProject.orgId !== actorOrgId) {
throw new UnauthorizedError({ message: "Service token not a part of this organization" }); throw new ForbiddenRequestError({ message: "Service token not a part of the specified organization" });
} }
if (serviceToken.projectId !== projectId) if (serviceToken.projectId !== projectId) {
throw new UnauthorizedError({ throw new ForbiddenRequestError({ name: "Service token not a part of the specified project" });
message: "Failed to find service authorization for given project" }
});
if (serviceTokenProject.orgId !== actorOrgId) if (serviceTokenProject.orgId !== actorOrgId) {
throw new UnauthorizedError({ throw new ForbiddenRequestError({ message: "Service token not a part of the specified organization" });
message: "Failed to find service authorization for given project" }
});
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []); const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
return { return {
@@ -335,8 +382,8 @@ export const permissionServiceFactory = ({
case ActorType.IDENTITY: case ActorType.IDENTITY:
return getIdentityProjectPermission(id, projectId, actorOrgId) as Promise<TProjectPermissionRT<T>>; return getIdentityProjectPermission(id, projectId, actorOrgId) as Promise<TProjectPermissionRT<T>>;
default: default:
throw new UnauthorizedError({ throw new BadRequestError({
message: "Permission not defined", message: "Invalid actor provided",
name: "Get project permission" name: "Get project permission"
}); });
} }
@@ -346,15 +393,23 @@ export const permissionServiceFactory = ({
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole); const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
if (isCustomRole) { if (isCustomRole) {
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId }); const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
if (!projectRole) throw new BadRequestError({ message: `Role not found: ${role}` }); if (!projectRole) throw new NotFoundError({ message: `Specified role was not found: ${role}` });
const rules = buildProjectPermissionRules([
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
]);
return { return {
permission: buildProjectPermission([ permission: createMongoAbility<ProjectPermissionSet>(rules, {
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions } conditionsMatcher
]), }),
role: projectRole role: projectRole
}; };
} }
return { permission: buildProjectPermission([{ role, permissions: [] }]) };
const rules = buildProjectPermissionRules([{ role, permissions: [] }]);
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
conditionsMatcher
});
return { permission };
}; };
return { return {
@@ -365,6 +420,6 @@ export const permissionServiceFactory = ({
getOrgPermissionByRole, getOrgPermissionByRole,
getProjectPermissionByRole, getProjectPermissionByRole,
buildOrgPermission, buildOrgPermission,
buildProjectPermission buildProjectPermissionRules
}; };
}; };

View File

@@ -1,9 +1,47 @@
export type TBuildProjectPermissionDTO = { import picomatch from "picomatch";
permissions?: unknown; import { z } from "zod";
role: string;
}[];
export type TBuildOrgPermissionDTO = { export enum PermissionConditionOperators {
permissions?: unknown; $IN = "$in",
role: string; $ALL = "$all",
}[]; $REGEX = "$regex",
$EQ = "$eq",
$NEQ = "$ne",
$GLOB = "$glob"
}
export const PermissionConditionSchema = {
[PermissionConditionOperators.$IN]: z.string().min(1).array(),
[PermissionConditionOperators.$ALL]: z.string().min(1).array(),
[PermissionConditionOperators.$REGEX]: z
.string()
.min(1)
.refine(
(el) => {
try {
// eslint-disable-next-line no-new
new RegExp(el);
return true;
} catch {
return false;
}
},
{ message: "Invalid regex pattern" }
),
[PermissionConditionOperators.$EQ]: z.string().min(1),
[PermissionConditionOperators.$NEQ]: z.string().min(1),
[PermissionConditionOperators.$GLOB]: z
.string()
.min(1)
.refine(
(el) => {
try {
picomatch.parse([el]);
return true;
} catch {
return false;
}
},
{ message: "Invalid glob pattern" }
)
};

View File

@@ -1,8 +1,12 @@
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability"; import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
import { z } from "zod";
import { TableName } from "@app/db/schemas";
import { conditionsMatcher } from "@app/lib/casl"; import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
export enum ProjectPermissionActions { export enum ProjectPermissionActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -37,7 +41,25 @@ export enum ProjectPermissionSub {
Kms = "kms" Kms = "kms"
} }
type SubjectFields = { export type SecretSubjectFields = {
environment: string;
secretPath: string;
// secretName: string;
// secretTags: string[];
};
export const CaslSecretsV2SubjectKnexMapper = (field: string) => {
switch (field) {
case "secretName":
return `${TableName.SecretV2}.key`;
case "secretTags":
return `${TableName.SecretTag}.slug`;
default:
break;
}
};
export type SecretFolderSubjectFields = {
environment: string; environment: string;
secretPath: string; secretPath: string;
}; };
@@ -45,11 +67,14 @@ type SubjectFields = {
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields) ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
] ]
| [ | [
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields) (
| ProjectPermissionSub.SecretFolders
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SecretFolderSubjectFields)
)
] ]
| [ProjectPermissionActions, ProjectPermissionSub.Role] | [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags] | [ProjectPermissionActions, ProjectPermissionSub.Tags]
@@ -76,128 +101,230 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]; | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
export const fullProjectPermissionSet: [ProjectPermissionActions, ProjectPermissionSub][] = [ const CASL_ACTION_SCHEMA_NATIVE_ENUM = <ACTION extends z.EnumLike>(actions: ACTION) =>
[ProjectPermissionActions.Read, ProjectPermissionSub.Secrets], z
[ProjectPermissionActions.Create, ProjectPermissionSub.Secrets], .union([z.nativeEnum(actions), z.nativeEnum(actions).array().min(1)])
[ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets], .transform((el) => (typeof el === "string" ? [el] : el));
[ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets],
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval], const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval], z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval],
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval],
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation], const SecretConditionSchema = z
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation], .object({
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation], environment: z.union([
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation], z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
]),
secretPath: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
})
.partial()
])
})
.partial();
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback], export const ProjectPermissionSchema = z.discriminatedUnion("subject", [
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback], z.object({
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
[ProjectPermissionActions.Read, ProjectPermissionSub.Member], action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
[ProjectPermissionActions.Create, ProjectPermissionSub.Member], "Describe what action an entity can take."
[ProjectPermissionActions.Edit, ProjectPermissionSub.Member], ),
[ProjectPermissionActions.Delete, ProjectPermissionSub.Member], conditions: SecretConditionSchema.describe(
"When specified, only matching conditions will be allowed to access given resource."
[ProjectPermissionActions.Read, ProjectPermissionSub.Groups], ).optional()
[ProjectPermissionActions.Create, ProjectPermissionSub.Groups], }),
[ProjectPermissionActions.Edit, ProjectPermissionSub.Groups], z.object({
[ProjectPermissionActions.Delete, ProjectPermissionSub.Groups], subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
[ProjectPermissionActions.Read, ProjectPermissionSub.Role], "Describe what action an entity can take."
[ProjectPermissionActions.Create, ProjectPermissionSub.Role], )
[ProjectPermissionActions.Edit, ProjectPermissionSub.Role], }),
[ProjectPermissionActions.Delete, ProjectPermissionSub.Role], z.object({
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
[ProjectPermissionActions.Read, ProjectPermissionSub.Integrations], action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
[ProjectPermissionActions.Create, ProjectPermissionSub.Integrations], "Describe what action an entity can take."
[ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations], )
[ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations], }),
z.object({
[ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks], subject: z.literal(ProjectPermissionSub.SecretRollback).describe("The entity this permission pertains to."),
[ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks], action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read, ProjectPermissionActions.Create]).describe(
[ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks], "Describe what action an entity can take."
[ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks], )
}),
[ProjectPermissionActions.Read, ProjectPermissionSub.Identity], z.object({
[ProjectPermissionActions.Create, ProjectPermissionSub.Identity], subject: z.literal(ProjectPermissionSub.Member).describe("The entity this permission pertains to."),
[ProjectPermissionActions.Edit, ProjectPermissionSub.Identity], action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
[ProjectPermissionActions.Delete, ProjectPermissionSub.Identity], "Describe what action an entity can take."
)
[ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens], }),
[ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens], z.object({
[ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens], subject: z.literal(ProjectPermissionSub.Groups).describe("The entity this permission pertains to."),
[ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens], action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
[ProjectPermissionActions.Read, ProjectPermissionSub.Settings], )
[ProjectPermissionActions.Create, ProjectPermissionSub.Settings], }),
[ProjectPermissionActions.Edit, ProjectPermissionSub.Settings], z.object({
[ProjectPermissionActions.Delete, ProjectPermissionSub.Settings], subject: z.literal(ProjectPermissionSub.Role).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
[ProjectPermissionActions.Read, ProjectPermissionSub.Environments], "Describe what action an entity can take."
[ProjectPermissionActions.Create, ProjectPermissionSub.Environments], )
[ProjectPermissionActions.Edit, ProjectPermissionSub.Environments], }),
[ProjectPermissionActions.Delete, ProjectPermissionSub.Environments], z.object({
subject: z.literal(ProjectPermissionSub.Integrations).describe("The entity this permission pertains to."),
[ProjectPermissionActions.Read, ProjectPermissionSub.Tags], action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
[ProjectPermissionActions.Create, ProjectPermissionSub.Tags], "Describe what action an entity can take."
[ProjectPermissionActions.Edit, ProjectPermissionSub.Tags], )
[ProjectPermissionActions.Delete, ProjectPermissionSub.Tags], }),
z.object({
// TODO(Daniel): Remove the audit logs permissions from project-level permissions. subject: z.literal(ProjectPermissionSub.Webhooks).describe("The entity this permission pertains to."),
// TODO: We haven't done this yet because it might break existing roles, since those roles will become "invalid" since the audit log permission defined on those roles, no longer exist in the project-level defined permissions. action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
[ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs], "Describe what action an entity can take."
[ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs], )
[ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs], }),
[ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs], z.object({
subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."),
[ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList], action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
[ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList], "Describe what action an entity can take."
[ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList], )
[ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList], }),
z.object({
// double check if all CRUD are needed for CA and Certificates subject: z.literal(ProjectPermissionSub.ServiceTokens).describe("The entity this permission pertains to."),
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities], action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities], "Describe what action an entity can take."
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities], )
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities], }),
z.object({
[ProjectPermissionActions.Read, ProjectPermissionSub.Certificates], subject: z.literal(ProjectPermissionSub.Settings).describe("The entity this permission pertains to."),
[ProjectPermissionActions.Create, ProjectPermissionSub.Certificates], action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
[ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates], "Describe what action an entity can take."
[ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates], )
}),
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates], z.object({
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates], subject: z.literal(ProjectPermissionSub.Environments).describe("The entity this permission pertains to."),
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates], action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates], "Describe what action an entity can take."
)
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts], }),
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts], z.object({
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts], subject: z.literal(ProjectPermissionSub.Tags).describe("The entity this permission pertains to."),
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts], action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections], )
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections], }),
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections], z.object({
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections], subject: z.literal(ProjectPermissionSub.AuditLogs).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
[ProjectPermissionActions.Edit, ProjectPermissionSub.Project], "Describe what action an entity can take."
[ProjectPermissionActions.Delete, ProjectPermissionSub.Project], )
}),
[ProjectPermissionActions.Edit, ProjectPermissionSub.Kms] z.object({
]; subject: z.literal(ProjectPermissionSub.IpAllowList).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.CertificateAuthorities).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to. "),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.PkiCollections).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.Project).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete]).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.Kms).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Edit]).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe(
"Describe what action an entity can take."
)
})
]);
const buildAdminPermissionRules = () => { const buildAdminPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
// Admins get full access to everything // Admins get full access to everything
fullProjectPermissionSet.forEach((permission) => { [
const [action, subject] = permission; ProjectPermissionSub.Secrets,
can(action, subject); ProjectPermissionSub.SecretApproval,
ProjectPermissionSub.SecretRotation,
ProjectPermissionSub.Member,
ProjectPermissionSub.Groups,
ProjectPermissionSub.Role,
ProjectPermissionSub.Integrations,
ProjectPermissionSub.Webhooks,
ProjectPermissionSub.Identity,
ProjectPermissionSub.ServiceTokens,
ProjectPermissionSub.Settings,
ProjectPermissionSub.Environments,
ProjectPermissionSub.Tags,
ProjectPermissionSub.AuditLogs,
ProjectPermissionSub.IpAllowList,
ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.Certificates,
ProjectPermissionSub.CertificateTemplates,
ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections
].forEach((el) => {
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
el as ProjectPermissionSub
);
}); });
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
return rules; return rules;
}; };
@@ -206,73 +333,116 @@ export const projectAdminPermissions = buildAdminPermissionRules();
const buildMemberPermissionRules = () => { const buildMemberPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets); [
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); ProjectPermissionActions.Read,
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Secrets
);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.Member);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); can([ProjectPermissionActions.Read], ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); can(
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); [
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); ProjectPermissionActions.Read,
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Integrations
);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); can(
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks); [
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); ProjectPermissionActions.Read,
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Webhooks
);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); can(
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity); [
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); ProjectPermissionActions.Read,
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity); ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Identity
);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); [
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens); ProjectPermissionActions.Read,
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens); ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.ServiceTokens
);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); can(
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings); [
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); ProjectPermissionActions.Read,
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings); ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Settings
);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); can(
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments); [
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments); ProjectPermissionActions.Read,
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments); ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Environments
);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); can(
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags); [
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags); ProjectPermissionActions.Read,
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags); ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Tags
);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can([ProjectPermissionActions.Read], ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
// double check if all CRUD are needed for CA and Certificates // double check if all CRUD are needed for CA and Certificates
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities); can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); can(
can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); [
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates); ProjectPermissionActions.Read,
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Certificates
);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates); can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
return rules; return rules;
}; };
@@ -382,32 +552,19 @@ export const isAtLeastAsPrivilegedWorkspace = (
return set1.size >= set2.size; return set1.size >= set2.size;
}; };
/* eslint-enable */
/* export const SecretV2SubjectFieldMapper = (arg: string) => {
* Case: The user requests to create a role with permissions that are not valid and not supposed to be used ever. switch (arg) {
* If we don't check for this, we can run into issues where functions like the `isAtLeastAsPrivileged` will not work as expected, because we compare the size of each permission set. case "environment":
* If the permission set contains invalid permissions, the size will be different, and result in incorrect results. return null;
*/ case "secretPath":
export const validateProjectPermissions = (permissions: unknown) => { return null;
const parsedPermissions = case "secretName":
typeof permissions === "string" ? (JSON.parse(permissions) as string[]) : (permissions as string[]); return `${TableName.SecretV2}.key`;
case "secretTags":
const flattenedPermissions = [...parsedPermissions]; return `${TableName.SecretTag}.slug`;
default:
for (const perm of flattenedPermissions) { throw new BadRequestError({ message: `Invalid dynamic knex operator field: ${arg}` });
const [action, subject] = perm;
if (
!fullProjectPermissionSet.find(
(currentPermission) => currentPermission[0] === action && currentPermission[1] === subject
)
) {
throw new BadRequestError({
message: `Permission action ${action} on subject ${subject} is not valid`,
name: "Create Role"
});
}
} }
}; };
/* eslint-enable */

View File

@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import ms from "ms"; import ms from "ms";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
@@ -42,7 +42,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
...dto ...dto
}: TCreateUserPrivilegeDTO) => { }: TCreateUserPrivilegeDTO) => {
const projectMembership = await projectMembershipDAL.findById(projectMembershipId); const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" }); if (!projectMembership) throw new NotFoundError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -94,14 +94,14 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
...dto ...dto
}: TUpdateUserPrivilegeDTO) => { }: TUpdateUserPrivilegeDTO) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" }); if (!userPrivilege) throw new NotFoundError({ message: "User additional privilege not found" });
const projectMembership = await projectMembershipDAL.findOne({ const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId, userId: userPrivilege.userId,
projectId: userPrivilege.projectId projectId: userPrivilege.projectId
}); });
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" }); if (!projectMembership) throw new NotFoundError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -147,13 +147,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => { const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" }); if (!userPrivilege) throw new NotFoundError({ message: "User additional privilege not found" });
const projectMembership = await projectMembershipDAL.findOne({ const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId, userId: userPrivilege.userId,
projectId: userPrivilege.projectId projectId: userPrivilege.projectId
}); });
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" }); if (!projectMembership) throw new NotFoundError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -176,13 +176,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorAuthMethod actorAuthMethod
}: TGetUserPrivilegeDetailsDTO) => { }: TGetUserPrivilegeDetailsDTO) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" }); if (!userPrivilege) throw new NotFoundError({ message: "User additional privilege not found" });
const projectMembership = await projectMembershipDAL.findOne({ const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId, userId: userPrivilege.userId,
projectId: userPrivilege.projectId projectId: userPrivilege.projectId
}); });
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" }); if (!projectMembership) throw new NotFoundError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -204,7 +204,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorAuthMethod actorAuthMethod
}: TListUserPrivilegesDTO) => { }: TListUserPrivilegesDTO) => {
const projectMembership = await projectMembershipDAL.findById(projectMembershipId); const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" }); if (!projectMembership) throw new NotFoundError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,

View File

@@ -19,10 +19,11 @@ import {
infisicalSymmetricDecrypt, infisicalSymmetricDecrypt,
infisicalSymmetricEncypt infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption"; } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { AuthTokenType } from "@app/services/auth/auth-type"; import { AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
@@ -51,6 +52,8 @@ type TSamlConfigServiceFactoryDep = {
TOrgDALFactory, TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
>; >;
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">; orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
@@ -71,7 +74,8 @@ export const samlConfigServiceFactory = ({
permissionService, permissionService,
licenseService, licenseService,
tokenService, tokenService,
smtpService smtpService,
identityMetadataDAL
}: TSamlConfigServiceFactoryDep) => { }: TSamlConfigServiceFactoryDep) => {
const createSamlCfg = async ({ const createSamlCfg = async ({
cert, cert,
@@ -187,7 +191,7 @@ export const samlConfigServiceFactory = ({
const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null }; const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null };
const orgBot = await orgBotDAL.findOne({ orgId }); const orgBot = await orgBotDAL.findOne({ orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); if (!orgBot) throw new NotFoundError({ message: "Organization bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({ const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey, ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV, iv: orgBot.symmetricKeyIV,
@@ -253,7 +257,7 @@ export const samlConfigServiceFactory = ({
ssoConfig = await samlConfigDAL.findById(id); ssoConfig = await samlConfigDAL.findById(id);
} }
if (!ssoConfig) throw new BadRequestError({ message: "Failed to find organization SSO data" }); if (!ssoConfig) throw new NotFoundError({ message: "Failed to find organization SSO data" });
// when dto is type id means it's internally used // when dto is type id means it's internally used
if (dto.type === "org") { if (dto.type === "org") {
@@ -279,7 +283,7 @@ export const samlConfigServiceFactory = ({
} = ssoConfig; } = ssoConfig;
const orgBot = await orgBotDAL.findOne({ orgId: ssoConfig.orgId }); const orgBot = await orgBotDAL.findOne({ orgId: ssoConfig.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); if (!orgBot) throw new NotFoundError({ message: "Organization bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({ const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey, ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV, iv: orgBot.symmetricKeyIV,
@@ -332,13 +336,14 @@ export const samlConfigServiceFactory = ({
lastName, lastName,
authProvider, authProvider,
orgId, orgId,
relayState relayState,
metadata
}: TSamlLoginDTO) => { }: TSamlLoginDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.SAML)) { if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.SAML)) {
throw new BadRequestError({ throw new ForbiddenRequestError({
message: "Login with SAML is disabled by administrator." message: "Login with SAML is disabled by administrator."
}); });
} }
@@ -350,7 +355,7 @@ export const samlConfigServiceFactory = ({
}); });
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) throw new BadRequestError({ message: "Org not found" }); if (!organization) throw new NotFoundError({ message: "Organization not found" });
let user: TUsers; let user: TUsers;
if (userAlias) { if (userAlias) {
@@ -386,6 +391,21 @@ export const samlConfigServiceFactory = ({
); );
} }
if (metadata && foundUser.id) {
await identityMetadataDAL.delete({ userId: foundUser.id, orgId }, tx);
if (metadata.length) {
await identityMetadataDAL.insertMany(
metadata.map(({ key, value }) => ({
userId: foundUser.id,
orgId,
key,
value
})),
tx
);
}
}
return foundUser; return foundUser;
}); });
} else { } else {
@@ -474,6 +494,20 @@ export const samlConfigServiceFactory = ({
); );
} }
if (metadata && newUser.id) {
await identityMetadataDAL.delete({ userId: newUser.id, orgId }, tx);
if (metadata.length) {
await identityMetadataDAL.insertMany(
metadata.map(({ key, value }) => ({
userId: newUser?.id,
orgId,
key,
value
})),
tx
);
}
}
return newUser; return newUser;
}); });
} }

View File

@@ -53,4 +53,5 @@ export type TSamlLoginDTO = {
orgId: string; orgId: string;
// saml thingy // saml thingy
relayState?: string; relayState?: string;
metadata?: { key: string; value: string }[];
}; };

View File

@@ -9,7 +9,7 @@ import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal"; import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
import { AuthTokenType } from "@app/services/auth/auth-type"; import { AuthTokenType } from "@app/services/auth/auth-type";
@@ -75,7 +75,14 @@ type TScimServiceFactoryDep = {
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
groupDAL: Pick< groupDAL: Pick<
TGroupDALFactory, TGroupDALFactory,
"create" | "findOne" | "findAllGroupMembers" | "delete" | "findGroups" | "transaction" | "updateById" | "update" | "create"
| "findOne"
| "findAllGroupPossibleMembers"
| "delete"
| "findGroups"
| "transaction"
| "updateById"
| "update"
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
userGroupMembershipDAL: Pick< userGroupMembershipDAL: Pick<
@@ -169,7 +176,7 @@ export const scimServiceFactory = ({
const deleteScimToken = async ({ scimTokenId, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteScimTokenDTO) => { const deleteScimToken = async ({ scimTokenId, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteScimTokenDTO) => {
let scimToken = await scimDAL.findById(scimTokenId); let scimToken = await scimDAL.findById(scimTokenId);
if (!scimToken) throw new BadRequestError({ message: "Failed to find SCIM token to delete" }); if (!scimToken) throw new NotFoundError({ message: "Failed to find SCIM token to delete" });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
@@ -775,7 +782,7 @@ export const scimServiceFactory = ({
}); });
} }
const users = await groupDAL.findAllGroupMembers({ const users = await groupDAL.findAllGroupPossibleMembers({
orgId: group.orgId, orgId: group.orgId,
groupId: group.id groupId: group.id
}); });

View File

@@ -1,33 +1,62 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies } from "@app/db/schemas"; import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies, TUsers } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>; export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>;
export const secretApprovalPolicyDALFactory = (db: TDbClient) => { export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
const secretApprovalPolicyOrm = ormify(db, TableName.SecretApprovalPolicy); const secretApprovalPolicyOrm = ormify(db, TableName.SecretApprovalPolicy);
const secretApprovalPolicyFindQuery = (tx: Knex, filter: TFindFilter<TSecretApprovalPolicies>) => const secretApprovalPolicyFindQuery = (
tx: Knex,
filter: TFindFilter<TSecretApprovalPolicies>,
customFilter?: {
sapId?: string;
}
) =>
tx(TableName.SecretApprovalPolicy) tx(TableName.SecretApprovalPolicy)
// eslint-disable-next-line // eslint-disable-next-line
.where(buildFindFilter(filter)) .where(buildFindFilter(filter))
.where((qb) => {
if (customFilter?.sapId) {
void qb.where(`${TableName.SecretApprovalPolicy}.id`, "=", customFilter.sapId);
}
})
.join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`) .join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.leftJoin( .leftJoin(
TableName.SecretApprovalPolicyApprover, TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId` `${TableName.SecretApprovalPolicyApprover}.policyId`
) )
.leftJoin(
.leftJoin(TableName.Users, `${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`) TableName.UserGroupMembership,
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("secretApprovalPolicyApproverUser"),
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
"secretApprovalPolicyApproverUser.id"
)
.leftJoin<TUsers>(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.select( .select(
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), tx.ref("id").withSchema("secretApprovalPolicyApproverUser").as("approverUserId"),
tx.ref("email").withSchema(TableName.Users).as("approverEmail"), tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("firstName").withSchema(TableName.Users).as("approverFirstName"), tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
tx.ref("lastName").withSchema(TableName.Users).as("approverLastName") tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName")
)
.select(
tx.ref("approverGroupId").withSchema(TableName.SecretApprovalPolicyApprover),
tx.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
tx.ref("email").withSchema(TableName.Users).as("approverGroupEmail"),
tx.ref("firstName").withSchema(TableName.Users).as("approverGroupFirstName"),
tx.ref("lastName").withSchema(TableName.Users).as("approverGroupLastName")
) )
.select( .select(
tx.ref("name").withSchema(TableName.Environment).as("envName"), tx.ref("name").withSchema(TableName.Environment).as("envName"),
@@ -55,11 +84,31 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
{ {
key: "approverUserId", key: "approverUserId",
label: "userApprovers" as const, label: "userApprovers" as const,
mapper: ({ approverUserId, approverEmail, approverFirstName, approverLastName }) => ({ mapper: ({
userId: approverUserId, approverUserId: userId,
email: approverEmail, approverEmail: email,
firstName: approverFirstName, approverFirstName: firstName,
lastName: approverLastName approverLastName: lastName
}) => ({
userId,
email,
firstName,
lastName
})
},
{
key: "approverGroupUserId",
label: "userApprovers" as const,
mapper: ({
approverGroupUserId: userId,
approverGroupEmail: email,
approverGroupFirstName: firstName,
approverGroupLastName: lastName
}) => ({
userId,
email,
firstName,
lastName
}) })
} }
] ]
@@ -71,9 +120,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
} }
}; };
const find = async (filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, tx?: Knex) => { const find = async (
filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
customFilter?: {
sapId?: string;
},
tx?: Knex
) => {
try { try {
const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter); const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter);
const formatedDoc = sqlNestRelationships({ const formatedDoc = sqlNestRelationships({
data: docs, data: docs,
key: "id", key: "id",
@@ -83,11 +138,35 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
...SecretApprovalPoliciesSchema.parse(data) ...SecretApprovalPoliciesSchema.parse(data)
}), }),
childrenMapper: [ childrenMapper: [
{
key: "approverUserId",
label: "approvers" as const,
mapper: ({ approverUserId: id, approverUsername }) => ({
type: ApproverType.User,
name: approverUsername,
id
})
},
{
key: "approverGroupId",
label: "approvers" as const,
mapper: ({ approverGroupId: id }) => ({
type: ApproverType.Group,
id
})
},
{ {
key: "approverUserId", key: "approverUserId",
label: "userApprovers" as const, label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({ mapper: ({ approverUserId: userId }) => ({
userId: approverUserId userId
})
},
{
key: "approverGroupUserId",
label: "userApprovers" as const,
mapper: ({ approverGroupUserId: userId }) => ({
userId
}) })
} }
] ]

View File

@@ -3,11 +3,13 @@ import picomatch from "picomatch";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { containsGlobPatterns } from "@app/lib/picomatch"; import { containsGlobPatterns } from "@app/lib/picomatch";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal"; import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal"; import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
@@ -15,6 +17,7 @@ import {
TCreateSapDTO, TCreateSapDTO,
TDeleteSapDTO, TDeleteSapDTO,
TGetBoardSapDTO, TGetBoardSapDTO,
TGetSapByIdDTO,
TListSapDTO, TListSapDTO,
TUpdateSapDTO TUpdateSapDTO
} from "./secret-approval-policy-types"; } from "./secret-approval-policy-types";
@@ -28,6 +31,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory; secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
userDAL: Pick<TUserDALFactory, "find">;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory; secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
@@ -39,6 +43,7 @@ export const secretApprovalPolicyServiceFactory = ({
permissionService, permissionService,
secretApprovalPolicyApproverDAL, secretApprovalPolicyApproverDAL,
projectEnvDAL, projectEnvDAL,
userDAL,
licenseService licenseService
}: TSecretApprovalPolicyServiceFactoryDep) => { }: TSecretApprovalPolicyServiceFactoryDep) => {
const createSecretApprovalPolicy = async ({ const createSecretApprovalPolicy = async ({
@@ -54,7 +59,19 @@ export const secretApprovalPolicyServiceFactory = ({
environment, environment,
enforcementLevel enforcementLevel
}: TCreateSapDTO) => { }: TCreateSapDTO) => {
if (approvals > approvers.length) const groupApprovers = approvers
?.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id);
const userApprovers = approvers
?.filter((approver) => approver.type === ApproverType.User)
.map((approver) => approver.id)
.filter(Boolean) as string[];
const userApproverNames = approvers
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
.filter(Boolean) as string[];
if (!groupApprovers.length && approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -78,7 +95,7 @@ export const secretApprovalPolicyServiceFactory = ({
} }
const env = await projectEnvDAL.findOne({ slug: environment, projectId }); const env = await projectEnvDAL.findOne({ slug: environment, projectId });
if (!env) throw new BadRequestError({ message: "Environment not found" }); if (!env) throw new NotFoundError({ message: "Environment not found" });
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => { const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
const doc = await secretApprovalPolicyDAL.create( const doc = await secretApprovalPolicyDAL.create(
@@ -91,15 +108,48 @@ export const secretApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
let userApproverIds = userApprovers;
if (userApproverNames.length) {
const approverUsers = await userDAL.find(
{
$in: {
username: userApproverNames
}
},
{ tx }
);
const approverNamesFromDb = approverUsers.map((user) => user.username);
const invalidUsernames = userApproverNames?.filter((username) => !approverNamesFromDb.includes(username));
if (invalidUsernames?.length) {
throw new BadRequestError({
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
});
}
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
}
await secretApprovalPolicyApproverDAL.insertMany( await secretApprovalPolicyApproverDAL.insertMany(
approvers.map((approverUserId) => ({ userApproverIds.map((approverUserId) => ({
approverUserId, approverUserId,
policyId: doc.id policyId: doc.id
})), })),
tx tx
); );
await secretApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((approverGroupId) => ({
approverGroupId,
policyId: doc.id
})),
tx
);
return doc; return doc;
}); });
return { ...secretApproval, environment: env, projectId }; return { ...secretApproval, environment: env, projectId };
}; };
@@ -115,8 +165,20 @@ export const secretApprovalPolicyServiceFactory = ({
secretPolicyId, secretPolicyId,
enforcementLevel enforcementLevel
}: TUpdateSapDTO) => { }: TUpdateSapDTO) => {
const groupApprovers = approvers
?.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id);
const userApprovers = approvers
?.filter((approver) => approver.type === ApproverType.User)
.map((approver) => approver.id)
.filter(Boolean) as string[];
const userApproverNames = approvers
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
.filter(Boolean) as string[];
const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId); const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); if (!secretApprovalPolicy) throw new NotFoundError({ message: "Secret approval policy not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -146,16 +208,52 @@ export const secretApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
if (approvers) { if (approvers) {
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx); let userApproverIds = userApprovers;
if (userApproverNames) {
const approverUsers = await userDAL.find(
{
$in: {
username: userApproverNames
}
},
{ tx }
);
const approverNamesFromDb = approverUsers.map((user) => user.username);
const invalidUsernames = userApproverNames?.filter((username) => !approverNamesFromDb.includes(username));
if (invalidUsernames?.length) {
throw new BadRequestError({
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
});
}
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
}
await secretApprovalPolicyApproverDAL.insertMany( await secretApprovalPolicyApproverDAL.insertMany(
approvers.map((approverUserId) => ({ userApproverIds.map((approverUserId) => ({
approverUserId, approverUserId,
policyId: doc.id policyId: doc.id
})), })),
tx tx
); );
} }
if (groupApprovers) {
await secretApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((approverGroupId) => ({
approverGroupId,
policyId: doc.id
})),
tx
);
}
return doc; return doc;
}); });
return { return {
@@ -173,7 +271,7 @@ export const secretApprovalPolicyServiceFactory = ({
actorOrgId actorOrgId
}: TDeleteSapDTO) => { }: TDeleteSapDTO) => {
const sapPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId); const sapPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
if (!sapPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); if (!sapPolicy) throw new NotFoundError({ message: "Secret approval policy not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -222,7 +320,7 @@ export const secretApprovalPolicyServiceFactory = ({
const getSecretApprovalPolicy = async (projectId: string, environment: string, path: string) => { const getSecretApprovalPolicy = async (projectId: string, environment: string, path: string) => {
const secretPath = removeTrailingSlash(path); const secretPath = removeTrailingSlash(path);
const env = await projectEnvDAL.findOne({ slug: environment, projectId }); const env = await projectEnvDAL.findOne({ slug: environment, projectId });
if (!env) throw new BadRequestError({ message: "Environment not found" }); if (!env) throw new NotFoundError({ message: "Environment not found" });
const policies = await secretApprovalPolicyDAL.find({ envId: env.id }); const policies = await secretApprovalPolicyDAL.find({ envId: env.id });
if (!policies.length) return; if (!policies.length) return;
@@ -260,12 +358,41 @@ export const secretApprovalPolicyServiceFactory = ({
return getSecretApprovalPolicy(projectId, environment, secretPath); return getSecretApprovalPolicy(projectId, environment, secretPath);
}; };
const getSecretApprovalPolicyById = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
sapId
}: TGetSapByIdDTO) => {
const [sapPolicy] = await secretApprovalPolicyDAL.find({}, { sapId });
if (!sapPolicy) {
throw new NotFoundError({
message: "Cannot find secret approval policy"
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
sapPolicy.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
return sapPolicy;
};
return { return {
createSecretApprovalPolicy, createSecretApprovalPolicy,
updateSecretApprovalPolicy, updateSecretApprovalPolicy,
deleteSecretApprovalPolicy, deleteSecretApprovalPolicy,
getSecretApprovalPolicy, getSecretApprovalPolicy,
getSecretApprovalPolicyByProjectId, getSecretApprovalPolicyByProjectId,
getSecretApprovalPolicyOfFolder getSecretApprovalPolicyOfFolder,
getSecretApprovalPolicyById
}; };
}; };

View File

@@ -1,10 +1,12 @@
import { EnforcementLevel, TProjectPermission } from "@app/lib/types"; import { EnforcementLevel, TProjectPermission } from "@app/lib/types";
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
export type TCreateSapDTO = { export type TCreateSapDTO = {
approvals: number; approvals: number;
secretPath?: string | null; secretPath?: string | null;
environment: string; environment: string;
approvers: string[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
projectId: string; projectId: string;
name: string; name: string;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
@@ -14,7 +16,7 @@ export type TUpdateSapDTO = {
secretPolicyId: string; secretPolicyId: string;
approvals?: number; approvals?: number;
secretPath?: string | null; secretPath?: string | null;
approvers: string[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
name?: string; name?: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -25,6 +27,8 @@ export type TDeleteSapDTO = {
export type TListSapDTO = TProjectPermission; export type TListSapDTO = TProjectPermission;
export type TGetSapByIdDTO = Omit<TProjectPermission, "projectId"> & { sapId: string };
export type TGetBoardSapDTO = { export type TGetBoardSapDTO = {
projectId: string; projectId: string;
environment: string; environment: string;

View File

@@ -48,16 +48,26 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
`committerUser.id` `committerUser.id`
) )
.join( .leftJoin(
TableName.SecretApprovalPolicyApprover, TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId` `${TableName.SecretApprovalPolicyApprover}.policyId`
) )
.join<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("secretApprovalPolicyApproverUser"), db(TableName.Users).as("secretApprovalPolicyApproverUser"),
`${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.SecretApprovalPolicyApprover}.approverUserId`,
"secretApprovalPolicyApproverUser.id" "secretApprovalPolicyApproverUser.id"
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("secretApprovalPolicyGroupApproverUser"),
`${TableName.UserGroupMembership}.userId`,
`secretApprovalPolicyGroupApproverUser.id`
)
.leftJoin( .leftJoin(
TableName.SecretApprovalRequestReviewer, TableName.SecretApprovalRequestReviewer,
`${TableName.SecretApprovalRequest}.id`, `${TableName.SecretApprovalRequest}.id`,
@@ -71,10 +81,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.SecretApprovalRequest)) .select(selectAllTableCols(TableName.SecretApprovalRequest))
.select( .select(
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
tx.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"), tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("email").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"), tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
tx.ref("username").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"), tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
tx.ref("firstName").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupFirstName"),
tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName"), tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName"),
tx.ref("lastName").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupLastName"),
tx.ref("email").withSchema("statusChangedByUser").as("statusChangedByUserEmail"), tx.ref("email").withSchema("statusChangedByUser").as("statusChangedByUserEmail"),
tx.ref("username").withSchema("statusChangedByUser").as("statusChangedByUserUsername"), tx.ref("username").withSchema("statusChangedByUser").as("statusChangedByUserUsername"),
tx.ref("firstName").withSchema("statusChangedByUser").as("statusChangedByUserFirstName"), tx.ref("firstName").withSchema("statusChangedByUser").as("statusChangedByUserFirstName"),
@@ -152,13 +167,30 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
key: "approverUserId", key: "approverUserId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ mapper: ({
approverUserId, approverUserId: userId,
approverEmail: email, approverEmail: email,
approverUsername: username, approverUsername: username,
approverLastName: lastName, approverLastName: lastName,
approverFirstName: firstName approverFirstName: firstName
}) => ({ }) => ({
userId: approverUserId, userId,
email,
firstName,
lastName,
username
})
},
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({
approverGroupUserId: userId,
approverGroupEmail: email,
approverGroupUsername: username,
approverGroupLastName: lastName,
approverGroupFirstName: firstName
}) => ({
userId,
email, email,
firstName, firstName,
lastName, lastName,
@@ -236,11 +268,16 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.policyId`, `${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id` `${TableName.SecretApprovalPolicy}.id`
) )
.join( .leftJoin(
TableName.SecretApprovalPolicyApprover, TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId` `${TableName.SecretApprovalPolicyApprover}.policyId`
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.join<TUsers>( .join<TUsers>(
db(TableName.Users).as("committerUser"), db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
@@ -269,6 +306,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
void bd void bd
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId) .where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId) .orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
.orWhere(`${TableName.UserGroupMembership}.userId`, userId)
) )
.select(selectAllTableCols(TableName.SecretApprovalRequest)) .select(selectAllTableCols(TableName.SecretApprovalRequest))
.select( .select(
@@ -289,6 +327,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
db.ref("email").withSchema("committerUser").as("committerUserEmail"), db.ref("email").withSchema("committerUser").as("committerUserEmail"),
db.ref("username").withSchema("committerUser").as("committerUserUsername"), db.ref("username").withSchema("committerUser").as("committerUserUsername"),
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"), db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
@@ -334,7 +373,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
{ {
key: "approverUserId", key: "approverUserId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ approverUserId }) => approverUserId mapper: ({ approverUserId }) => ({ userId: approverUserId })
}, },
{ {
key: "commitId", key: "commitId",
@@ -344,6 +383,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
id, id,
secretId secretId
}) })
},
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({ approverGroupUserId }) => ({ userId: approverGroupUserId })
} }
] ]
}); });
@@ -371,11 +415,16 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.policyId`, `${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id` `${TableName.SecretApprovalPolicy}.id`
) )
.join( .leftJoin(
TableName.SecretApprovalPolicyApprover, TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId` `${TableName.SecretApprovalPolicyApprover}.policyId`
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.join<TUsers>( .join<TUsers>(
db(TableName.Users).as("committerUser"), db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
@@ -404,6 +453,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
void bd void bd
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId) .where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId) .orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
.orWhere(`${TableName.UserGroupMembership}.userId`, userId)
) )
.select(selectAllTableCols(TableName.SecretApprovalRequest)) .select(selectAllTableCols(TableName.SecretApprovalRequest))
.select( .select(
@@ -424,6 +474,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
db.ref("email").withSchema("committerUser").as("committerUserEmail"), db.ref("email").withSchema("committerUser").as("committerUserEmail"),
db.ref("username").withSchema("committerUser").as("committerUserUsername"), db.ref("username").withSchema("committerUser").as("committerUserUsername"),
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"), db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
@@ -469,7 +520,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
{ {
key: "approverUserId", key: "approverUserId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ approverUserId }) => approverUserId mapper: ({ approverUserId }) => ({ userId: approverUserId })
}, },
{ {
key: "commitId", key: "commitId",
@@ -479,6 +530,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
id, id,
secretId secretId
}) })
},
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({ approverGroupUserId }) => ({
userId: approverGroupUserId
})
} }
] ]
}); });

View File

@@ -8,7 +8,7 @@ import {
TSecretApprovalRequestsSecrets, TSecretApprovalRequestsSecrets,
TSecretTags TSecretTags
} from "@app/db/schemas"; } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { DatabaseError, NotFoundError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApprovalRequestSecretDALFactory>; export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApprovalRequestSecretDALFactory>;
@@ -31,7 +31,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
); );
if (existingApprovalSecrets.length !== data.length) { if (existingApprovalSecrets.length !== data.length) {
throw new BadRequestError({ message: "Some of the secret approvals do not exist" }); throw new NotFoundError({ message: "Some of the secret approvals do not exist" });
} }
if (data.length === 0) return []; if (data.length === 0) return [];

View File

@@ -10,7 +10,7 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy, pick, unique } from "@app/lib/fn"; import { groupBy, pick, unique } from "@app/lib/fn";
import { setKnexStringValue } from "@app/lib/knex"; import { setKnexStringValue } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -204,7 +204,7 @@ export const secretApprovalRequestServiceFactory = ({
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const secretApprovalRequest = await secretApprovalRequestDAL.findById(id); const secretApprovalRequest = await secretApprovalRequestDAL.findById(id);
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (!secretApprovalRequest) throw new NotFoundError({ message: "Secret approval request not found" });
const { projectId } = secretApprovalRequest; const { projectId } = secretApprovalRequest;
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
@@ -222,7 +222,7 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalRequest.committerUserId !== actorId && secretApprovalRequest.committerUserId !== actorId &&
!policy.approvers.find(({ userId }) => userId === actorId) !policy.approvers.find(({ userId }) => userId === actorId)
) { ) {
throw new UnauthorizedError({ message: "User has no access" }); throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
} }
let secrets; let secrets;
@@ -271,7 +271,7 @@ export const secretApprovalRequestServiceFactory = ({
: undefined : undefined
})); }));
} else { } else {
if (!botKey) throw new BadRequestError({ message: "Bot key not found" }); if (!botKey) throw new NotFoundError({ message: "Project bot key not found" });
const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id); const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
secrets = encrypedSecrets.map((el) => ({ secrets = encrypedSecrets.map((el) => ({
...el, ...el,
@@ -307,7 +307,7 @@ export const secretApprovalRequestServiceFactory = ({
actorOrgId actorOrgId
}: TReviewRequestDTO) => { }: TReviewRequestDTO) => {
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId); const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (!secretApprovalRequest) throw new NotFoundError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
@@ -331,7 +331,7 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalRequest.committerUserId !== actorId && secretApprovalRequest.committerUserId !== actorId &&
!policy.approvers.find(({ userId }) => userId === actorId) !policy.approvers.find(({ userId }) => userId === actorId)
) { ) {
throw new UnauthorizedError({ message: "User has no access" }); throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
} }
const reviewStatus = await secretApprovalRequestReviewerDAL.transaction(async (tx) => { const reviewStatus = await secretApprovalRequestReviewerDAL.transaction(async (tx) => {
const review = await secretApprovalRequestReviewerDAL.findOne( const review = await secretApprovalRequestReviewerDAL.findOne(
@@ -365,7 +365,7 @@ export const secretApprovalRequestServiceFactory = ({
actorAuthMethod actorAuthMethod
}: TStatusChangeDTO) => { }: TStatusChangeDTO) => {
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId); const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (!secretApprovalRequest) throw new NotFoundError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
@@ -389,7 +389,7 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalRequest.committerUserId !== actorId && secretApprovalRequest.committerUserId !== actorId &&
!policy.approvers.find(({ userId }) => userId === actorId) !policy.approvers.find(({ userId }) => userId === actorId)
) { ) {
throw new UnauthorizedError({ message: "User has no access" }); throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
} }
if (secretApprovalRequest.hasMerged) throw new BadRequestError({ message: "Approval request has been merged" }); if (secretApprovalRequest.hasMerged) throw new BadRequestError({ message: "Approval request has been merged" });
@@ -414,7 +414,7 @@ export const secretApprovalRequestServiceFactory = ({
bypassReason bypassReason
}: TMergeSecretApprovalRequestDTO) => { }: TMergeSecretApprovalRequestDTO) => {
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId); const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (!secretApprovalRequest) throw new NotFoundError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
@@ -439,7 +439,7 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalRequest.committerUserId !== actorId && secretApprovalRequest.committerUserId !== actorId &&
!policy.approvers.find(({ userId }) => userId === actorId) !policy.approvers.find(({ userId }) => userId === actorId)
) { ) {
throw new UnauthorizedError({ message: "User has no access" }); throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
} }
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>( const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
(prev, curr) => ({ ...prev, [curr.userId.toString()]: curr.status as ApprovalStatus }), (prev, curr) => ({ ...prev, [curr.userId.toString()]: curr.status as ApprovalStatus }),
@@ -447,8 +447,8 @@ export const secretApprovalRequestServiceFactory = ({
); );
const hasMinApproval = const hasMinApproval =
secretApprovalRequest.policy.approvals <= secretApprovalRequest.policy.approvals <=
secretApprovalRequest.policy.approvers.filter( secretApprovalRequest.policy.approvers.filter(({ userId: approverId }) =>
({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED approverId ? reviewers[approverId] === ApprovalStatus.APPROVED : false
).length; ).length;
const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft; const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
@@ -462,7 +462,7 @@ export const secretApprovalRequestServiceFactory = ({
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2( const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2(
secretApprovalRequest.id secretApprovalRequest.id
); );
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" }); if (!secretApprovalSecrets) throw new NotFoundError({ message: "No secrets found" });
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
@@ -602,7 +602,7 @@ export const secretApprovalRequestServiceFactory = ({
}); });
} else { } else {
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id); const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" }); if (!secretApprovalSecrets) throw new NotFoundError({ message: "No secrets found" });
const conflicts: Array<{ secretId: string; op: SecretOperations }> = []; const conflicts: Array<{ secretId: string; op: SecretOperations }> = [];
let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create); let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create);
@@ -612,8 +612,8 @@ export const secretApprovalRequestServiceFactory = ({
secretDAL, secretDAL,
inputSecrets: secretCreationCommits.map(({ secretBlindIndex }) => { inputSecrets: secretCreationCommits.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) { if (!secretBlindIndex) {
throw new BadRequestError({ throw new NotFoundError({
message: "Missing secret blind index" message: "Secret blind index not found"
}); });
} }
return { secretBlindIndex }; return { secretBlindIndex };
@@ -639,8 +639,8 @@ export const secretApprovalRequestServiceFactory = ({
.filter(({ secretBlindIndex, secret }) => secret && secret.secretBlindIndex !== secretBlindIndex) .filter(({ secretBlindIndex, secret }) => secret && secret.secretBlindIndex !== secretBlindIndex)
.map(({ secretBlindIndex }) => { .map(({ secretBlindIndex }) => {
if (!secretBlindIndex) { if (!secretBlindIndex) {
throw new BadRequestError({ throw new NotFoundError({
message: "Missing secret blind index" message: "Secret blind index not found"
}); });
} }
return { secretBlindIndex }; return { secretBlindIndex };
@@ -762,8 +762,8 @@ export const secretApprovalRequestServiceFactory = ({
secretQueueService, secretQueueService,
inputSecrets: secretDeletionCommits.map(({ secretBlindIndex }) => { inputSecrets: secretDeletionCommits.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) { if (!secretBlindIndex) {
throw new BadRequestError({ throw new NotFoundError({
message: "Missing secret blind index" message: "Secret blind index not found"
}); });
} }
return { secretBlindIndex, type: SecretType.Shared }; return { secretBlindIndex, type: SecretType.Shared };
@@ -789,7 +789,7 @@ export const secretApprovalRequestServiceFactory = ({
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]); const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
projectId, projectId,
secretPath: folder.path, secretPath: folder.path,
@@ -805,7 +805,7 @@ export const secretApprovalRequestServiceFactory = ({
const requestedByUser = await userDAL.findOne({ id: actorId }); const requestedByUser = await userDAL.findOne({ id: actorId });
const approverUsers = await userDAL.find({ const approverUsers = await userDAL.find({
$in: { $in: {
id: policy.approvers.map((approver: { userId: string }) => approver.userId) id: policy.approvers.map((approver: { userId: string | null | undefined }) => approver.userId!)
} }
}); });
@@ -860,14 +860,14 @@ export const secretApprovalRequestServiceFactory = ({
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) if (!folder)
throw new BadRequestError({ throw new NotFoundError({
message: "Folder not found for the given environment slug & secret path", message: "Folder not found for the given environment slug & secret path",
name: "GenSecretApproval" name: "GenSecretApproval"
}); });
const folderId = folder.id; const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" }); if (!blindIndexCfg) throw new NotFoundError({ message: "Blind index not found", name: "Update secret" });
const commits: Omit<TSecretApprovalRequestsSecretsInsert, "requestId">[] = []; const commits: Omit<TSecretApprovalRequestsSecretsInsert, "requestId">[] = [];
const commitTagIds: Record<string, string[]> = {}; const commitTagIds: Record<string, string[]> = {};
@@ -961,7 +961,7 @@ export const secretApprovalRequestServiceFactory = ({
secretDAL secretDAL
}); });
const secretsGroupedByBlindIndex = groupBy(secrets, (i) => { const secretsGroupedByBlindIndex = groupBy(secrets, (i) => {
if (!i.secretBlindIndex) throw new BadRequestError({ message: "Missing secret blind index" }); if (!i.secretBlindIndex) throw new NotFoundError({ message: "Secret blind index not found" });
return i.secretBlindIndex; return i.secretBlindIndex;
}); });
const deletedSecretIds = deletedSecrets.map( const deletedSecretIds = deletedSecrets.map(
@@ -972,7 +972,7 @@ export const secretApprovalRequestServiceFactory = ({
...deletedSecrets.map((el) => { ...deletedSecrets.map((el) => {
const secretId = secretsGroupedByBlindIndex[keyName2BlindIndex[el.secretName]][0].id; const secretId = secretsGroupedByBlindIndex[keyName2BlindIndex[el.secretName]][0].id;
if (!latestSecretVersions[secretId].secretBlindIndex) if (!latestSecretVersions[secretId].secretBlindIndex)
throw new BadRequestError({ message: "Failed to find secret blind index" }); throw new NotFoundError({ message: "Secret blind index not found" });
return { return {
op: SecretOperations.Delete as const, op: SecretOperations.Delete as const,
...latestSecretVersions[secretId], ...latestSecretVersions[secretId],
@@ -988,7 +988,7 @@ export const secretApprovalRequestServiceFactory = ({
const tagIds = unique(Object.values(commitTagIds).flat()); const tagIds = unique(Object.values(commitTagIds).flat());
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : []; const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
if (tagIds.length !== tags.length) throw new BadRequestError({ message: "Tag not found" }); if (tagIds.length !== tags.length) throw new NotFoundError({ message: "Tag not found" });
const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => { const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => {
const doc = await secretApprovalRequestDAL.create( const doc = await secretApprovalRequestDAL.create(
@@ -1054,7 +1054,7 @@ export const secretApprovalRequestServiceFactory = ({
const commitsGroupByBlindIndex = groupBy(approvalCommits, (i) => { const commitsGroupByBlindIndex = groupBy(approvalCommits, (i) => {
if (!i.secretBlindIndex) { if (!i.secretBlindIndex) {
throw new BadRequestError({ message: "Missing secret blind index" }); throw new NotFoundError({ message: "Secret blind index not found" });
} }
return i.secretBlindIndex; return i.secretBlindIndex;
}); });
@@ -1132,7 +1132,7 @@ export const secretApprovalRequestServiceFactory = ({
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) if (!folder)
throw new BadRequestError({ throw new NotFoundError({
message: "Folder not found for the given environment slug & secret path", message: "Folder not found for the given environment slug & secret path",
name: "GenSecretApproval" name: "GenSecretApproval"
}); });
@@ -1191,8 +1191,8 @@ export const secretApprovalRequestServiceFactory = ({
})) }))
); );
if (secretsToUpdateStoredInDB.length !== secretsToUpdate.length) if (secretsToUpdateStoredInDB.length !== secretsToUpdate.length)
throw new BadRequestError({ throw new NotFoundError({
message: `Secret not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}` message: `Secret does not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
}); });
// now find any secret that needs to update its name // now find any secret that needs to update its name
@@ -1207,8 +1207,8 @@ export const secretApprovalRequestServiceFactory = ({
})) }))
); );
if (secrets.length) if (secrets.length)
throw new BadRequestError({ throw new NotFoundError({
message: `Secret not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}` message: `Secret does not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
}); });
} }
@@ -1267,8 +1267,8 @@ export const secretApprovalRequestServiceFactory = ({
})) }))
); );
if (secretsToDeleteInDB.length !== deletedSecrets.length) if (secretsToDeleteInDB.length !== deletedSecrets.length)
throw new BadRequestError({ throw new NotFoundError({
message: `Secret not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}` message: `Secret does not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
}); });
const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key); const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key);
const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id); const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id);
@@ -1291,7 +1291,7 @@ export const secretApprovalRequestServiceFactory = ({
const tagIds = unique(Object.values(commitTagIds).flat()); const tagIds = unique(Object.values(commitTagIds).flat());
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : []; const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
if (tagIds.length !== tags.length) throw new BadRequestError({ message: "Tag not found" }); if (tagIds.length !== tags.length) throw new NotFoundError({ message: "Tag not found" });
const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => { const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => {
const doc = await secretApprovalRequestDAL.create( const doc = await secretApprovalRequestDAL.create(

View File

@@ -4,7 +4,7 @@ import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approv
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { groupBy, unique } from "@app/lib/fn"; import { groupBy, unique } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -295,7 +295,7 @@ export const secretReplicationServiceFactory = ({
const [destinationFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [ const [destinationFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [
destinationSecretImport.folderId destinationSecretImport.folderId
]); ]);
if (!destinationFolder) throw new BadRequestError({ message: "Imported folder not found" }); if (!destinationFolder) throw new NotFoundError({ message: "Imported folder not found" });
let destinationReplicationFolder = await folderDAL.findOne({ let destinationReplicationFolder = await folderDAL.findOne({
parentId: destinationFolder.id, parentId: destinationFolder.id,
@@ -506,7 +506,7 @@ export const secretReplicationServiceFactory = ({
return; return;
} }
if (!botKey) throw new BadRequestError({ message: "Bot not found" }); if (!botKey) throw new NotFoundError({ message: "Project bot not found" });
// these are the secrets to be added in replicated folders // these are the secrets to be added in replicated folders
const sourceLocalSecrets = await secretDAL.find({ folderId: folder.id, type: SecretType.Shared }); const sourceLocalSecrets = await secretDAL.find({ folderId: folder.id, type: SecretType.Shared });
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id }); const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
@@ -545,7 +545,7 @@ export const secretReplicationServiceFactory = ({
const [destinationFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [ const [destinationFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [
destinationSecretImport.folderId destinationSecretImport.folderId
]); ]);
if (!destinationFolder) throw new BadRequestError({ message: "Imported folder not found" }); if (!destinationFolder) throw new NotFoundError({ message: "Imported folder not found" });
let destinationReplicationFolder = await folderDAL.findOne({ let destinationReplicationFolder = await folderDAL.findOne({
parentId: destinationFolder.id, parentId: destinationFolder.id,

View File

@@ -13,7 +13,7 @@ import {
infisicalSymmetricEncypt infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption"; } from "@app/lib/crypto/encryption";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
import { BadRequestError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
@@ -332,7 +332,7 @@ export const secretRotationQueueFactory = ({
); );
}); });
} else { } else {
if (!botKey) throw new BadRequestError({ message: "Bot not found" }); if (!botKey) throw new NotFoundError({ message: "Project bot not found" });
const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({ const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({
secretId, secretId,
value: encryptSymmetric128BitHexKeyUTF8( value: encryptSymmetric128BitHexKeyUTF8(
@@ -372,7 +372,7 @@ export const secretRotationQueueFactory = ({
); );
await secretVersionDAL.insertMany( await secretVersionDAL.insertMany(
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => { updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => {
if (!el.secretBlindIndex) throw new BadRequestError({ message: "Missing blind index" }); if (!el.secretBlindIndex) throw new NotFoundError({ message: "Secret blind index not found" });
return { return {
...el, ...el,
secretId: id, secretId: id,

View File

@@ -3,7 +3,7 @@ import Ajv from "ajv";
import { ProjectVersion } from "@app/db/schemas"; import { ProjectVersion } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
@@ -94,7 +94,7 @@ export const secretRotationServiceFactory = ({
); );
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Secret path not found" }); if (!folder) throw new NotFoundError({ message: "Secret path not found" });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
@@ -108,14 +108,14 @@ export const secretRotationServiceFactory = ({
$in: { id: Object.values(outputs) } $in: { id: Object.values(outputs) }
}); });
if (selectedSecrets.length !== Object.values(outputs).length) if (selectedSecrets.length !== Object.values(outputs).length)
throw new BadRequestError({ message: "Secrets not found" }); throw new NotFoundError({ message: "Secrets not found" });
} else { } else {
const selectedSecrets = await secretDAL.find({ const selectedSecrets = await secretDAL.find({
folderId: folder.id, folderId: folder.id,
$in: { id: Object.values(outputs) } $in: { id: Object.values(outputs) }
}); });
if (selectedSecrets.length !== Object.values(outputs).length) if (selectedSecrets.length !== Object.values(outputs).length)
throw new BadRequestError({ message: "Secrets not found" }); throw new NotFoundError({ message: "Secrets not found" });
} }
const plan = await licenseService.getPlan(project.orgId); const plan = await licenseService.getPlan(project.orgId);
@@ -125,7 +125,7 @@ export const secretRotationServiceFactory = ({
}); });
const selectedTemplate = rotationTemplates.find(({ name }) => name === provider); const selectedTemplate = rotationTemplates.find(({ name }) => name === provider);
if (!selectedTemplate) throw new BadRequestError({ message: "Provider not found" }); if (!selectedTemplate) throw new NotFoundError({ message: "Provider not found" });
const formattedInputs: Record<string, unknown> = {}; const formattedInputs: Record<string, unknown> = {};
Object.entries(inputs).forEach(([key, value]) => { Object.entries(inputs).forEach(([key, value]) => {
const { type } = selectedTemplate.template.inputs.properties[key]; const { type } = selectedTemplate.template.inputs.properties[key];
@@ -198,7 +198,7 @@ export const secretRotationServiceFactory = ({
return docs; return docs;
} }
if (!botKey) throw new BadRequestError({ message: "bot not found" }); if (!botKey) throw new NotFoundError({ message: "Project bot not found" });
const docs = await secretRotationDAL.find({ projectId }); const docs = await secretRotationDAL.find({ projectId });
return docs.map((el) => ({ return docs.map((el) => ({
...el, ...el,
@@ -220,7 +220,7 @@ export const secretRotationServiceFactory = ({
const restartById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TRestartDTO) => { const restartById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TRestartDTO) => {
const doc = await secretRotationDAL.findById(rotationId); const doc = await secretRotationDAL.findById(rotationId);
if (!doc) throw new BadRequestError({ message: "Rotation not found" }); if (!doc) throw new NotFoundError({ message: "Rotation not found" });
const project = await projectDAL.findById(doc.projectId); const project = await projectDAL.findById(doc.projectId);
const plan = await licenseService.getPlan(project.orgId); const plan = await licenseService.getPlan(project.orgId);
@@ -244,7 +244,7 @@ export const secretRotationServiceFactory = ({
const deleteById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TDeleteDTO) => { const deleteById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TDeleteDTO) => {
const doc = await secretRotationDAL.findById(rotationId); const doc = await secretRotationDAL.findById(rotationId);
if (!doc) throw new BadRequestError({ message: "Rotation not found" }); if (!doc) throw new NotFoundError({ message: "Rotation not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,

View File

@@ -7,7 +7,7 @@ import { ProbotOctokit } from "probot";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { TGitAppDALFactory } from "./git-app-dal"; import { TGitAppDALFactory } from "./git-app-dal";
import { TGitAppInstallSessionDALFactory } from "./git-app-install-session-dal"; import { TGitAppInstallSessionDALFactory } from "./git-app-install-session-dal";
@@ -63,7 +63,7 @@ export const secretScanningServiceFactory = ({
actorOrgId actorOrgId
}: TLinkInstallSessionDTO) => { }: TLinkInstallSessionDTO) => {
const session = await gitAppInstallSessionDAL.findOne({ sessionId }); const session = await gitAppInstallSessionDAL.findOne({ sessionId });
if (!session) throw new UnauthorizedError({ message: "Session not found" }); if (!session) throw new NotFoundError({ message: "Session was not found" });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,

View File

@@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, InternalServerError } from "@app/lib/errors"; import { InternalServerError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@@ -99,7 +99,7 @@ export const secretSnapshotServiceFactory = ({
); );
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
return snapshotDAL.countOfSnapshotsByFolderId(folder.id); return snapshotDAL.countOfSnapshotsByFolderId(folder.id);
}; };
@@ -131,7 +131,7 @@ export const secretSnapshotServiceFactory = ({
); );
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
const snapshots = await snapshotDAL.find({ folderId: folder.id }, { limit, offset, sort: [["createdAt", "desc"]] }); const snapshots = await snapshotDAL.find({ folderId: folder.id }, { limit, offset, sort: [["createdAt", "desc"]] });
return snapshots; return snapshots;
@@ -139,7 +139,7 @@ export const secretSnapshotServiceFactory = ({
const getSnapshotData = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetSnapshotDataDTO) => { const getSnapshotData = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetSnapshotDataDTO) => {
const snapshot = await snapshotDAL.findById(id); const snapshot = await snapshotDAL.findById(id);
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" }); if (!snapshot) throw new NotFoundError({ message: "Snapshot not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -173,7 +173,7 @@ export const secretSnapshotServiceFactory = ({
} else { } else {
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id); const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id);
const { botKey } = await projectBotService.getBotKey(snapshot.projectId); const { botKey } = await projectBotService.getBotKey(snapshot.projectId);
if (!botKey) throw new BadRequestError({ message: "bot not found" }); if (!botKey) throw new NotFoundError({ message: "Project bot not found" });
snapshotDetails = { snapshotDetails = {
...encryptedSnapshotDetails, ...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
@@ -225,7 +225,7 @@ export const secretSnapshotServiceFactory = ({
try { try {
if (!licenseService.isValidLicense) throw new InternalServerError({ message: "Invalid license" }); if (!licenseService.isValidLicense) throw new InternalServerError({ message: "Invalid license" });
const folder = await folderDAL.findById(folderId); const folder = await folderDAL.findById(folderId);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
const shouldUseSecretV2Bridge = folder.projectVersion === 3; const shouldUseSecretV2Bridge = folder.projectVersion === 3;
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
@@ -309,7 +309,7 @@ export const secretSnapshotServiceFactory = ({
actorOrgId actorOrgId
}: TRollbackSnapshotDTO) => { }: TRollbackSnapshotDTO) => {
const snapshot = await snapshotDAL.findById(snapshotId); const snapshot = await snapshotDAL.findById(snapshotId);
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" }); if (!snapshot) throw new NotFoundError({ message: "Snapshot not found" });
const shouldUseBridge = snapshot.projectVersion === 3; const shouldUseBridge = snapshot.projectVersion === 3;
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(

View File

@@ -16,6 +16,9 @@ export const KeyStorePrefixes = {
WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-", WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-",
WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-", WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-",
WaitUntilReadyProjectEnvironmentOperation: (projectId: string) =>
`wait-until-ready-project-environments-operation-${projectId}`,
ProjectEnvironmentLock: (projectId: string) => `project-environment-lock-${projectId}` as const,
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) => SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const, `sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) => SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>

View File

@@ -24,6 +24,9 @@ export const GROUPS = {
id: "The id of the group to add the user to.", id: "The id of the group to add the user to.",
username: "The username of the user to add to the group." username: "The username of the user to add to the group."
}, },
GET_BY_ID: {
id: "The id of the group to fetch"
},
DELETE_USER: { DELETE_USER: {
id: "The id of the group to remove the user from.", id: "The id of the group to remove the user from.",
username: "The username of the user to remove from the group." username: "The username of the user to remove from the group."
@@ -357,7 +360,11 @@ export const ORGANIZATIONS = {
organizationId: "The ID of the organization to update the membership for.", organizationId: "The ID of the organization to update the membership for.",
membershipId: "The ID of the membership to update.", membershipId: "The ID of the membership to update.",
role: "The new role of the membership.", role: "The new role of the membership.",
isActive: "The active status of the membership" isActive: "The active status of the membership",
metadata: {
key: "The key for user metadata tag.",
value: "The value for user metadata tag."
}
}, },
DELETE_USER_MEMBERSHIP: { DELETE_USER_MEMBERSHIP: {
organizationId: "The ID of the organization to delete the membership from.", organizationId: "The ID of the organization to delete the membership from.",

View File

@@ -23,8 +23,19 @@ export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
/** /**
* Extracts and formats permissions from a CASL Ability object or a raw permission set. * Extracts and formats permissions from a CASL Ability object or a raw permission set.
*/ */
const extractPermissions = (ability: MongoAbility) => const extractPermissions = (ability: MongoAbility) => {
ability.rules.map((permission) => `${permission.action as string}_${permission.subject as string}`); const permissions: string[] = [];
ability.rules.forEach((permission) => {
if (typeof permission.action === "string") {
permissions.push(`${permission.action}_${permission.subject as string}`);
} else {
permission.action.forEach((permissionAction) => {
permissions.push(`${permissionAction}_${permission.subject as string}`);
});
}
});
return permissions;
};
/** /**
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set. * Compares two sets of permissions to determine if the first set is at least as privileged as the second set.

View File

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

View File

@@ -40,9 +40,9 @@ export class ForbiddenRequestError extends Error {
error: unknown; error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) { constructor({ name, error, message }: { message?: string; name?: string; error?: unknown } = {}) {
super(message ?? "You are not allowed to access this resource"); super(message ?? "You are not allowed to access this resource");
this.name = name || "ForbideenError"; this.name = name || "ForbiddenError";
this.error = error; this.error = error;
} }
} }

View File

@@ -52,3 +52,21 @@ export const unique = <T, K extends string | number | symbol>(array: readonly T[
); );
return Object.values(valueMap); return Object.values(valueMap);
}; };
/**
* Convert an array to a dictionary by mapping each item
* into a dictionary key & value
*/
export const objectify = <T, Key extends string | number | symbol, Value = T>(
array: readonly T[],
getKey: (item: T) => Key,
getValue: (item: T) => Value = (item) => item as unknown as Value
): Record<Key, Value> => {
return array.reduce(
(acc, item) => {
acc[getKey(item)] = getValue(item);
return acc;
},
{} as Record<Key, Value>
);
};

View File

@@ -9,3 +9,8 @@ export const removeTrailingSlash = (str: string) => {
return str.endsWith("/") ? str.slice(0, -1) : str; return str.endsWith("/") ? str.slice(0, -1) : str;
}; };
export const prefixWithSlash = (str: string) => {
if (str.startsWith("/")) return str;
return `/${str}`;
};

View File

@@ -1,6 +1,6 @@
import net from "node:net"; import net from "node:net";
import { UnauthorizedError } from "../errors"; import { ForbiddenRequestError } from "../errors";
export enum IPType { export enum IPType {
IPV4 = "ipv4", IPV4 = "ipv4",
@@ -126,7 +126,7 @@ export const checkIPAgainstBlocklist = ({ ipAddress, trustedIps }: { ipAddress:
const check = blockList.check(ipAddress, type); const check = blockList.check(ipAddress, type);
if (!check) if (!check)
throw new UnauthorizedError({ throw new ForbiddenRequestError({
message: "Failed to authenticate" message: "You are not allowed to access this resource from the current IP address"
}); });
}; };

View File

@@ -0,0 +1,89 @@
import { Knex } from "knex";
import { UnauthorizedError } from "../errors";
type TKnexDynamicPrimitiveOperator = {
operator: "eq" | "ne" | "startsWith" | "endsWith";
value: string;
field: string;
};
type TKnexDynamicInOperator = {
operator: "in";
value: string[] | number[];
field: string;
};
type TKnexNonGroupOperator = TKnexDynamicInOperator | TKnexDynamicPrimitiveOperator;
type TKnexGroupOperator = {
operator: "and" | "or" | "not";
value: (TKnexNonGroupOperator | TKnexGroupOperator)[];
};
// akhilmhdh: This is still in pending state and not yet ready. If you want to use it ping me.
// used when you need to write a complex query with the orm
// use it when you need complex or and and condition - most of the time not needed
// majorly used with casl permission to filter data based on permission
export type TKnexDynamicOperator = TKnexGroupOperator | TKnexNonGroupOperator;
export const buildDynamicKnexQuery = (dynamicQuery: TKnexDynamicOperator, rootQueryBuild: Knex.QueryBuilder) => {
const stack = [{ filterAst: dynamicQuery, queryBuilder: rootQueryBuild }];
while (stack.length) {
const { filterAst, queryBuilder } = stack.pop()!;
switch (filterAst.operator) {
case "eq": {
void queryBuilder.where(filterAst.field, "=", filterAst.value);
break;
}
case "ne": {
void queryBuilder.whereNot(filterAst.field, filterAst.value);
break;
}
case "startsWith": {
void queryBuilder.whereILike(filterAst.field, `${filterAst.value}%`);
break;
}
case "endsWith": {
void queryBuilder.whereILike(filterAst.field, `%${filterAst.value}`);
break;
}
case "and": {
void queryBuilder.andWhere((subQueryBuilder) => {
filterAst.value.forEach((el) => {
stack.push({
queryBuilder: subQueryBuilder,
filterAst: el
});
});
});
break;
}
case "or": {
void queryBuilder.orWhere((subQueryBuilder) => {
filterAst.value.forEach((el) => {
stack.push({
queryBuilder: subQueryBuilder,
filterAst: el
});
});
});
break;
}
case "not": {
void queryBuilder.whereNot((subQueryBuilder) => {
filterAst.value.forEach((el) => {
stack.push({
queryBuilder: subQueryBuilder,
filterAst: el
});
});
});
break;
}
default:
throw new UnauthorizedError({ message: `Invalid knex dynamic operator: ${filterAst.operator}` });
}
}
};

View File

@@ -20,6 +20,7 @@ import { TQueueServiceFactory } from "@app/queue";
import { TSmtpService } from "@app/services/smtp/smtp-service"; import { TSmtpService } from "@app/services/smtp/smtp-service";
import { globalRateLimiterCfg } from "./config/rateLimiter"; import { globalRateLimiterCfg } from "./config/rateLimiter";
import { addErrorsToResponseSchemas } from "./plugins/add-errors-to-response-schemas";
import { fastifyErrHandler } from "./plugins/error-handler"; import { fastifyErrHandler } from "./plugins/error-handler";
import { registerExternalNextjs } from "./plugins/external-nextjs"; import { registerExternalNextjs } from "./plugins/external-nextjs";
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod"; import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
@@ -75,6 +76,8 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
credentials: true, credentials: true,
origin: appCfg.SITE_URL || true origin: appCfg.SITE_URL || true
}); });
await server.register(addErrorsToResponseSchemas);
// pull ip based on various proxy headers // pull ip based on various proxy headers
await server.register(fastifyIp); await server.register(fastifyIp);

View File

@@ -0,0 +1,15 @@
/* eslint-disable no-param-reassign */
import fp from "fastify-plugin";
import { DefaultResponseErrorsSchema } from "../routes/sanitizedSchemas";
export const addErrorsToResponseSchemas = fp(async (server) => {
server.addHook("onRoute", (routeOptions) => {
if (routeOptions.schema && routeOptions.schema.response) {
routeOptions.schema.response = {
...DefaultResponseErrorsSchema,
...routeOptions.schema.response
};
}
});
});

View File

@@ -70,7 +70,7 @@ export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => {
metadata: {} metadata: {}
}; };
} else { } else {
throw new BadRequestError({ message: "Missing logic for other actor" }); throw new BadRequestError({ message: "Invalid actor type provided" });
} }
req.auditLogInfo = payload; req.auditLogInfo = payload;
}); });

View File

@@ -5,7 +5,7 @@ import jwt, { JwtPayload } from "jsonwebtoken";
import { TServiceTokens, TUsers } from "@app/db/schemas"; import { TServiceTokens, TUsers } from "@app/db/schemas";
import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types"; import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types";
@@ -167,7 +167,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
break; break;
} }
default: default:
throw new UnauthorizedError({ name: "Unknown token strategy" }); throw new BadRequestError({ message: "Invalid token strategy provided" });
} }
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify"; import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify";
import { UnauthorizedError } from "@app/lib/errors"; import { ForbiddenRequestError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
export const verifySuperAdmin = <T extends FastifyRequest>( export const verifySuperAdmin = <T extends FastifyRequest>(
@@ -9,9 +9,8 @@ export const verifySuperAdmin = <T extends FastifyRequest>(
done: HookHandlerDoneFunction done: HookHandlerDoneFunction
) => { ) => {
if (req.auth.actor !== ActorType.USER || !req.auth.user.superAdmin) if (req.auth.actor !== ActorType.USER || !req.auth.user.superAdmin)
throw new UnauthorizedError({ throw new ForbiddenRequestError({
name: "Unauthorized access", message: "Requires elevated super admin privileges"
message: "Requires superadmin access"
}); });
done(); done();
}; };

View File

@@ -1,6 +1,6 @@
import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify"; import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify";
import { UnauthorizedError } from "@app/lib/errors"; import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
interface TAuthOptions { interface TAuthOptions {
@@ -11,11 +11,11 @@ export const verifyAuth =
<T extends FastifyRequest>(authStrategies: AuthMode[], options: TAuthOptions = { requireOrg: true }) => <T extends FastifyRequest>(authStrategies: AuthMode[], options: TAuthOptions = { requireOrg: true }) =>
(req: T, _res: FastifyReply, done: HookHandlerDoneFunction) => { (req: T, _res: FastifyReply, done: HookHandlerDoneFunction) => {
if (!Array.isArray(authStrategies)) throw new Error("Auth strategy must be array"); if (!Array.isArray(authStrategies)) throw new Error("Auth strategy must be array");
if (!req.auth) throw new UnauthorizedError({ name: "Unauthorized access", message: "Token missing" }); if (!req.auth) throw new UnauthorizedError({ message: "Token missing" });
const isAccessAllowed = authStrategies.some((strategy) => strategy === req.auth.authMode); const isAccessAllowed = authStrategies.some((strategy) => strategy === req.auth.authMode);
if (!isAccessAllowed) { if (!isAccessAllowed) {
throw new UnauthorizedError({ name: `${req.url} Unauthorized Access` }); throw new ForbiddenRequestError({ name: `Forbidden access to ${req.url}` });
} }
// New optional option. There are some routes which do not require an organization ID to be present on the request. // New optional option. There are some routes which do not require an organization ID to be present on the request.

View File

@@ -6,6 +6,7 @@ import { ZodError } from "zod";
import { import {
BadRequestError, BadRequestError,
DatabaseError, DatabaseError,
ForbiddenRequestError,
InternalServerError, InternalServerError,
NotFoundError, NotFoundError,
ScimRequestError, ScimRequestError,
@@ -18,25 +19,50 @@ enum JWTErrors {
InvalidAlgorithm = "invalid algorithm" InvalidAlgorithm = "invalid algorithm"
} }
enum HttpStatusCodes {
BadRequest = 400,
NotFound = 404,
Unauthorized = 401,
Forbidden = 403,
// eslint-disable-next-line @typescript-eslint/no-shadow
InternalServerError = 500
}
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => { export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
server.setErrorHandler((error, req, res) => { server.setErrorHandler((error, req, res) => {
req.log.error(error); req.log.error(error);
if (error instanceof BadRequestError) { if (error instanceof BadRequestError) {
void res.status(400).send({ statusCode: 400, message: error.message, error: error.name }); void res
.status(HttpStatusCodes.BadRequest)
.send({ statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name });
} else if (error instanceof NotFoundError) { } else if (error instanceof NotFoundError) {
void res.status(404).send({ statusCode: 404, message: error.message, error: error.name }); void res
.status(HttpStatusCodes.NotFound)
.send({ statusCode: HttpStatusCodes.NotFound, message: error.message, error: error.name });
} else if (error instanceof UnauthorizedError) { } else if (error instanceof UnauthorizedError) {
void res.status(403).send({ statusCode: 403, message: error.message, error: error.name }); void res
.status(HttpStatusCodes.Unauthorized)
.send({ statusCode: HttpStatusCodes.Unauthorized, message: error.message, error: error.name });
} else if (error instanceof DatabaseError || error instanceof InternalServerError) { } else if (error instanceof DatabaseError || error instanceof InternalServerError) {
void res.status(500).send({ statusCode: 500, message: "Something went wrong", error: error.name }); void res
.status(HttpStatusCodes.InternalServerError)
.send({ statusCode: HttpStatusCodes.InternalServerError, message: "Something went wrong", error: error.name });
} else if (error instanceof ZodError) { } else if (error instanceof ZodError) {
void res.status(403).send({ statusCode: 403, error: "ValidationFailure", message: error.issues }); void res
.status(HttpStatusCodes.Unauthorized)
.send({ statusCode: HttpStatusCodes.Unauthorized, error: "ValidationFailure", message: error.issues });
} else if (error instanceof ForbiddenError) { } else if (error instanceof ForbiddenError) {
void res.status(401).send({ void res.status(HttpStatusCodes.Forbidden).send({
statusCode: 401, statusCode: HttpStatusCodes.Forbidden,
error: "PermissionDenied", error: "PermissionDenied",
message: `You are not allowed to ${error.action} on ${error.subjectType}` message: `You are not allowed to ${error.action} on ${error.subjectType}`
}); });
} else if (error instanceof ForbiddenRequestError) {
void res.status(HttpStatusCodes.Forbidden).send({
statusCode: HttpStatusCodes.Forbidden,
message: error.message,
error: error.name
});
} else if (error instanceof ScimRequestError) { } else if (error instanceof ScimRequestError) {
void res.status(error.status).send({ void res.status(error.status).send({
schemas: error.schemas, schemas: error.schemas,
@@ -59,8 +85,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
return error.message; return error.message;
})(); })();
void res.status(401).send({ void res.status(HttpStatusCodes.Forbidden).send({
statusCode: 401, statusCode: HttpStatusCodes.Forbidden,
error: "TokenError", error: "TokenError",
message message
}); });

View File

@@ -1,5 +1,5 @@
import { CronJob } from "cron"; import { CronJob } from "cron";
import { Redis } from "ioredis"; // import { Redis } from "ioredis";
import { Knex } from "knex"; import { Knex } from "knex";
import { z } from "zod"; import { z } from "zod";
@@ -74,7 +74,6 @@ import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { TKeyStoreFactory } from "@app/keystore/keystore"; import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { TQueueServiceFactory } from "@app/queue"; import { TQueueServiceFactory } from "@app/queue";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue"; import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
@@ -97,10 +96,12 @@ import { certificateAuthorityServiceFactory } from "@app/services/certificate-au
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal"; import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { identityDALFactory } from "@app/services/identity/identity-dal"; import { identityDALFactory } from "@app/services/identity/identity-dal";
import { identityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal"; import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
import { identityServiceFactory } from "@app/services/identity/identity-service"; import { identityServiceFactory } from "@app/services/identity/identity-service";
import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal"; import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal";
@@ -265,6 +266,7 @@ export const registerRoutes = async (
const serviceTokenDAL = serviceTokenDALFactory(db); const serviceTokenDAL = serviceTokenDALFactory(db);
const identityDAL = identityDALFactory(db); const identityDAL = identityDALFactory(db);
const identityMetadataDAL = identityMetadataDALFactory(db);
const identityAccessTokenDAL = identityAccessTokenDALFactory(db); const identityAccessTokenDAL = identityAccessTokenDALFactory(db);
const identityOrgMembershipDAL = identityOrgDALFactory(db); const identityOrgMembershipDAL = identityOrgDALFactory(db);
const identityProjectDAL = identityProjectDALFactory(db); const identityProjectDAL = identityProjectDALFactory(db);
@@ -380,11 +382,13 @@ export const registerRoutes = async (
secretApprovalPolicyApproverDAL: sapApproverDAL, secretApprovalPolicyApproverDAL: sapApproverDAL,
permissionService, permissionService,
secretApprovalPolicyDAL, secretApprovalPolicyDAL,
licenseService licenseService,
userDAL
}); });
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL }); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
const samlService = samlConfigServiceFactory({ const samlService = samlConfigServiceFactory({
identityMetadataDAL,
permissionService, permissionService,
orgBotDAL, orgBotDAL,
orgDAL, orgDAL,
@@ -488,6 +492,7 @@ export const registerRoutes = async (
}); });
const orgService = orgServiceFactory({ const orgService = orgServiceFactory({
userAliasDAL, userAliasDAL,
identityMetadataDAL,
licenseService, licenseService,
samlConfigDAL, samlConfigDAL,
orgRoleDAL, orgRoleDAL,
@@ -506,7 +511,8 @@ export const registerRoutes = async (
smtpService, smtpService,
userDAL, userDAL,
groupDAL, groupDAL,
orgBotDAL orgBotDAL,
oidcConfigDAL
}); });
const signupService = authSignupServiceFactory({ const signupService = authSignupServiceFactory({
tokenService, tokenService,
@@ -742,6 +748,7 @@ export const registerRoutes = async (
const projectEnvService = projectEnvServiceFactory({ const projectEnvService = projectEnvServiceFactory({
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
keyStore,
licenseService, licenseService,
projectDAL, projectDAL,
folderDAL folderDAL
@@ -917,16 +924,19 @@ export const registerRoutes = async (
const secretSharingService = secretSharingServiceFactory({ const secretSharingService = secretSharingServiceFactory({
permissionService, permissionService,
secretSharingDAL, secretSharingDAL,
orgDAL orgDAL,
kmsService
}); });
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
groupDAL,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
projectMembershipDAL, projectMembershipDAL,
projectDAL projectDAL,
userDAL
}); });
const accessApprovalRequestService = accessApprovalRequestServiceFactory({ const accessApprovalRequestService = accessApprovalRequestServiceFactory({
@@ -942,7 +952,8 @@ export const registerRoutes = async (
smtpService, smtpService,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
projectSlackConfigDAL, projectSlackConfigDAL,
kmsService kmsService,
groupDAL
}); });
const secretReplicationService = secretReplicationServiceFactory({ const secretReplicationService = secretReplicationServiceFactory({
@@ -1023,7 +1034,8 @@ export const registerRoutes = async (
identityDAL, identityDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityProjectDAL, identityProjectDAL,
licenseService licenseService,
identityMetadataDAL
}); });
const identityAccessTokenService = identityAccessTokenServiceFactory({ const identityAccessTokenService = identityAccessTokenServiceFactory({
@@ -1182,6 +1194,14 @@ export const registerRoutes = async (
workflowIntegrationDAL workflowIntegrationDAL
}); });
const migrationService = externalMigrationServiceFactory({
projectService,
orgService,
projectEnvService,
permissionService,
secretService
});
await superAdminService.initServerCfg(); await superAdminService.initServerCfg();
// //
// setup the communication with license key server // setup the communication with license key server
@@ -1265,7 +1285,8 @@ export const registerRoutes = async (
externalKms: externalKmsService, externalKms: externalKmsService,
orgAdmin: orgAdminService, orgAdmin: orgAdminService,
slack: slackService, slack: slackService,
workflowIntegration: workflowIntegrationService workflowIntegration: workflowIntegrationService,
migration: migrationService
}); });
const cronJobs: CronJob[] = []; const cronJobs: CronJob[] = [];
@@ -1304,33 +1325,33 @@ export const registerRoutes = async (
}) })
} }
}, },
handler: async (request, reply) => { handler: async () => {
const cfg = getConfig(); const cfg = getConfig();
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
try { // try {
await db.raw("SELECT NOW()"); // await db.raw("SELECT NOW()");
} catch (err) { // } catch (err) {
logger.error("Health check: database connection failed", err); // logger.error("Health check: database connection failed", err);
return reply.code(503).send({ // return reply.code(503).send({
date: new Date(), // date: new Date(),
message: "Service unavailable" // message: "Service unavailable"
}); // });
} // }
if (cfg.isRedisConfigured) { // if (cfg.isRedisConfigured) {
const redis = new Redis(cfg.REDIS_URL); // const redis = new Redis(cfg.REDIS_URL);
try { // try {
await redis.ping(); // await redis.ping();
redis.disconnect(); // redis.disconnect();
} catch (err) { // } catch (err) {
logger.error("Health check: redis connection failed", err); // logger.error("Health check: redis connection failed", err);
return reply.code(503).send({ // return reply.code(503).send({
date: new Date(), // date: new Date(),
message: "Service unavailable" // message: "Service unavailable"
}); // });
} // }
} // }
return { return {
date: new Date(), date: new Date(),

View File

@@ -27,6 +27,34 @@ export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({
updatedAt: true updatedAt: true
}); });
export const DefaultResponseErrorsSchema = {
400: z.object({
statusCode: z.literal(400),
message: z.string(),
error: z.string()
}),
404: z.object({
statusCode: z.literal(404),
message: z.string(),
error: z.string()
}),
401: z.object({
statusCode: z.literal(401),
message: z.any(),
error: z.string()
}),
403: z.object({
statusCode: z.literal(403),
message: z.string(),
error: z.string()
}),
500: z.object({
statusCode: z.literal(500),
message: z.string(),
error: z.string()
})
};
export const sapPubSchema = SecretApprovalPoliciesSchema.merge( export const sapPubSchema = SecretApprovalPoliciesSchema.merge(
z.object({ z.object({
environment: z.object({ environment: z.object({

View File

@@ -2,7 +2,7 @@ import { z } from "zod";
import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -227,8 +227,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
handler: async (req, res) => { handler: async (req, res) => {
const appCfg = getConfig(); const appCfg = getConfig();
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
if (serverCfg.initialized) if (serverCfg.initialized) throw new BadRequestError({ message: "Admin account has already been set up" });
throw new UnauthorizedError({ name: "Admin sign up", message: "Admin has been created" });
const { user, token, organization } = await server.services.superAdmin.adminSignUp({ const { user, token, organization } = await server.services.superAdmin.adminSignUp({
...req.body, ...req.body,
ip: req.realIp, ip: req.realIp,

View File

@@ -2,7 +2,7 @@ import jwt from "jsonwebtoken";
import { z } from "zod"; import { z } from "zod";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; import { AuthMode, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
@@ -71,23 +71,34 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
const refreshToken = req.cookies.jid; const refreshToken = req.cookies.jid;
const appCfg = getConfig(); const appCfg = getConfig();
if (!refreshToken) if (!refreshToken)
throw new BadRequestError({ throw new NotFoundError({
name: "Auth token route", name: "AuthTokenNotFound",
message: "Failed to find refresh token" message: "Failed to find refresh token"
}); });
const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload; const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload;
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN)
throw new UnauthorizedError({ message: "Invalid token", name: "Auth token route" }); throw new UnauthorizedError({
message: "The token provided is not a refresh token",
name: "InvalidToken"
});
const tokenVersion = await server.services.authToken.getUserTokenSessionById( const tokenVersion = await server.services.authToken.getUserTokenSessionById(
decodedToken.tokenVersionId, decodedToken.tokenVersionId,
decodedToken.userId decodedToken.userId
); );
if (!tokenVersion) throw new UnauthorizedError({ message: "Invalid token", name: "Auth token route" }); if (!tokenVersion)
throw new UnauthorizedError({
message: "Valid token version not found",
name: "InvalidToken"
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) {
throw new UnauthorizedError({ message: "Invalid token", name: "Auth token route" }); throw new UnauthorizedError({
message: "Token version mismatch",
name: "InvalidToken"
});
}
const token = jwt.sign( const token = jwt.sign(
{ {

View File

@@ -1,7 +1,9 @@
import { ForbiddenError, subject } from "@casl/ability";
import { z } from "zod"; import { z } from "zod";
import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas"; import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { DASHBOARD } from "@app/lib/api-docs"; import { DASHBOARD } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
@@ -15,6 +17,20 @@ import { AuthMode } from "@app/services/auth/auth-type";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
// handle querystring boolean values
const booleanSchema = z
.union([z.boolean(), z.string().trim()])
.transform((value) => {
if (typeof value === "string") {
// ie if not empty, 0 or false, return true
return Boolean(value) && Number(value) !== 0 && value.toLowerCase() !== "false";
}
return value;
})
.optional()
.default(true);
export const registerDashboardRouter = async (server: FastifyZodProvider) => { export const registerDashboardRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
@@ -55,21 +71,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.orderDirection) .describe(DASHBOARD.SECRET_OVERVIEW_LIST.orderDirection)
.optional(), .optional(),
search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(), search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(),
includeSecrets: z.coerce includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
.boolean() includeFolders: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
.optional() includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
.default(true)
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
includeFolders: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
includeDynamicSecrets: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -173,7 +177,30 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
} }
if (includeDynamicSecrets) { if (!includeDynamicSecrets && !includeSecrets)
return {
folders,
totalFolderCount,
totalCount: totalFolderCount ?? 0
};
const { permission } = await server.services.permission.getProjectPermission(
req.permission.type,
req.permission.id,
projectId,
req.permission.authMethod,
req.permission.orgId
);
const permissiveEnvs = // filter envs user has access to
environments.filter((environment) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
)
);
if (includeDynamicSecrets && permissiveEnvs.length) {
// this is the unique count, ie duplicate secrets across envs only count as 1 // this is the unique count, ie duplicate secrets across envs only count as 1
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({ totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
actor: req.permission.type, actor: req.permission.type,
@@ -182,8 +209,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId, projectId,
search, search,
environmentSlugs: environments, environmentSlugs: permissiveEnvs,
path: secretPath path: secretPath,
isInternal: true
}); });
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
@@ -196,10 +224,11 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
search, search,
orderBy, orderBy,
orderDirection, orderDirection,
environmentSlugs: environments, environmentSlugs: permissiveEnvs,
path: secretPath, path: secretPath,
limit: remainingLimit, limit: remainingLimit,
offset: adjustedOffset offset: adjustedOffset,
isInternal: true
}); });
// get the count of unique dynamic secret names to properly adjust remaining limit // get the count of unique dynamic secret names to properly adjust remaining limit
@@ -212,17 +241,18 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
} }
if (includeSecrets) { if (includeSecrets && permissiveEnvs.length) {
// this is the unique count, ie duplicate secrets across envs only count as 1 // this is the unique count, ie duplicate secrets across envs only count as 1
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({ totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
environments, environments: permissiveEnvs,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId, projectId,
path: secretPath, path: secretPath,
search search,
isInternal: true
}); });
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
@@ -230,7 +260,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
environments, environments: permissiveEnvs,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId, projectId,
path: secretPath, path: secretPath,
@@ -238,10 +268,11 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
orderDirection, orderDirection,
search, search,
limit: remainingLimit, limit: remainingLimit,
offset: adjustedOffset offset: adjustedOffset,
isInternal: true
}); });
for await (const environment of environments) { for await (const environment of permissiveEnvs) {
const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length; const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length;
if (secretCountFromEnv) { if (secretCountFromEnv) {
@@ -325,26 +356,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(), .optional(),
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(), search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(), tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
includeSecrets: z.coerce includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
.boolean() includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
.optional() includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
.default(true) includeImports: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports)
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
includeFolders: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
includeDynamicSecrets: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
includeImports: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -498,56 +513,44 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
} }
if (includeDynamicSecrets) { try {
totalDynamicSecretCount = await server.services.dynamicSecret.getDynamicSecretCount({ if (includeDynamicSecrets) {
actor: req.permission.type, totalDynamicSecretCount = await server.services.dynamicSecret.getDynamicSecretCount({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId,
search,
environmentSlug: environment,
path: secretPath
});
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByEnv({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId, projectId,
search, search,
orderBy,
orderDirection,
environmentSlug: environment, environmentSlug: environment,
path: secretPath, path: secretPath
limit: remainingLimit,
offset: adjustedOffset
}); });
remainingLimit -= dynamicSecrets.length; if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
adjustedOffset = 0; dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByEnv({
} else { actor: req.permission.type,
adjustedOffset = Math.max(0, adjustedOffset - totalDynamicSecretCount); actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId,
search,
orderBy,
orderDirection,
environmentSlug: environment,
path: secretPath,
limit: remainingLimit,
offset: adjustedOffset
});
remainingLimit -= dynamicSecrets.length;
adjustedOffset = 0;
} else {
adjustedOffset = Math.max(0, adjustedOffset - totalDynamicSecretCount);
}
} }
}
if (includeSecrets) { if (includeSecrets) {
totalSecretCount = await server.services.secret.getSecretsCount({ totalSecretCount = await server.services.secret.getSecretsCount({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environment,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
search,
tagSlugs: tags
});
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
const secretsRaw = await server.services.secret.getSecretsRaw({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
@@ -555,44 +558,62 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId, projectId,
path: secretPath, path: secretPath,
orderBy,
orderDirection,
search, search,
limit: remainingLimit,
offset: adjustedOffset,
tagSlugs: tags tagSlugs: tags
}); });
secrets = secretsRaw.secrets; if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
const secretsRaw = await server.services.secret.getSecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environment,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
orderBy,
orderDirection,
search,
limit: remainingLimit,
offset: adjustedOffset,
tagSlugs: tags
});
await server.services.auditLog.createAuditLog({ secrets = secretsRaw.secrets;
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secrets.length
}
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) { await server.services.auditLog.createAuditLog({
await server.services.telemetry.sendPostHogEvents({ projectId,
event: PostHogEventTypes.SecretPulled, ...req.auditLogInfo,
distinctId: getTelemetryDistinctId(req), event: {
properties: { type: EventType.GET_SECRETS,
numberOfSecrets: secrets.length, metadata: {
workspaceId: projectId, environment,
environment, secretPath,
secretPath, numberOfSecrets: secrets.length
channel: getUserAgentType(req.headers["user-agent"]), }
...req.auditLogInfo
} }
}); });
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secrets.length,
workspaceId: projectId,
environment,
secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
}
} }
} }
} catch (error) {
if (!(error instanceof ForbiddenError)) {
throw error;
}
} }
return { return {

View File

@@ -9,6 +9,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { validateAzureAuthField } from "@app/services/identity-azure-auth/identity-azure-auth-validators"; import { validateAzureAuthField } from "@app/services/identity-azure-auth/identity-azure-auth-validators";
import {} from "../sanitizedSchemas";
export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider) => { export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",

View File

@@ -29,7 +29,11 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
name: z.string().trim().describe(IDENTITIES.CREATE.name), name: z.string().trim().describe(IDENTITIES.CREATE.name),
organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId), organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId),
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role) role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role),
metadata: z
.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })
.array()
.optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -93,7 +97,11 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}), }),
body: z.object({ body: z.object({
name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name), name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name),
role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role) role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role),
metadata: z
.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })
.array()
.optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -193,6 +201,14 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
identity: IdentityOrgMembershipsSchema.extend({ identity: IdentityOrgMembershipsSchema.extend({
metadata: z
.object({
key: z.string().trim().min(1),
id: z.string().trim().min(1),
value: z.string().trim().min(1)
})
.array()
.optional(),
customRole: OrgRolesSchema.pick({ customRole: OrgRolesSchema.pick({
id: true, id: true,
name: true, name: true,

View File

@@ -1,3 +1,5 @@
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
import { registerAdminRouter } from "./admin-router"; import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-router"; import { registerAuthRoutes } from "./auth-router";
import { registerProjectBotRouter } from "./bot-router"; import { registerProjectBotRouter } from "./bot-router";
@@ -101,4 +103,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerIdentityRouter, { prefix: "/identities" }); await server.register(registerIdentityRouter, { prefix: "/identities" });
await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" }); await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" });
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" }); await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
}; };

View File

@@ -11,6 +11,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema"; import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema";
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
import {} from "../sanitizedSchemas";
export const registerIntegrationRouter = async (server: FastifyZodProvider) => { export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
@@ -129,9 +131,9 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
.default("/") .default("/")
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(INTEGRATION.UPDATE.secretPath), .describe(INTEGRATION.UPDATE.secretPath),
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment), targetEnvironment: z.string().trim().optional().describe(INTEGRATION.UPDATE.targetEnvironment),
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner), owner: z.string().trim().optional().describe(INTEGRATION.UPDATE.owner),
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment), environment: z.string().trim().optional().describe(INTEGRATION.UPDATE.environment),
metadata: IntegrationMetadataSchema.optional() metadata: IntegrationMetadataSchema.optional()
}), }),
response: { response: {

View File

@@ -11,6 +11,8 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs"; import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { getLastMidnightDateISO } from "@app/lib/fn"; import { getLastMidnightDateISO } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -26,7 +28,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
response: { response: {
200: z.object({ 200: z.object({
organizations: OrganizationsSchema.array() organizations: OrganizationsSchema.extend({
orgAuthMethod: z.string()
}).array()
}) })
} }
}, },
@@ -143,6 +147,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const appCfg = getConfig();
if (appCfg.isCloud) {
throw new BadRequestError({ message: "Infisical cloud audit log is in maintenance mode." });
}
const auditLogs = await server.services.auditLog.listAuditLogs({ const auditLogs = await server.services.auditLog.listAuditLogs({
filter: { filter: {
...req.query, ...req.query,

View File

@@ -3,7 +3,7 @@ import { z } from "zod";
import { SecretFoldersSchema } from "@app/db/schemas"; import { SecretFoldersSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { FOLDERS } from "@app/lib/api-docs"; import { FOLDERS } from "@app/lib/api-docs";
import { removeTrailingSlash } from "@app/lib/fn"; import { prefixWithSlash, removeTrailingSlash } from "@app/lib/fn";
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter"; import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -26,9 +26,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
workspaceId: z.string().trim().describe(FOLDERS.CREATE.workspaceId), workspaceId: z.string().trim().describe(FOLDERS.CREATE.workspaceId),
environment: z.string().trim().describe(FOLDERS.CREATE.environment), environment: z.string().trim().describe(FOLDERS.CREATE.environment),
name: z.string().trim().describe(FOLDERS.CREATE.name), name: z.string().trim().describe(FOLDERS.CREATE.name),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.CREATE.path), path: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash)
.transform(removeTrailingSlash)
.describe(FOLDERS.CREATE.path),
// backward compatiability with cli // backward compatiability with cli
directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.CREATE.directory) directory: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash)
.transform(removeTrailingSlash)
.describe(FOLDERS.CREATE.directory)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -86,9 +98,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId), workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId),
environment: z.string().trim().describe(FOLDERS.UPDATE.environment), environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
name: z.string().trim().describe(FOLDERS.UPDATE.name), name: z.string().trim().describe(FOLDERS.UPDATE.name),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.path), path: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash)
.transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.path),
// backward compatiability with cli // backward compatiability with cli
directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.directory) directory: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash)
.transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.directory)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -147,7 +171,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
id: z.string().describe(FOLDERS.UPDATE.folderId), id: z.string().describe(FOLDERS.UPDATE.folderId),
environment: z.string().trim().describe(FOLDERS.UPDATE.environment), environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
name: z.string().trim().describe(FOLDERS.UPDATE.name), name: z.string().trim().describe(FOLDERS.UPDATE.name),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.path) path: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash)
.transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.path)
}) })
.array() .array()
.min(1) .min(1)
@@ -211,9 +241,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
body: z.object({ body: z.object({
workspaceId: z.string().trim().describe(FOLDERS.DELETE.workspaceId), workspaceId: z.string().trim().describe(FOLDERS.DELETE.workspaceId),
environment: z.string().trim().describe(FOLDERS.DELETE.environment), environment: z.string().trim().describe(FOLDERS.DELETE.environment),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.DELETE.path), path: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash)
.transform(removeTrailingSlash)
.describe(FOLDERS.DELETE.path),
// keep this here as cli need directory // keep this here as cli need directory
directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.DELETE.directory) directory: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash)
.transform(removeTrailingSlash)
.describe(FOLDERS.DELETE.directory)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -267,9 +309,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
querystring: z.object({ querystring: z.object({
workspaceId: z.string().trim().describe(FOLDERS.LIST.workspaceId), workspaceId: z.string().trim().describe(FOLDERS.LIST.workspaceId),
environment: z.string().trim().describe(FOLDERS.LIST.environment), environment: z.string().trim().describe(FOLDERS.LIST.environment),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.LIST.path), path: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash)
.transform(removeTrailingSlash)
.describe(FOLDERS.LIST.path),
// backward compatiability with cli // backward compatiability with cli
directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.LIST.directory) directory: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash)
.transform(removeTrailingSlash)
.describe(FOLDERS.LIST.directory)
}), }),
response: { response: {
200: z.object({ 200: z.object({

View File

@@ -55,10 +55,10 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
}, },
schema: { schema: {
params: z.object({ params: z.object({
id: z.string().uuid() id: z.string()
}), }),
body: z.object({ body: z.object({
hashedHex: z.string().min(1), hashedHex: z.string().min(1).optional(),
password: z.string().optional() password: z.string().optional()
}), }),
response: { response: {
@@ -73,7 +73,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
accessType: true accessType: true
}) })
.extend({ .extend({
orgName: z.string().optional() orgName: z.string().optional(),
secretValue: z.string().optional()
}) })
.optional() .optional()
}) })
@@ -99,17 +100,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
}, },
schema: { schema: {
body: z.object({ body: z.object({
encryptedValue: z.string(), secretValue: z.string().max(10_000),
password: z.string().optional(), password: z.string().optional(),
hashedHex: z.string(),
iv: z.string(),
tag: z.string(),
expiresAt: z.string(), expiresAt: z.string(),
expiresAfterViews: z.number().min(1).optional() expiresAfterViews: z.number().min(1).optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
id: z.string().uuid() id: z.string()
}) })
} }
}, },
@@ -132,17 +130,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
body: z.object({ body: z.object({
name: z.string().max(50).optional(), name: z.string().max(50).optional(),
password: z.string().optional(), password: z.string().optional(),
encryptedValue: z.string(), secretValue: z.string(),
hashedHex: z.string(),
iv: z.string(),
tag: z.string(),
expiresAt: z.string(), expiresAt: z.string(),
expiresAfterViews: z.number().min(1).optional(), expiresAfterViews: z.number().min(1).optional(),
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization) accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
}), }),
response: { response: {
200: z.object({ 200: z.object({
id: z.string().uuid() id: z.string()
}) })
} }
}, },
@@ -168,7 +163,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
}, },
schema: { schema: {
params: z.object({ params: z.object({
sharedSecretId: z.string().uuid() sharedSecretId: z.string()
}), }),
response: { response: {
200: SecretSharingSchema 200: SecretSharingSchema

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