Compare commits

...

185 Commits

Author SHA1 Message Date
787f8318fe updated locks 2024-10-03 23:50:53 +04:00
9a27873af5 requested changes 2024-10-03 23:50:53 +04:00
0abab57d83 fix: variable naming 2024-10-03 23:50:53 +04:00
d5662dfef4 feat: allow creation of multiple project envs 2024-10-03 23:50:53 +04:00
ee2ee48b47 Merge pull request #2528 from Infisical/meet/fix-mustache-import-error
fix: change mustache import
2024-10-03 23:30:18 +04:00
896d977b95 fixed typescript 2024-10-03 23:12:10 +04:00
d1966b60a8 fix: ldif module import 2024-10-04 00:19:25 +05:30
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
bdf1f7c601 Update integration-service.ts 2024-10-03 18:30:17 +04:00
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
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
73a9cf01f3 feat: add better error message 2024-10-03 16:44:57 +05:30
97e860cf21 feat: add better error on invalid LDIF 2024-10-03 16:37:21 +05:30
25f694bbdb feat: Improve docs and add docs button 2024-10-03 09:56:27 +05:30
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
859c556425 feat: Add documentation and refactor 2024-10-02 22:55:48 +05:30
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
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
8ed04d0b75 fix: made all update fields optional 2024-10-02 20:09:31 +04:00
5b5a8ff03f chore: switch to bad request error 2024-10-02 21:20:42 +05:30
e0199084ad fix: refactor and handle modify 2024-10-02 20:51:02 +05:30
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
2273c21eb2 Clean PR 2024-10-02 09:10:22 -04:00
97c2b15e29 fix: secret sharing view count 2024-10-02 15:20:06 +04:00
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
7b64288019 Update identity-kubernetes-auth-service.ts 2024-10-02 13:39:15 +04:00
e6e1ed7ca9 Merge pull request #2512 from Infisical/feat/enforce-oidc-sso
feat: enforce oidc sso
2024-10-02 11:42:31 +08:00
73838190fd Merge remote-tracking branch 'origin/main' into feat/enforce-oidc-sso 2024-10-02 11:01:03 +08:00
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
fdc62e21ef misc: addressed review comments 2024-10-02 02:10:46 +08:00
32f866f834 Merge remote-tracking branch 'origin/main' into feat/enforce-oidc-sso 2024-10-02 02:06:39 +08:00
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
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
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
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
f0210c2607 feat: fixed UI and added permissions check to backend 2024-10-01 05:17:46 +04:00
ad88aaf17f fix: address changes 2024-09-30 16:53:42 -07:00
0485b56e8d fix: improvements 2024-10-01 03:51:55 +04:00
b65842f5c1 fix: requested changes 2024-10-01 00:16:18 +04:00
22b6e0afcd chore: refactor 2024-10-01 01:34:24 +05:30
b0e536e576 fix: improve UI and lint fix 2024-10-01 01:34:24 +05:30
54e4314e88 feat: add documentation 2024-10-01 01:34:24 +05:30
d00b1847cc feat: add UI for migration from EnvKey 2024-10-01 01:34:24 +05:30
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
659b6d5d19 Merge pull request #2515 from scott-ray-wilson/region-select
Feature: Add Data Region Select
2024-09-30 14:56:47 -04:00
9c33251c44 Update secret-sharing-service.ts 2024-09-30 22:51:42 +04:00
1a0896475c fix: added new identifier field for non-uuid IDs 2024-09-30 22:51:42 +04:00
7e820745a4 Update 20240930134623_secret-sharing-string-id.ts 2024-09-30 22:51:02 +04:00
fa63c150dd requested changes 2024-09-30 22:51:02 +04:00
1a2495a95c fix: improved root kms encryption methods 2024-09-30 22:51:02 +04:00
d79099946a feat(secret-sharing): server-side encryption 2024-09-30 22:51:02 +04:00
27afad583b fix: missed file 2024-10-01 00:03:47 +05:30
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
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
2b35e20b1d chore: rolled back bot not found errors 2024-09-30 22:16:00 +04:00
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
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
ba1db870a4 Merge pull request #2502 from Infisical/daniel/error-fixes
fix(api): error improvements
2024-09-30 13:51:03 -04:00
7885a3b0ff requested changes 2024-09-30 21:45:11 +04:00
66485f0464 fix: error improvements 2024-09-30 21:31:47 +04:00
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
3a6e79c575 Revert "Revert "feat(api): better errors and documentation"" 2024-09-30 12:58:57 -04:00
70aa73482e fix: only display region select for cloud 2024-09-30 09:58:49 -07:00
2fa30bdd0e improvement: add info about migrating regions 2024-09-30 07:08:33 -07:00
b28fe30bba chore: add region select component 2024-09-30 07:05:23 -07:00
9ba39e99c6 feature: add region select to login/signup and improve login layout 2024-09-30 07:03:02 -07:00
0e6aed7497 feat: add LDAP support for dynamic secrets 2024-09-30 19:32:24 +05:30
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
23abab987f feat: enforce oidc sso 2024-09-30 20:59:48 +08:00
a44b3efeb7 fix: allow errors to propogate in integration sync to facilitate retries unless final attempt 2024-09-27 17:02:20 -07:00
1992a09ac2 chore: lint fix 2024-09-28 03:20:02 +05:30
efa54e0c46 Merge pull request #2506 from Infisical/maidul-wdjhwedj
remove health checks for rds and redis
2024-09-27 17:31:19 -04:00
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
4090c894fc Revert "feat(api): better errors and documentation" 2024-09-27 17:25:11 -04:00
221bde01f8 remove health checks for rds and redis 2024-09-27 17:24:09 -04:00
b191a3c2f4 fix: check if column exists in migration 2024-09-28 02:35:10 +05:30
032197ee9f Update access-approval-policy-fns.ts 2024-09-27 22:03:46 +04:00
d5a4eb609a fix: error improvements 2024-09-27 21:22:14 +04:00
e7f1980b80 improvement: switch slug to use badge 2024-09-27 09:46:16 -07:00
d430293c66 Merge pull request #2494 from Infisical/daniel/api-errors
feat(api): better errors and documentation
2024-09-27 20:25:10 +04:00
180d2692cd Re-trigger tests 2024-09-27 20:17:17 +04:00
433e58655a Update add-errors-to-response-schemas.ts 2024-09-27 20:12:08 +04:00
5ffb6b7232 fixed tests 2024-09-27 20:02:43 +04:00
55ca9149d5 Re-trigger tests 2024-09-27 20:02:43 +04:00
4ea57ca9a0 requested changes 2024-09-27 20:02:43 +04:00
7ac4b0b79f feat(api-docs): add error responses to API documentation 2024-09-27 20:02:43 +04:00
2d51ed317f feat(api): improve errors and error handling 2024-09-27 20:02:43 +04:00
02c51b05b6 Update login.mdx to remove sentence 2024-09-27 10:33:36 -04:00
cd09f03f0b chore: swap to boolean cast instead of !! 2024-09-27 07:19:57 -07:00
bc475e0f08 misc: added proper notifs for paths with policies in overview 2024-09-27 22:18:47 +08:00
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
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
59da513481 fix: address modal close unresponsive 2024-09-27 20:30:28 +08:00
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
afd6dd5257 improvement: improve query param boolean handling for dashboard queries and move dashboard router to v1 2024-09-26 17:50:57 -07:00
3a43d7c5d5 improvement: add tooltip to secret table resource count and match secret icon color 2024-09-26 16:40:33 -07:00
65375886bd fix: handle overflow on dropdown content 2024-09-26 16:22:41 -07:00
8495107849 improvement: display slug for aws regions 2024-09-26 16:14:23 -07:00
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
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
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
971987c786 fix: display all envs in secrets overview header 2024-09-26 09:32:15 -07:00
cd71a13bb7 fix: refactor secrets overview endpoint to filter envs for secrets with read permissions 2024-09-26 09:24:29 -07:00
98290fe31b remove audit logs 2024-09-26 12:23:11 -04:00
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
658a044e85 Merge pull request #2492 from Infisical/maidul-gdfvdfkw
hide audit log filter in prod
2024-09-26 11:42:37 -04:00
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
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
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
1fcfab7efa feat: remove finalizers 2024-09-26 02:40:30 +04:00
499334eef1 fixed finalizers 2024-09-26 02:35:16 +04:00
9fd76b8729 chore: updated helm 2024-09-25 18:29:55 +04:00
80d450e980 fix(k8-operator): updating CRD does not reflect in operator 2024-09-25 18:26:50 +04:00
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
bf8e1f2bfd misc: added missing filter 2024-09-25 21:36:28 +08:00
f7d10ceeda Merge remote-tracking branch 'origin/main' into misc/approval-policy-tf-resource-prereq-1 2024-09-25 21:15:46 +08:00
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
51638b7c71 fix: check user group membership correctly 2024-09-25 18:02:32 +05:30
adaddad370 misc: added rate limiting 2024-09-25 18:46:44 +08:00
cf6ff58f16 misc: access approval prerequisites 2024-09-25 18:38:06 +08:00
3e3f42a8f7 doc: add groups endpoints to api reference documentation 2024-09-25 15:31:54 +08:00
974e21d856 fix: addressed bugs 2024-09-25 14:30:22 +08:00
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
3a9a6767a0 fix: throw not found when entity is not found 2024-09-24 21:01:09 +04:00
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
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
59f3581370 misc: made it specific for cloud 2024-09-25 00:31:13 +08:00
ccae63936c misc: added maintenance notice to audit log page and handled project auto-select 2024-09-25 00:27:36 +08:00
6733349af0 misc: updated secret approval policy api to support TF usecase 2024-09-25 00:07:11 +08:00
f63c6b725b feat: allow users to replace auth methods 2024-09-24 21:07:43 +05:30
50b51f1810 Merge pull request #2475 from Infisical/daniel/prefix-secret-folders
fix(folders-api): prefix paths
2024-09-24 17:30:47 +04:00
fc39b3b0dd fix(dashboard): fix imports missing secrets counter 2024-09-24 17:24:38 +04:00
5964976e47 fix(folders-api): prefix paths 2024-09-24 15:49:27 +04:00
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
2469c8d0c6 fix: group listing using project id 2024-09-24 02:24:37 +05:30
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
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
6b2273d314 update message 2024-09-23 15:32:11 -04:00
b886e66ee9 Remove service token notice 2024-09-23 15:25:36 -04:00
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
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
fd7d8ddf2d fix: group selection on create policy 2024-09-23 20:59:05 +05:30
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
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
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
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
fcbc7fcece chore: fix test 2024-09-23 10:53:58 +05:30
c2252c65a4 chore: lint fix 2024-09-23 10:30:49 +05:30
e150673de4 chore: Refactor and remove new tables 2024-09-23 10:26:58 +05:30
14c89c9be5 misc: addressed invalid redirect condition in signup invite page 2024-09-22 20:32:55 +08:00
ebea74b607 fix: address capitalization 2024-09-21 19:41:58 -07:00
5bbe5421bf docs: add images 2024-09-20 17:32:14 -07:00
279289989f docs: add entra / azure mfa docs 2024-09-20 17:31:32 -07:00
12ecefa832 chore: remove logs 2024-09-20 09:31:18 +05:30
dd9a00679d chore: fix type 2024-09-20 09:03:43 +05:30
081502848d feat: allow secret approvals with user groups 2024-09-20 08:51:48 +05:30
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 { 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({
method: "POST",
url: `/api/v1/secret-approvals`,
@ -26,7 +27,7 @@ describe("Secret approval policy router", async () => {
const policy = await createPolicy({
secretPath: "/",
approvals: 1,
approvers: [seedData1.id],
approvers: [{id:seedData1.id, type: ApproverType.User}],
name: "test-policy"
});

View File

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

View File

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

View File

@ -61,10 +61,12 @@
"jwks-rsa": "^3.1.0",
"knex": "^3.0.1",
"ldapjs": "^3.0.7",
"ldif": "^0.5.1",
"libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0",
"mongodb": "^6.8.1",
"ms": "^2.1.3",
"mustache": "^4.2.0",
"mysql2": "^3.9.8",
"nanoid": "^3.3.4",
"nodemailer": "^6.9.9",
@ -85,6 +87,7 @@
"safe-regex": "^2.1.1",
"scim-patch": "^0.8.3",
"scim2-parse-filter": "^0.2.10",
"sjcl": "^1.0.8",
"smee-client": "^2.0.0",
"tedious": "^18.2.1",
"tweetnacl": "^1.0.3",
@ -108,6 +111,7 @@
"@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13",
"@types/lodash.isequal": "^4.5.8",
"@types/mustache": "^4.2.5",
"@types/node": "^20.9.5",
"@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12",
@ -117,6 +121,7 @@
"@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6",
"@types/sjcl": "^1.0.34",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^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",
"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": {
"version": "20.9.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz",
@ -7296,6 +7308,13 @@
"@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": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz",
@ -13008,6 +13027,12 @@
"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": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
@ -13704,6 +13729,15 @@
"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": {
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz",
@ -16397,6 +16431,15 @@
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@ -17874,12 +17917,14 @@
"node_modules/tweetnacl": {
"version": "1.0.3",
"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": {
"version": "0.15.1",
"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": {
"version": "0.4.0",

View File

@ -71,6 +71,7 @@
"@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13",
"@types/lodash.isequal": "^4.5.8",
"@types/mustache": "^4.2.5",
"@types/node": "^20.9.5",
"@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12",
@ -80,6 +81,7 @@
"@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6",
"@types/sjcl": "^1.0.34",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
@ -158,10 +160,12 @@
"jwks-rsa": "^3.1.0",
"knex": "^3.0.1",
"ldapjs": "^3.0.7",
"ldif": "^0.5.1",
"libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0",
"mongodb": "^6.8.1",
"ms": "^2.1.3",
"mustache": "^4.2.0",
"mysql2": "^3.9.8",
"nanoid": "^3.3.4",
"nodemailer": "^6.9.9",
@ -182,6 +186,7 @@
"safe-regex": "^2.1.1",
"scim-patch": "^0.8.3",
"scim2-parse-filter": "^0.2.10",
"sjcl": "^1.0.8",
"smee-client": "^2.0.0",
"tedious": "^18.2.1",
"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 { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-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 { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
@ -181,6 +182,7 @@ declare module "fastify" {
orgAdmin: TOrgAdminServiceFactory;
slack: TSlackServiceFactory;
workflowIntegration: TWorkflowIntegrationServiceFactory;
migration: TExternalMigrationServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

View File

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

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-gcp-auths";
export * from "./identity-kubernetes-auths";
export * from "./identity-metadata";
export * from "./identity-oidc-auths";
export * from "./identity-org-memberships";
export * from "./identity-project-additional-privilege";

View File

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

View File

@ -26,7 +26,8 @@ export const OidcConfigsSchema = z.object({
isActive: z.boolean(),
createdAt: 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>;

View File

@ -12,7 +12,8 @@ export const SecretApprovalPoliciesApproversSchema = z.object({
policyId: z.string().uuid(),
createdAt: 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>;

View File

@ -5,14 +5,16 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SecretSharingSchema = z.object({
id: z.string().uuid(),
encryptedValue: z.string(),
iv: z.string(),
tag: z.string(),
hashedHex: z.string(),
encryptedValue: z.string().nullable().optional(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
hashedHex: z.string().nullable().optional(),
expiresAt: z.date(),
userId: 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"),
name: z.string().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>;

View File

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

View File

@ -3,10 +3,11 @@ import slugify from "@sindresorhus/slugify";
import { z } from "zod";
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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
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";
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {

View File

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

View File

@ -61,7 +61,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
id: samlConfigId
};
} 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);
@ -100,6 +100,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
async (req, profile, cb) => {
try {
if (!profile) throw new BadRequestError({ message: "Missing profile" });
const email =
profile?.email ??
// 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({
externalId: profile.nameID,
email,
@ -130,7 +139,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
lastName: lastName as string,
relayState: (req.body as { RelayState?: string }).RelayState,
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 });
} catch (error) {

View File

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

View File

@ -13,7 +13,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
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({
email: true,
firstName: true,
@ -46,7 +46,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
id: z.string(),
name: z.string(),
approvals: z.number(),
approvers: z.string().array(),
approvers: z
.object({
userId: z.string().nullable().optional()
})
.array(),
secretPath: z.string().optional().nullable(),
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(),
environment: z.string(),
reviewers: z.object({ userId: z.string(), status: z.string() }).array(),
approvers: z.string().array()
approvers: z
.object({
userId: z.string().nullable().optional()
})
.array()
}).array()
})
}

View File

@ -5,22 +5,38 @@ import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies } from
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
import { ApproverType } from "./access-approval-policy-types";
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy);
const accessApprovalPolicyFindQuery = async (tx: Knex, filter: TFindFilter<TAccessApprovalPolicies>) => {
const accessApprovalPolicyFindQuery = async (
tx: Knex,
filter: TFindFilter<TAccessApprovalPolicies>,
customFilter?: {
policyId?: string;
}
) => {
const result = await tx(TableName.AccessApprovalPolicy)
// eslint-disable-next-line
.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`)
.leftJoin(
TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`,
`${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("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
@ -30,10 +46,10 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
return result;
};
const findById = async (id: string, tx?: Knex) => {
const findById = async (policyId: string, tx?: Knex) => {
try {
const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), {
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id
[`${TableName.AccessApprovalPolicy}.id` as "id"]: policyId
});
const formattedDoc = sqlNestRelationships({
data: doc,
@ -50,9 +66,18 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
childrenMapper: [
{
key: "approverUserId",
label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({
userId: approverUserId
label: "approvers" as const,
mapper: ({ approverUserId: id }) => ({
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 {
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter);
const formattedDocs = sqlNestRelationships({
data: docs,
@ -84,9 +115,19 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
childrenMapper: [
{
key: "approverUserId",
label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({
userId: approverUserId
label: "approvers" as const,
mapper: ({ approverUserId: id, approverUsername }) => ({
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 { BadRequestError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TVerifyApprovers } from "./access-approval-policy-types";
import { TIsApproversValid } from "./access-approval-policy-types";
export const verifyApprovers = async ({
export const isApproversValid = async ({
userIds,
projectId,
orgId,
@ -14,9 +13,9 @@ export const verifyApprovers = async ({
actorAuthMethod,
secretPath,
permissionService
}: TVerifyApprovers) => {
for await (const userId of userIds) {
try {
}: TIsApproversValid) => {
try {
for await (const userId of userIds) {
const { permission: approverPermission } = await permissionService.getProjectPermission(
ActorType.USER,
userId,
@ -29,8 +28,9 @@ export const verifyApprovers = async ({
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath })
);
} catch (err) {
throw new BadRequestError({ message: "One or more approvers doesn't have access to be specified secret path" });
}
} 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 { 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 { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TGroupDALFactory } from "../group/group-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
import { verifyApprovers } from "./access-approval-policy-fns";
import { isApproversValid } from "./access-approval-policy-fns";
import {
ApproverType,
TCreateAccessApprovalPolicy,
TDeleteAccessApprovalPolicy,
TGetAccessApprovalPolicyByIdDTO,
TGetAccessPolicyCountByEnvironmentDTO,
TListAccessApprovalPoliciesDTO,
TUpdateAccessApprovalPolicy
@ -25,6 +29,8 @@ type TSecretApprovalPolicyServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
groupDAL: TGroupDALFactory;
userDAL: Pick<TUserDALFactory, "find">;
};
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
@ -32,9 +38,11 @@ export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprov
export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
groupDAL,
permissionService,
projectEnvDAL,
projectDAL
projectDAL,
userDAL
}: TSecretApprovalPolicyServiceFactoryDep) => {
const createAccessApprovalPolicy = async ({
name,
@ -50,9 +58,23 @@ export const accessApprovalPolicyServiceFactory = ({
enforcementLevel
}: TCreateAccessApprovalPolicy) => {
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" });
const { permission } = await permissionService.getProjectPermission(
@ -67,18 +89,65 @@ export const accessApprovalPolicyServiceFactory = ({
ProjectPermissionSub.SecretApproval
);
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,
orgId: actorOrgId,
envSlug: environment,
secretPath,
actorAuthMethod,
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 doc = await accessApprovalPolicyDAL.create(
{
@ -90,13 +159,26 @@ export const accessApprovalPolicyServiceFactory = ({
},
tx
);
await accessApprovalPolicyApproverDAL.insertMany(
approvers.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),
tx
);
if (approverUserIds.length) {
await accessApprovalPolicyApproverDAL.insertMany(
approverUserIds.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),
tx
);
}
if (groupApprovers) {
await accessApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((groupId) => ({
approverGroupId: groupId,
policyId: doc.id
})),
tx
);
}
return doc;
});
return { ...accessApproval, environment: env, projectId: project.id };
@ -110,7 +192,7 @@ export const accessApprovalPolicyServiceFactory = ({
projectSlug
}: TListAccessApprovalPoliciesDTO) => {
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.
/* const { permission } = */ await permissionService.getProjectPermission(
@ -138,8 +220,30 @@ export const accessApprovalPolicyServiceFactory = ({
approvals,
enforcementLevel
}: 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);
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(
actor,
actorId,
@ -161,26 +265,100 @@ export const accessApprovalPolicyServiceFactory = ({
},
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,
orgId: actorOrgId,
envSlug: accessApprovalPolicy.environment.slug,
secretPath: doc.secretPath!,
actorAuthMethod,
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(
approvers.map((userId) => ({
userApproverIds.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),
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 {
@ -198,7 +376,7 @@ export const accessApprovalPolicyServiceFactory = ({
actorOrgId
}: TDeleteAccessApprovalPolicy) => {
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(
actor,
@ -226,7 +404,7 @@ export const accessApprovalPolicyServiceFactory = ({
}: TGetAccessPolicyCountByEnvironmentDTO) => {
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(
actor,
@ -235,22 +413,53 @@ export const accessApprovalPolicyServiceFactory = ({
actorAuthMethod,
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 });
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 });
if (!policies) throw new BadRequestError({ message: "No policies found" });
if (!policies) throw new NotFoundError({ message: "No policies found" });
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 {
getAccessPolicyCountByEnvSlug,
createAccessApprovalPolicy,
deleteAccessApprovalPolicy,
updateAccessApprovalPolicy,
getAccessApprovalPolicyByProjectSlug
getAccessApprovalPolicyByProjectSlug,
getAccessApprovalPolicyById
};
};

View File

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

View File

@ -39,6 +39,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId`
)
.leftJoin(
TableName.UserGroupMembership,
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.join<TUsers>(
db(TableName.Users).as("requestedByUser"),
@ -59,6 +65,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
)
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"))
.select(
db.ref("projectId").withSchema(TableName.Environment),
@ -142,7 +149,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
label: "reviewers" as const,
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`
)
.join(
.leftJoin(
TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId`
)
.join<TUsers>(
.leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalPolicyApproverUser"),
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
"accessApprovalPolicyApproverUser.id"
)
.leftJoin(
TableName.UserGroupMembership,
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalPolicyGroupApproverUser"),
`${TableName.UserGroupMembership}.userId`,
"accessApprovalPolicyGroupApproverUser.id"
)
.leftJoin(
TableName.AccessApprovalRequestReviewer,
@ -200,10 +223,15 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
tx.ref("userId").withSchema(TableName.UserGroupMembership),
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("accessApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
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("accessApprovalPolicyGroupApproverUser").as("approverGroupLastName"),
tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
@ -282,6 +310,23 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
lastName,
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 { UnauthorizedError } from "@app/lib/errors";
import { BadRequestError } from "@app/lib/errors";
import { TVerifyPermission } from "./access-approval-request-types";
@ -19,7 +19,7 @@ export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) =
);
if (!permission || !permission.length) {
throw new UnauthorizedError({ message: "No permission provided" });
throw new BadRequestError({ message: "No permission provided" });
}
const requestedPermissions: string[] = [];
@ -39,10 +39,10 @@ export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) =
const permissionEnv = firstPermission.conditions?.environment;
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") {
throw new UnauthorizedError({ message: "Permission path is not a string" });
throw new BadRequestError({ message: "Permission path is not a string" });
}
return {

View File

@ -3,7 +3,7 @@ import ms from "ms";
import { ProjectMembershipRole } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
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 { 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 { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
@ -57,6 +58,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
TAccessApprovalRequestReviewerDALFactory,
"create" | "find" | "findOne" | "transaction"
>;
groupDAL: Pick<TGroupDALFactory, "findAllGroupPossibleMembers">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<
@ -70,6 +72,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
export const accessApprovalRequestServiceFactory = ({
groupDAL,
projectDAL,
projectEnvDAL,
permissionService,
@ -96,7 +99,7 @@ export const accessApprovalRequestServiceFactory = ({
}: TCreateAccessApprovalRequestDTO) => {
const cfg = getConfig();
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.
const { membership } = await permissionService.getProjectPermission(
@ -106,31 +109,56 @@ export const accessApprovalRequestServiceFactory = ({
actorAuthMethod,
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);
if (!requestedByUser) throw new UnauthorizedError({ message: "User not found" });
if (!requestedByUser) throw new ForbiddenRequestError({ message: "User not found" });
await projectDAL.checkProjectUpgradeStatus(project.id);
const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({ permissions: requestedPermissions });
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
if (!environment) throw new UnauthorizedError({ message: "Environment not found" });
if (!environment) throw new NotFoundError({ message: "Environment not found" });
const policy = await accessApprovalPolicyDAL.findOne({
envId: environment.id,
secretPath
});
if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." });
if (!policy) throw new NotFoundError({ message: "No policy matching criteria was found." });
const approverIds: string[] = [];
const approverGroupIds: string[] = [];
const approvers = await accessApprovalPolicyApproverDAL.find({
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({
$in: {
id: approvers.map((approver) => approver.approverUserId)
id: [...new Set(approverIds)]
}
});
@ -236,7 +264,7 @@ export const accessApprovalRequestServiceFactory = ({
actorAuthMethod
}: TListApprovalRequestsDTO) => {
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(
actor,
@ -245,7 +273,9 @@ export const accessApprovalRequestServiceFactory = ({
actorAuthMethod,
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 });
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
@ -270,7 +300,7 @@ export const accessApprovalRequestServiceFactory = ({
actorOrgId
}: TReviewAccessRequestDTO) => {
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 { membership, hasRole } = await permissionService.getProjectPermission(
@ -281,19 +311,21 @@ export const accessApprovalRequestServiceFactory = ({
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 (
!hasRole(ProjectMembershipRole.Admin) &&
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
) {
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);
await verifyApprovers({
const approversValid = await isApproversValid({
projectId: accessApprovalRequest.projectId,
orgId: actorOrgId,
envSlug: accessApprovalRequest.environment,
@ -303,6 +335,10 @@ export const accessApprovalRequestServiceFactory = ({
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 });
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
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 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(
actor,
@ -394,7 +430,9 @@ export const accessApprovalRequestServiceFactory = ({
actorAuthMethod,
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 });

View File

@ -5,7 +5,7 @@ import { SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
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 { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue";
@ -43,14 +43,15 @@ export const auditLogStreamServiceFactory = ({
actorOrgId,
actorAuthMethod
}: 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 plan = await licenseService.getPlan(actorOrgId);
if (!plan.auditLogStreams)
if (!plan.auditLogStreams) {
throw new BadRequestError({
message: "Failed to create audit log streams due to plan restriction. Upgrade plan to create group."
});
}
const { permission } = await permissionService.getOrgPermission(
actor,
@ -120,7 +121,7 @@ export const auditLogStreamServiceFactory = ({
actorOrgId,
actorAuthMethod
}: 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);
if (!plan.auditLogStreams)
@ -129,7 +130,7 @@ export const auditLogStreamServiceFactory = ({
});
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 { 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) => {
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);
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 { 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 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 { 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 { 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 { 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 { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@ -19,7 +18,6 @@ type TCertificateAuthorityCrlServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
// licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
@ -66,7 +64,7 @@ export const certificateAuthorityCrlServiceFactory = ({
*/
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => {
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(
actor,
@ -81,13 +79,6 @@ export const certificateAuthorityCrlServiceFactory = ({
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 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 { getConfig } from "@app/lib/config/env";
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 { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@ -61,7 +61,7 @@ export const dynamicSecretLeaseServiceFactory = ({
}: TCreateDynamicSecretLeaseDTO) => {
const appCfg = getConfig();
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 { permission } = await permissionService.getProjectPermission(
@ -84,10 +84,10 @@ export const dynamicSecretLeaseServiceFactory = ({
}
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 });
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);
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
@ -134,7 +134,7 @@ export const dynamicSecretLeaseServiceFactory = ({
leaseId
}: TRenewDynamicSecretLeaseDTO) => {
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 { permission } = await permissionService.getProjectPermission(
@ -157,10 +157,10 @@ export const dynamicSecretLeaseServiceFactory = ({
}
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);
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 selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
@ -208,7 +208,7 @@ export const dynamicSecretLeaseServiceFactory = ({
isForced
}: TDeleteDynamicSecretLeaseDTO) => {
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 { permission } = await permissionService.getProjectPermission(
@ -224,10 +224,10 @@ export const dynamicSecretLeaseServiceFactory = ({
);
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);
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 selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
@ -273,7 +273,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorAuthMethod
}: TListDynamicSecretLeasesDTO) => {
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 { permission } = await permissionService.getProjectPermission(
@ -289,10 +289,10 @@ export const dynamicSecretLeaseServiceFactory = ({
);
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 });
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 });
return dynamicSecretLeases;
@ -309,7 +309,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorAuthMethod
}: TDetailsDynamicSecretLeaseDTO) => {
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 { permission } = await permissionService.getProjectPermission(
@ -325,10 +325,10 @@ export const dynamicSecretLeaseServiceFactory = ({
);
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);
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
if (!dynamicSecretLease) throw new NotFoundError({ message: "Dynamic secret lease not found" });
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 { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
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 { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@ -66,7 +66,7 @@ export const dynamicSecretServiceFactory = ({
actorAuthMethod
}: TCreateDynamicSecretDTO) => {
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 { permission } = await permissionService.getProjectPermission(
@ -89,7 +89,7 @@ export const dynamicSecretServiceFactory = ({
}
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 });
if (existingDynamicSecret)
@ -134,7 +134,7 @@ export const dynamicSecretServiceFactory = ({
actorAuthMethod
}: TUpdateDynamicSecretDTO) => {
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;
@ -158,10 +158,10 @@ export const dynamicSecretServiceFactory = ({
}
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 });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
if (!dynamicSecretCfg) throw new NotFoundError({ message: "Dynamic secret not found" });
if (newName) {
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
@ -213,7 +213,7 @@ export const dynamicSecretServiceFactory = ({
isForced
}: TDeleteDynamicSecretDTO) => {
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;
@ -230,7 +230,7 @@ export const dynamicSecretServiceFactory = ({
);
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 });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
@ -271,7 +271,7 @@ export const dynamicSecretServiceFactory = ({
actor
}: TDetailsDynamicSecretDTO) => {
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 { permission } = await permissionService.getProjectPermission(
@ -287,10 +287,10 @@ export const dynamicSecretServiceFactory = ({
);
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 });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
if (!dynamicSecretCfg) throw new NotFoundError({ message: "Dynamic secret not found" });
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
@ -313,26 +313,29 @@ export const dynamicSecretServiceFactory = ({
projectId,
path,
environmentSlugs,
search
search,
isInternal
}: TListDynamicSecretsMultiEnvDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
if (!isInternal) {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
// verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) =>
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
)
);
// verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) =>
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: 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(
{ $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);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
if (!folder) throw new NotFoundError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.find(
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
@ -395,7 +398,7 @@ export const dynamicSecretServiceFactory = ({
if (!projectId) {
if (!projectSlug) throw new BadRequestError({ message: "Project ID or slug required" });
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;
}
@ -412,7 +415,7 @@ export const dynamicSecretServiceFactory = ({
);
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(
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
@ -434,26 +437,29 @@ export const dynamicSecretServiceFactory = ({
path,
environmentSlugs,
projectId,
isInternal,
...params
}: TListDynamicSecretsMultiEnvDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
if (!isInternal) {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
// verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) =>
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
)
);
// verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) =>
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: 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({
folderIds: folders.map((folder) => folder.id),

View File

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

View File

@ -3,6 +3,7 @@ import { AwsIamProvider } from "./aws-iam";
import { AzureEntraIDProvider } from "./azure-entra-id";
import { CassandraProvider } from "./cassandra";
import { ElasticSearchProvider } from "./elastic-search";
import { LdapProvider } from "./ldap";
import { DynamicSecretProviders } from "./models";
import { MongoAtlasProvider } from "./mongo-atlas";
import { MongoDBProvider } from "./mongo-db";
@ -20,5 +21,6 @@ export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.MongoDB]: MongoDBProvider(),
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
[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)
});
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 {
SqlDatabase = "sql-database",
Cassandra = "cassandra",
@ -184,7 +195,8 @@ export enum DynamicSecretProviders {
ElasticSearch = "elastic-search",
MongoDB = "mongo-db",
RabbitMq = "rabbit-mq",
AzureEntraID = "azure-entra-id"
AzureEntraID = "azure-entra-id",
Ldap = "ldap"
}
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.MongoDB), inputs: DynamicSecretMongoDBSchema }),
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 = {

View File

@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability";
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 { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@ -145,7 +145,7 @@ export const externalKmsServiceFactory = ({
const kmsSlug = slug ? slugify(slug) : undefined;
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 = "";
const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
@ -220,7 +220,7 @@ export const externalKmsServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
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 kms = await kmsDAL.deleteById(kmsDoc.id, tx);
@ -258,7 +258,7 @@ export const externalKmsServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
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({
type: KmsDataKey.Organization,
@ -298,7 +298,7 @@ export const externalKmsServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
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({
type: KmsDataKey.Organization,

View File

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

View File

@ -2,7 +2,7 @@ import { Knex } from "knex";
import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
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 {
TAddUsersToGroup,
@ -73,24 +73,24 @@ const addAcceptedUsersToGroup = async ({
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
throw new NotFoundError({
message: "Failed to find project owner"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
throw new NotFoundError({
message: "Failed to find project owner's latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId }, tx);
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
throw new NotFoundError({
message: "Failed to find project bot"
});
}
@ -200,7 +200,7 @@ export const addUsersToGroupByUserIds = async ({
userIds.forEach((userId) => {
if (!existingUserOrgMembershipsUserIdsSet.has(userId))
throw new BadRequestError({
throw new ForbiddenRequestError({
message: `User with id ${userId} is not part of the organization`
});
});
@ -303,7 +303,7 @@ export const removeUsersFromGroupByUserIds = async ({
userIds.forEach((userId) => {
if (!existingUserGroupMembershipsUserIdsSet.has(userId))
throw new BadRequestError({
throw new ForbiddenRequestError({
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));
userIds.forEach((userId) => {
if (!usersUserIdsSet.has(userId)) {
throw new BadRequestError({
throw new NotFoundError({
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 { 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 { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
@ -30,7 +30,10 @@ import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
type TGroupServiceFactoryDep = {
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">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
userGroupMembershipDAL: Pick<
@ -59,7 +62,7 @@ export const groupServiceFactory = ({
licenseService
}: TGroupServiceFactoryDep) => {
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(
actor,
@ -82,7 +85,8 @@ export const groupServiceFactory = ({
);
const isCustomRole = Boolean(customRole);
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({
name,
@ -105,7 +109,7 @@ export const groupServiceFactory = ({
actorAuthMethod,
actorOrgId
}: 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(
actor,
@ -124,7 +128,7 @@ export const groupServiceFactory = ({
const group = await groupDAL.findOne({ orgId: actorOrgId, id });
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;
@ -137,7 +141,7 @@ export const groupServiceFactory = ({
const isCustomRole = Boolean(customOrgRole);
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
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;
}
@ -161,7 +165,7 @@ export const groupServiceFactory = ({
};
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(
actor,
@ -188,9 +192,7 @@ export const groupServiceFactory = ({
};
const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => {
if (!actorOrgId) {
throw new BadRequestError({ message: "Failed to read group without organization" });
}
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission } = await permissionService.getOrgPermission(
actor,
@ -221,7 +223,7 @@ export const groupServiceFactory = ({
actorAuthMethod,
actorOrgId
}: 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(
actor,
@ -238,11 +240,11 @@ export const groupServiceFactory = ({
});
if (!group)
throw new BadRequestError({
throw new NotFoundError({
message: `Failed to find group with ID ${id}`
});
const users = await groupDAL.findAllGroupMembers({
const users = await groupDAL.findAllGroupPossibleMembers({
orgId: group.orgId,
groupId: group.id,
offset,
@ -256,7 +258,7 @@ export const groupServiceFactory = ({
};
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(
actor,
@ -274,7 +276,7 @@ export const groupServiceFactory = ({
});
if (!group)
throw new BadRequestError({
throw new NotFoundError({
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" });
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({
group,
@ -311,7 +313,7 @@ export const groupServiceFactory = ({
actorAuthMethod,
actorOrgId
}: 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(
actor,
@ -329,7 +331,7 @@ export const groupServiceFactory = ({
});
if (!group)
throw new BadRequestError({
throw new NotFoundError({
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" });
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({
group,

View File

@ -4,7 +4,7 @@ import ms from "ms";
import { z } from "zod";
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 { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-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
export const UnpackedPermissionSchema = z.object({
subject: z.union([z.string().min(1), z.string().array()]).optional(),
action: z.union([z.string().min(1), z.string().array()]),
conditions: z
.object({
environment: z.string().optional(),
secretPath: z
.object({
$glob: z.string().min(1)
})
.optional()
})
.optional()
subject: z
.union([z.string().min(1), z.string().array()])
.transform((el) => (typeof el !== "string" ? el[0] : el))
.optional(),
action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
conditions: z.unknown().optional()
});
const unpackPermissions = (permissions: unknown) =>
@ -71,12 +65,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
...dto
}: TCreateIdentityPrivilegeDTO) => {
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 identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
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(
actor,
@ -143,12 +137,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod
}: TUpdateIdentityPrivilegeDTO) => {
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 identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
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(
actor,
@ -173,7 +167,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
slug,
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) {
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
slug: data.slug,
@ -224,12 +218,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod
}: TDeleteIdentityPrivilegeDTO) => {
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 identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
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(
actor,
@ -254,7 +248,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
slug,
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);
return {
@ -274,12 +268,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod
}: TGetIdentityPrivilegeDetailsDTO) => {
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 identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
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(
actor,
actorId,
@ -293,7 +287,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
slug,
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 {
...identityPrivilege,
@ -310,12 +304,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
projectSlug
}: TListIdentityPrivilegesDTO) => {
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 identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
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(
actor,
actorId,

View File

@ -21,7 +21,7 @@ import {
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} 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 { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
@ -253,7 +253,7 @@ export const ldapConfigServiceFactory = ({
};
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({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
@ -289,10 +289,10 @@ export const ldapConfigServiceFactory = ({
const getLdapCfg = async (filter: { orgId: string; isActive?: boolean; id?: string }) => {
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 });
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({
ciphertext: orgBot.encryptedSymmetricKey,
@ -375,7 +375,7 @@ export const ldapConfigServiceFactory = ({
const bootLdap = async (organizationSlug: string) => {
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({
orgId: organization.id,
@ -420,7 +420,7 @@ export const ldapConfigServiceFactory = ({
const serverCfg = await getServerCfg();
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.LDAP)) {
throw new BadRequestError({
throw new ForbiddenRequestError({
message: "Login with LDAP is disabled by administrator."
});
}
@ -432,7 +432,7 @@ export const ldapConfigServiceFactory = ({
});
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) {
await userDAL.transaction(async (tx) => {
@ -700,7 +700,7 @@ export const ldapConfigServiceFactory = ({
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);
@ -741,13 +741,13 @@ export const ldapConfigServiceFactory = ({
const groups = await searchGroups(ldapConfig, groupSearchFilter, ldapConfig.groupSearchBase);
if (!groups.some((g) => g.cn === ldapGroupCN)) {
throw new BadRequestError({
throw new NotFoundError({
message: "Failed to find LDAP Group CN"
});
}
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({
ldapConfigId,
@ -781,7 +781,7 @@ export const ldapConfigServiceFactory = ({
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({
ldapConfigId: ldapConfig.id,

View File

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

View File

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

View File

@ -50,6 +50,7 @@ export const permissionDALFactory = (db: TDbClient) => {
.select(
selectAllTableCols(TableName.OrgMembership),
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("groupId").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(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(
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("username").withSchema(TableName.Users).as("username"),
// groups specific
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
@ -256,6 +263,9 @@ export const permissionDALFactory = (db: TDbClient) => {
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
// 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("orgId").withSchema(TableName.Project),
db.ref("id").withSchema(TableName.Project).as("projectId")
@ -266,6 +276,7 @@ export const permissionDALFactory = (db: TDbClient) => {
key: "projectId",
parentMapper: ({
orgId,
username,
orgAuthEnforced,
membershipId,
groupMembershipId,
@ -278,6 +289,7 @@ export const permissionDALFactory = (db: TDbClient) => {
orgAuthEnforced,
userId,
projectId,
username,
id: membershipId || groupMembershipId,
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
@ -353,6 +365,15 @@ export const permissionDALFactory = (db: TDbClient) => {
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
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.IdentityProjectMembership}.id`
)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`)
.leftJoin(
TableName.ProjectRoles,
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
@ -414,11 +436,17 @@ export const permissionDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembership}.projectId`,
`${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)
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
.select(
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("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
@ -442,15 +470,19 @@ export const permissionDALFactory = (db: TDbClient) => {
db
.ref("temporaryAccessEndTime")
.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({
data: docs,
key: "membershipId",
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId }) => ({
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({
id: membershipId,
identityId,
username: identityName,
projectId,
createdAt: membershipCreatedAt,
updatedAt: membershipUpdatedAt,
@ -488,6 +520,15 @@ export const permissionDALFactory = (db: TDbClient) => {
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
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 { UnauthorizedError } from "@app/lib/errors";
import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
@ -14,14 +14,19 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
].includes(actorAuthMethod);
}
function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) {
function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"]) {
if (actorAuthMethod === undefined) {
throw new UnauthorizedError({ name: "No auth method defined" });
}
if (isSamlEnforced && actorAuthMethod !== null && !isAuthMethodSaml(actorAuthMethod)) {
throw new UnauthorizedError({ name: "Cannot access org-scoped resource" });
if (
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 { PackRule, unpackRules } from "@casl/ability/extra";
import { MongoQuery } from "@ucast/mongo2js";
import handlebars from "handlebars";
import {
OrgMembershipRole,
@ -10,7 +11,8 @@ import {
TProjectMemberships
} from "@app/db/schemas";
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 { TOrgRoleDALFactory } from "@app/services/org/org-role-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 { TPermissionDALFactory } from "./permission-dal";
import { validateOrgSAML } from "./permission-fns";
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-types";
import { validateOrgSSO } from "./permission-fns";
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types";
import {
buildServiceTokenProjectPermission,
projectAdminPermissions,
@ -62,7 +64,7 @@ export const permissionServiceFactory = ({
permissions as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]
);
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), []);
@ -72,7 +74,7 @@ export const permissionServiceFactory = ({
});
};
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => {
const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
const rules = projectUserRoles
.map(({ role, permissions }) => {
switch (role) {
@ -90,7 +92,7 @@ export const permissionServiceFactory = ({
);
}
default:
throw new BadRequestError({
throw new NotFoundError({
name: "ProjectRoleInvalid",
message: "Project role not found"
});
@ -98,9 +100,7 @@ export const permissionServiceFactory = ({
})
.reduce((curr, prev) => prev.concat(curr), []);
return createMongoAbility<ProjectPermissionSet>(rules, {
conditionsMatcher
});
return rules;
};
/*
@ -114,11 +114,11 @@ export const permissionServiceFactory = ({
) => {
// when token is scoped, ensure the passed org id is same as user org id
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);
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) {
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.
@ -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.
// 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) {
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(
membership?.groups?.map(({ role, customRolePermission }) => ({
@ -143,9 +143,9 @@ export const permissionServiceFactory = ({
const getIdentityOrgPermission = async (identityId: string, orgId: string) => {
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) {
throw new BadRequestError({ name: "Custom permission not found" });
throw new NotFoundError({ name: "Custom organization permission not found" });
}
return {
permission: buildOrgPermission([{ role: membership.role, permissions: membership.permissions }]),
@ -166,8 +166,8 @@ export const permissionServiceFactory = ({
case ActorType.IDENTITY:
return getIdentityOrgPermission(id, orgId);
default:
throw new UnauthorizedError({
message: "Permission not defined",
throw new BadRequestError({
message: "Invalid actor provided",
name: "Get org permission"
});
}
@ -179,7 +179,7 @@ export const permissionServiceFactory = ({
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
if (isCustomRole) {
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 {
permission: buildOrgPermission([{ role: OrgMembershipRole.Custom, permissions: orgRole.permissions }]),
role: orgRole
@ -196,12 +196,12 @@ export const permissionServiceFactory = ({
userOrgId?: string
): Promise<TProjectPermissionRT<ActorType.USER>> => {
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 (
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.
@ -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.
// 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) {
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
const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
@ -223,8 +223,32 @@ export const permissionServiceFactory = ({
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 {
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
permission,
membership: userProjectPermission,
hasRole: (role: string) =>
userProjectPermission.roles.findIndex(
@ -239,18 +263,19 @@ export const permissionServiceFactory = ({
identityOrgId: string | undefined
): Promise<TProjectPermissionRT<ActorType.IDENTITY>> => {
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 (
identityProjectPermission.roles.some(
({ 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) {
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 =
@ -261,8 +286,32 @@ export const permissionServiceFactory = ({
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 {
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
permission,
membership: identityProjectPermission,
hasRole: (role: string) =>
identityProjectPermission.roles.findIndex(
@ -277,25 +326,23 @@ export const permissionServiceFactory = ({
actorOrgId: string | undefined
) => {
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);
if (!serviceTokenProject) throw new BadRequestError({ message: "Service token not linked to a project" });
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)
throw new UnauthorizedError({
message: "Failed to find service authorization for given project"
});
if (serviceToken.projectId !== projectId) {
throw new ForbiddenRequestError({ name: "Service token not a part of the specified project" });
}
if (serviceTokenProject.orgId !== actorOrgId)
throw new UnauthorizedError({
message: "Failed to find service authorization for given project"
});
if (serviceTokenProject.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Service token not a part of the specified organization" });
}
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
return {
@ -335,8 +382,8 @@ export const permissionServiceFactory = ({
case ActorType.IDENTITY:
return getIdentityProjectPermission(id, projectId, actorOrgId) as Promise<TProjectPermissionRT<T>>;
default:
throw new UnauthorizedError({
message: "Permission not defined",
throw new BadRequestError({
message: "Invalid actor provided",
name: "Get project permission"
});
}
@ -346,15 +393,23 @@ export const permissionServiceFactory = ({
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
if (isCustomRole) {
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 {
permission: buildProjectPermission([
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
]),
permission: createMongoAbility<ProjectPermissionSet>(rules, {
conditionsMatcher
}),
role: projectRole
};
}
return { permission: buildProjectPermission([{ role, permissions: [] }]) };
const rules = buildProjectPermissionRules([{ role, permissions: [] }]);
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
conditionsMatcher
});
return { permission };
};
return {
@ -365,6 +420,6 @@ export const permissionServiceFactory = ({
getOrgPermissionByRole,
getProjectPermissionByRole,
buildOrgPermission,
buildProjectPermission
buildProjectPermissionRules
};
};

View File

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

View File

@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability";
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 { TPermissionServiceFactory } from "../permission/permission-service";
@ -42,7 +42,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
...dto
}: TCreateUserPrivilegeDTO) => {
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(
actor,
@ -94,14 +94,14 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
...dto
}: TUpdateUserPrivilegeDTO) => {
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({
userId: userPrivilege.userId,
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(
actor,
@ -147,13 +147,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => {
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({
userId: userPrivilege.userId,
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(
actor,
@ -176,13 +176,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorAuthMethod
}: TGetUserPrivilegeDetailsDTO) => {
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({
userId: userPrivilege.userId,
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(
actor,
@ -204,7 +204,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorAuthMethod
}: TListUserPrivilegesDTO) => {
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(
actor,

View File

@ -19,10 +19,11 @@ import {
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} 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 { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
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 { TOrgDALFactory } from "@app/services/org/org-dal";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
@ -51,6 +52,8 @@ type TSamlConfigServiceFactoryDep = {
TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
>;
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
@ -71,7 +74,8 @@ export const samlConfigServiceFactory = ({
permissionService,
licenseService,
tokenService,
smtpService
smtpService,
identityMetadataDAL
}: TSamlConfigServiceFactoryDep) => {
const createSamlCfg = async ({
cert,
@ -187,7 +191,7 @@ export const samlConfigServiceFactory = ({
const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null };
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({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
@ -253,7 +257,7 @@ export const samlConfigServiceFactory = ({
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
if (dto.type === "org") {
@ -279,7 +283,7 @@ export const samlConfigServiceFactory = ({
} = ssoConfig;
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({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
@ -332,13 +336,14 @@ export const samlConfigServiceFactory = ({
lastName,
authProvider,
orgId,
relayState
relayState,
metadata
}: TSamlLoginDTO) => {
const appCfg = getConfig();
const serverCfg = await getServerCfg();
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.SAML)) {
throw new BadRequestError({
throw new ForbiddenRequestError({
message: "Login with SAML is disabled by administrator."
});
}
@ -350,7 +355,7 @@ export const samlConfigServiceFactory = ({
});
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;
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;
});
} 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;
});
}

View File

@ -53,4 +53,5 @@ export type TSamlLoginDTO = {
orgId: string;
// saml thingy
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 { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
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 { TOrgPermission } from "@app/lib/types";
import { AuthTokenType } from "@app/services/auth/auth-type";
@ -75,7 +75,14 @@ type TScimServiceFactoryDep = {
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
groupDAL: Pick<
TGroupDALFactory,
"create" | "findOne" | "findAllGroupMembers" | "delete" | "findGroups" | "transaction" | "updateById" | "update"
| "create"
| "findOne"
| "findAllGroupPossibleMembers"
| "delete"
| "findGroups"
| "transaction"
| "updateById"
| "update"
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
userGroupMembershipDAL: Pick<
@ -169,7 +176,7 @@ export const scimServiceFactory = ({
const deleteScimToken = async ({ scimTokenId, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteScimTokenDTO) => {
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(
actor,
@ -775,7 +782,7 @@ export const scimServiceFactory = ({
});
}
const users = await groupDAL.findAllGroupMembers({
const users = await groupDAL.findAllGroupPossibleMembers({
orgId: group.orgId,
groupId: group.id
});

View File

@ -1,33 +1,62 @@
import { Knex } from "knex";
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 { 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 const secretApprovalPolicyDALFactory = (db: TDbClient) => {
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)
// eslint-disable-next-line
.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`)
.leftJoin(
TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId`
)
.leftJoin(TableName.Users, `${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
.leftJoin(
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(
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
tx.ref("email").withSchema(TableName.Users).as("approverEmail"),
tx.ref("firstName").withSchema(TableName.Users).as("approverFirstName"),
tx.ref("lastName").withSchema(TableName.Users).as("approverLastName")
tx.ref("id").withSchema("secretApprovalPolicyApproverUser").as("approverUserId"),
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
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(
tx.ref("name").withSchema(TableName.Environment).as("envName"),
@ -55,11 +84,31 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
{
key: "approverUserId",
label: "userApprovers" as const,
mapper: ({ approverUserId, approverEmail, approverFirstName, approverLastName }) => ({
userId: approverUserId,
email: approverEmail,
firstName: approverFirstName,
lastName: approverLastName
mapper: ({
approverUserId: userId,
approverEmail: email,
approverFirstName: firstName,
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 {
const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter);
const formatedDoc = sqlNestRelationships({
data: docs,
key: "id",
@ -83,11 +138,35 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
...SecretApprovalPoliciesSchema.parse(data)
}),
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",
label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({
userId: approverUserId
mapper: ({ approverUserId: userId }) => ({
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 { 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 { containsGlobPatterns } from "@app/lib/picomatch";
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 { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
@ -15,6 +17,7 @@ import {
TCreateSapDTO,
TDeleteSapDTO,
TGetBoardSapDTO,
TGetSapByIdDTO,
TListSapDTO,
TUpdateSapDTO
} from "./secret-approval-policy-types";
@ -28,6 +31,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
userDAL: Pick<TUserDALFactory, "find">;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
@ -39,6 +43,7 @@ export const secretApprovalPolicyServiceFactory = ({
permissionService,
secretApprovalPolicyApproverDAL,
projectEnvDAL,
userDAL,
licenseService
}: TSecretApprovalPolicyServiceFactoryDep) => {
const createSecretApprovalPolicy = async ({
@ -54,7 +59,19 @@ export const secretApprovalPolicyServiceFactory = ({
environment,
enforcementLevel
}: 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" });
const { permission } = await permissionService.getProjectPermission(
@ -78,7 +95,7 @@ export const secretApprovalPolicyServiceFactory = ({
}
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 doc = await secretApprovalPolicyDAL.create(
@ -91,15 +108,48 @@ export const secretApprovalPolicyServiceFactory = ({
},
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(
approvers.map((approverUserId) => ({
userApproverIds.map((approverUserId) => ({
approverUserId,
policyId: doc.id
})),
tx
);
await secretApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((approverGroupId) => ({
approverGroupId,
policyId: doc.id
})),
tx
);
return doc;
});
return { ...secretApproval, environment: env, projectId };
};
@ -115,8 +165,20 @@ export const secretApprovalPolicyServiceFactory = ({
secretPolicyId,
enforcementLevel
}: 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);
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(
actor,
@ -146,16 +208,52 @@ export const secretApprovalPolicyServiceFactory = ({
},
tx
);
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
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(
approvers.map((approverUserId) => ({
userApproverIds.map((approverUserId) => ({
approverUserId,
policyId: doc.id
})),
tx
);
}
if (groupApprovers) {
await secretApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((approverGroupId) => ({
approverGroupId,
policyId: doc.id
})),
tx
);
}
return doc;
});
return {
@ -173,7 +271,7 @@ export const secretApprovalPolicyServiceFactory = ({
actorOrgId
}: TDeleteSapDTO) => {
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(
actor,
@ -222,7 +320,7 @@ export const secretApprovalPolicyServiceFactory = ({
const getSecretApprovalPolicy = async (projectId: string, environment: string, path: string) => {
const secretPath = removeTrailingSlash(path);
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 });
if (!policies.length) return;
@ -260,12 +358,41 @@ export const secretApprovalPolicyServiceFactory = ({
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 {
createSecretApprovalPolicy,
updateSecretApprovalPolicy,
deleteSecretApprovalPolicy,
getSecretApprovalPolicy,
getSecretApprovalPolicyByProjectId,
getSecretApprovalPolicyOfFolder
getSecretApprovalPolicyOfFolder,
getSecretApprovalPolicyById
};
};

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import {
TSecretApprovalRequestsSecrets,
TSecretTags
} 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";
export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApprovalRequestSecretDALFactory>;
@ -31,7 +31,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
);
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 [];

View File

@ -10,7 +10,7 @@ import {
} from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
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 { setKnexStringValue } from "@app/lib/knex";
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" });
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 { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
@ -222,7 +222,7 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalRequest.committerUserId !== 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;
@ -271,7 +271,7 @@ export const secretApprovalRequestServiceFactory = ({
: undefined
}));
} 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);
secrets = encrypedSecrets.map((el) => ({
...el,
@ -307,7 +307,7 @@ export const secretApprovalRequestServiceFactory = ({
actorOrgId
}: TReviewRequestDTO) => {
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" });
const plan = await licenseService.getPlan(actorOrgId);
@ -331,7 +331,7 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalRequest.committerUserId !== 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 review = await secretApprovalRequestReviewerDAL.findOne(
@ -365,7 +365,7 @@ export const secretApprovalRequestServiceFactory = ({
actorAuthMethod
}: TStatusChangeDTO) => {
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" });
const plan = await licenseService.getPlan(actorOrgId);
@ -389,7 +389,7 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalRequest.committerUserId !== 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" });
@ -414,7 +414,7 @@ export const secretApprovalRequestServiceFactory = ({
bypassReason
}: TMergeSecretApprovalRequestDTO) => {
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" });
const plan = await licenseService.getPlan(actorOrgId);
@ -439,7 +439,7 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalRequest.committerUserId !== 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>>(
(prev, curr) => ({ ...prev, [curr.userId.toString()]: curr.status as ApprovalStatus }),
@ -447,8 +447,8 @@ export const secretApprovalRequestServiceFactory = ({
);
const hasMinApproval =
secretApprovalRequest.policy.approvals <=
secretApprovalRequest.policy.approvers.filter(
({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
secretApprovalRequest.policy.approvers.filter(({ userId: approverId }) =>
approverId ? reviewers[approverId] === ApprovalStatus.APPROVED : false
).length;
const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
@ -462,7 +462,7 @@ export const secretApprovalRequestServiceFactory = ({
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2(
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({
type: KmsDataKey.SecretManager,
@ -602,7 +602,7 @@ export const secretApprovalRequestServiceFactory = ({
});
} else {
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 }> = [];
let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create);
@ -612,8 +612,8 @@ export const secretApprovalRequestServiceFactory = ({
secretDAL,
inputSecrets: secretCreationCommits.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) {
throw new BadRequestError({
message: "Missing secret blind index"
throw new NotFoundError({
message: "Secret blind index not found"
});
}
return { secretBlindIndex };
@ -639,8 +639,8 @@ export const secretApprovalRequestServiceFactory = ({
.filter(({ secretBlindIndex, secret }) => secret && secret.secretBlindIndex !== secretBlindIndex)
.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) {
throw new BadRequestError({
message: "Missing secret blind index"
throw new NotFoundError({
message: "Secret blind index not found"
});
}
return { secretBlindIndex };
@ -762,8 +762,8 @@ export const secretApprovalRequestServiceFactory = ({
secretQueueService,
inputSecrets: secretDeletionCommits.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) {
throw new BadRequestError({
message: "Missing secret blind index"
throw new NotFoundError({
message: "Secret blind index not found"
});
}
return { secretBlindIndex, type: SecretType.Shared };
@ -789,7 +789,7 @@ export const secretApprovalRequestServiceFactory = ({
await snapshotService.performSnapshot(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({
projectId,
secretPath: folder.path,
@ -805,7 +805,7 @@ export const secretApprovalRequestServiceFactory = ({
const requestedByUser = await userDAL.findOne({ id: actorId });
const approverUsers = await userDAL.find({
$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);
if (!folder)
throw new BadRequestError({
throw new NotFoundError({
message: "Folder not found for the given environment slug & secret path",
name: "GenSecretApproval"
});
const folderId = folder.id;
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 commitTagIds: Record<string, string[]> = {};
@ -961,7 +961,7 @@ export const secretApprovalRequestServiceFactory = ({
secretDAL
});
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;
});
const deletedSecretIds = deletedSecrets.map(
@ -972,7 +972,7 @@ export const secretApprovalRequestServiceFactory = ({
...deletedSecrets.map((el) => {
const secretId = secretsGroupedByBlindIndex[keyName2BlindIndex[el.secretName]][0].id;
if (!latestSecretVersions[secretId].secretBlindIndex)
throw new BadRequestError({ message: "Failed to find secret blind index" });
throw new NotFoundError({ message: "Secret blind index not found" });
return {
op: SecretOperations.Delete as const,
...latestSecretVersions[secretId],
@ -988,7 +988,7 @@ export const secretApprovalRequestServiceFactory = ({
const tagIds = unique(Object.values(commitTagIds).flat());
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 doc = await secretApprovalRequestDAL.create(
@ -1054,7 +1054,7 @@ export const secretApprovalRequestServiceFactory = ({
const commitsGroupByBlindIndex = groupBy(approvalCommits, (i) => {
if (!i.secretBlindIndex) {
throw new BadRequestError({ message: "Missing secret blind index" });
throw new NotFoundError({ message: "Secret blind index not found" });
}
return i.secretBlindIndex;
});
@ -1132,7 +1132,7 @@ export const secretApprovalRequestServiceFactory = ({
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
throw new BadRequestError({
throw new NotFoundError({
message: "Folder not found for the given environment slug & secret path",
name: "GenSecretApproval"
});
@ -1191,8 +1191,8 @@ export const secretApprovalRequestServiceFactory = ({
}))
);
if (secretsToUpdateStoredInDB.length !== secretsToUpdate.length)
throw new BadRequestError({
message: `Secret not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
throw new NotFoundError({
message: `Secret does not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
});
// now find any secret that needs to update its name
@ -1207,8 +1207,8 @@ export const secretApprovalRequestServiceFactory = ({
}))
);
if (secrets.length)
throw new BadRequestError({
message: `Secret not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
throw new NotFoundError({
message: `Secret does not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
});
}
@ -1267,8 +1267,8 @@ export const secretApprovalRequestServiceFactory = ({
}))
);
if (secretsToDeleteInDB.length !== deletedSecrets.length)
throw new BadRequestError({
message: `Secret not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
throw new NotFoundError({
message: `Secret does not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
});
const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key);
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 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 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 { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
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 { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
@ -295,7 +295,7 @@ export const secretReplicationServiceFactory = ({
const [destinationFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [
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({
parentId: destinationFolder.id,
@ -506,7 +506,7 @@ export const secretReplicationServiceFactory = ({
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
const sourceLocalSecrets = await secretDAL.find({ folderId: folder.id, type: SecretType.Shared });
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
@ -545,7 +545,7 @@ export const secretReplicationServiceFactory = ({
const [destinationFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [
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({
parentId: destinationFolder.id,

View File

@ -13,7 +13,7 @@ import {
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
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 { alphaNumericNanoId } from "@app/lib/nanoid";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
@ -332,7 +332,7 @@ export const secretRotationQueueFactory = ({
);
});
} 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 }) => ({
secretId,
value: encryptSymmetric128BitHexKeyUTF8(
@ -372,7 +372,7 @@ export const secretRotationQueueFactory = ({
);
await secretVersionDAL.insertMany(
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 {
...el,
secretId: id,

View File

@ -3,7 +3,7 @@ import Ajv from "ajv";
import { ProjectVersion } from "@app/db/schemas";
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 { TProjectDALFactory } from "@app/services/project/project-dal";
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);
if (!folder) throw new BadRequestError({ message: "Secret path not found" });
if (!folder) throw new NotFoundError({ message: "Secret path not found" });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
@ -108,14 +108,14 @@ export const secretRotationServiceFactory = ({
$in: { id: Object.values(outputs) }
});
if (selectedSecrets.length !== Object.values(outputs).length)
throw new BadRequestError({ message: "Secrets not found" });
throw new NotFoundError({ message: "Secrets not found" });
} else {
const selectedSecrets = await secretDAL.find({
folderId: folder.id,
$in: { id: Object.values(outputs) }
});
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);
@ -125,7 +125,7 @@ export const secretRotationServiceFactory = ({
});
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> = {};
Object.entries(inputs).forEach(([key, value]) => {
const { type } = selectedTemplate.template.inputs.properties[key];
@ -198,7 +198,7 @@ export const secretRotationServiceFactory = ({
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 });
return docs.map((el) => ({
...el,
@ -220,7 +220,7 @@ export const secretRotationServiceFactory = ({
const restartById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TRestartDTO) => {
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 plan = await licenseService.getPlan(project.orgId);
@ -244,7 +244,7 @@ export const secretRotationServiceFactory = ({
const deleteById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TDeleteDTO) => {
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(
actor,

View File

@ -7,7 +7,7 @@ import { ProbotOctokit } from "probot";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
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 { TGitAppInstallSessionDALFactory } from "./git-app-install-session-dal";
@ -63,7 +63,7 @@ export const secretScanningServiceFactory = ({
actorOrgId
}: TLinkInstallSessionDTO) => {
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(
actor,

View File

@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
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 { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@ -99,7 +99,7 @@ export const secretSnapshotServiceFactory = ({
);
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);
};
@ -131,7 +131,7 @@ export const secretSnapshotServiceFactory = ({
);
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"]] });
return snapshots;
@ -139,7 +139,7 @@ export const secretSnapshotServiceFactory = ({
const getSnapshotData = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetSnapshotDataDTO) => {
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(
actor,
actorId,
@ -173,7 +173,7 @@ export const secretSnapshotServiceFactory = ({
} else {
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id);
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 = {
...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
@ -225,7 +225,7 @@ export const secretSnapshotServiceFactory = ({
try {
if (!licenseService.isValidLicense) throw new InternalServerError({ message: "Invalid license" });
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;
if (shouldUseSecretV2Bridge) {
@ -309,7 +309,7 @@ export const secretSnapshotServiceFactory = ({
actorOrgId
}: TRollbackSnapshotDTO) => {
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 { permission } = await permissionService.getProjectPermission(

View File

@ -16,6 +16,9 @@ export const KeyStorePrefixes = {
WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-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) =>
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
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.",
username: "The username of the user to add to the group."
},
GET_BY_ID: {
id: "The id of the group to fetch"
},
DELETE_USER: {
id: "The id of the group to remove the user from.",
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.",
membershipId: "The ID of the membership to update.",
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: {
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.
*/
const extractPermissions = (ability: MongoAbility) =>
ability.rules.map((permission) => `${permission.action as string}_${permission.subject as string}`);
const extractPermissions = (ability: MongoAbility) => {
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.

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;
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");
this.name = name || "ForbideenError";
this.name = name || "ForbiddenError";
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);
};
/**
* 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;
};
export const prefixWithSlash = (str: string) => {
if (str.startsWith("/")) return str;
return `/${str}`;
};

View File

@ -1,6 +1,6 @@
import net from "node:net";
import { UnauthorizedError } from "../errors";
import { ForbiddenRequestError } from "../errors";
export enum IPType {
IPV4 = "ipv4",
@ -126,7 +126,7 @@ export const checkIPAgainstBlocklist = ({ ipAddress, trustedIps }: { ipAddress:
const check = blockList.check(ipAddress, type);
if (!check)
throw new UnauthorizedError({
message: "Failed to authenticate"
throw new ForbiddenRequestError({
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 { globalRateLimiterCfg } from "./config/rateLimiter";
import { addErrorsToResponseSchemas } from "./plugins/add-errors-to-response-schemas";
import { fastifyErrHandler } from "./plugins/error-handler";
import { registerExternalNextjs } from "./plugins/external-nextjs";
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
@ -75,6 +76,8 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
credentials: true,
origin: appCfg.SITE_URL || true
});
await server.register(addErrorsToResponseSchemas);
// pull ip based on various proxy headers
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: {}
};
} else {
throw new BadRequestError({ message: "Missing logic for other actor" });
throw new BadRequestError({ message: "Invalid actor type provided" });
}
req.auditLogInfo = payload;
});

View File

@ -5,7 +5,7 @@ import jwt, { JwtPayload } from "jsonwebtoken";
import { TServiceTokens, TUsers } from "@app/db/schemas";
import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types";
import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors";
import { BadRequestError } from "@app/lib/errors";
import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types";
@ -167,7 +167,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
break;
}
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 { UnauthorizedError } from "@app/lib/errors";
import { ForbiddenRequestError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type";
export const verifySuperAdmin = <T extends FastifyRequest>(
@ -9,9 +9,8 @@ export const verifySuperAdmin = <T extends FastifyRequest>(
done: HookHandlerDoneFunction
) => {
if (req.auth.actor !== ActorType.USER || !req.auth.user.superAdmin)
throw new UnauthorizedError({
name: "Unauthorized access",
message: "Requires superadmin access"
throw new ForbiddenRequestError({
message: "Requires elevated super admin privileges"
});
done();
};

View File

@ -1,6 +1,6 @@
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";
interface TAuthOptions {
@ -11,11 +11,11 @@ export const verifyAuth =
<T extends FastifyRequest>(authStrategies: AuthMode[], options: TAuthOptions = { requireOrg: true }) =>
(req: T, _res: FastifyReply, done: HookHandlerDoneFunction) => {
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);
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.

View File

@ -6,6 +6,7 @@ import { ZodError } from "zod";
import {
BadRequestError,
DatabaseError,
ForbiddenRequestError,
InternalServerError,
NotFoundError,
ScimRequestError,
@ -18,25 +19,50 @@ enum JWTErrors {
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) => {
server.setErrorHandler((error, req, res) => {
req.log.error(error);
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) {
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) {
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) {
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) {
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) {
void res.status(401).send({
statusCode: 401,
void res.status(HttpStatusCodes.Forbidden).send({
statusCode: HttpStatusCodes.Forbidden,
error: "PermissionDenied",
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) {
void res.status(error.status).send({
schemas: error.schemas,
@ -59,8 +85,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
return error.message;
})();
void res.status(401).send({
statusCode: 401,
void res.status(HttpStatusCodes.Forbidden).send({
statusCode: HttpStatusCodes.Forbidden,
error: "TokenError",
message
});

View File

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

View File

@ -27,6 +27,34 @@ export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({
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(
z.object({
environment: z.object({

View File

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

View File

@ -2,7 +2,7 @@ import jwt from "jsonwebtoken";
import { z } from "zod";
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
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 appCfg = getConfig();
if (!refreshToken)
throw new BadRequestError({
name: "Auth token route",
message: "Failed to find refresh token"
throw new NotFoundError({
name: "AuthTokenNotFound",
message: "Failed to find refresh token"
});
const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload;
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(
decodedToken.tokenVersionId,
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)
throw new UnauthorizedError({ message: "Invalid token", name: "Auth token route" });
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) {
throw new UnauthorizedError({
message: "Token version mismatch",
name: "InvalidToken"
});
}
const token = jwt.sign(
{

View File

@ -1,7 +1,9 @@
import { ForbiddenError, subject } from "@casl/ability";
import { z } from "zod";
import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
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 { BadRequestError } from "@app/lib/errors";
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 { 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) => {
server.route({
method: "GET",
@ -55,21 +71,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.orderDirection)
.optional(),
search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(),
includeSecrets: z.coerce
.boolean()
.optional()
.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)
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
}),
response: {
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
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
actor: req.permission.type,
@ -182,8 +209,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
projectId,
search,
environmentSlugs: environments,
path: secretPath
environmentSlugs: permissiveEnvs,
path: secretPath,
isInternal: true
});
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
@ -196,10 +224,11 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
search,
orderBy,
orderDirection,
environmentSlugs: environments,
environmentSlugs: permissiveEnvs,
path: secretPath,
limit: remainingLimit,
offset: adjustedOffset
offset: adjustedOffset,
isInternal: true
});
// 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
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environments,
environments: permissiveEnvs,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
search
search,
isInternal: true
});
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
@ -230,7 +260,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environments,
environments: permissiveEnvs,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
@ -238,10 +268,11 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
orderDirection,
search,
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;
if (secretCountFromEnv) {
@ -325,26 +356,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.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(),
includeSecrets: z.coerce
.boolean()
.optional()
.default(true)
.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)
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
includeImports: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports)
}),
response: {
200: z.object({
@ -498,56 +513,44 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}
}
if (includeDynamicSecrets) {
totalDynamicSecretCount = await server.services.dynamicSecret.getDynamicSecretCount({
actor: req.permission.type,
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({
try {
if (includeDynamicSecrets) {
totalDynamicSecretCount = await server.services.dynamicSecret.getDynamicSecretCount({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId,
search,
orderBy,
orderDirection,
environmentSlug: environment,
path: secretPath,
limit: remainingLimit,
offset: adjustedOffset
path: secretPath
});
remainingLimit -= dynamicSecrets.length;
adjustedOffset = 0;
} else {
adjustedOffset = Math.max(0, adjustedOffset - totalDynamicSecretCount);
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByEnv({
actor: req.permission.type,
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) {
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({
if (includeSecrets) {
totalSecretCount = await server.services.secret.getSecretsCount({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
@ -555,44 +558,62 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
orderBy,
orderDirection,
search,
limit: remainingLimit,
offset: adjustedOffset,
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({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secrets.length
}
}
});
secrets = secretsRaw.secrets;
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
await server.services.auditLog.createAuditLog({
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.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 {

View File

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

View File

@ -29,7 +29,11 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
body: z.object({
name: z.string().trim().describe(IDENTITIES.CREATE.name),
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: {
200: z.object({
@ -93,7 +97,11 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}),
body: z.object({
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: {
200: z.object({
@ -193,6 +201,14 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
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({
id: true,
name: true,

View File

@ -1,3 +1,5 @@
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-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(registerSecretSharingRouter, { prefix: "/secret-sharing" });
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 { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
import {} from "../sanitizedSchemas";
export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
@ -129,9 +131,9 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
.default("/")
.transform(removeTrailingSlash)
.describe(INTEGRATION.UPDATE.secretPath),
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment),
targetEnvironment: z.string().trim().optional().describe(INTEGRATION.UPDATE.targetEnvironment),
owner: z.string().trim().optional().describe(INTEGRATION.UPDATE.owner),
environment: z.string().trim().optional().describe(INTEGRATION.UPDATE.environment),
metadata: IntegrationMetadataSchema.optional()
}),
response: {

View File

@ -11,6 +11,8 @@ import {
} from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -26,7 +28,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
schema: {
response: {
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]),
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({
filter: {
...req.query,

View File

@ -3,7 +3,7 @@ import { z } from "zod";
import { SecretFoldersSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
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),
environment: z.string().trim().describe(FOLDERS.CREATE.environment),
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
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: {
200: z.object({
@ -86,9 +98,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId),
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
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
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: {
200: z.object({
@ -147,7 +171,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
id: z.string().describe(FOLDERS.UPDATE.folderId),
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
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()
.min(1)
@ -211,9 +241,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
body: z.object({
workspaceId: z.string().trim().describe(FOLDERS.DELETE.workspaceId),
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
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: {
200: z.object({
@ -267,9 +309,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
querystring: z.object({
workspaceId: z.string().trim().describe(FOLDERS.LIST.workspaceId),
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
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: {
200: z.object({

View File

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

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