Compare commits

...

107 Commits

Author SHA1 Message Date
Daniel Hougaard
787f8318fe updated locks 2024-10-03 23:50:53 +04:00
Daniel Hougaard
9a27873af5 requested changes 2024-10-03 23:50:53 +04:00
Daniel Hougaard
0abab57d83 fix: variable naming 2024-10-03 23:50:53 +04:00
Daniel Hougaard
d5662dfef4 feat: allow creation of multiple project envs 2024-10-03 23:50:53 +04:00
Daniel Hougaard
ee2ee48b47 Merge pull request #2528 from Infisical/meet/fix-mustache-import-error
fix: change mustache import
2024-10-03 23:30:18 +04:00
Daniel Hougaard
896d977b95 fixed typescript 2024-10-03 23:12:10 +04:00
Meet
d1966b60a8 fix: ldif module import 2024-10-04 00:19:25 +05:30
Daniel Hougaard
e3cbcf5853 Merge pull request #2526 from Infisical/daniel/integration-not-found-error
fix(api): integration not found error
2024-10-03 18:35:35 +04:00
Daniel Hougaard
bdf1f7c601 Update integration-service.ts 2024-10-03 18:30:17 +04:00
Daniel Hougaard
24b23d4f90 Merge pull request #2482 from Infisical/daniel/shorter-share-url
feat(secret-sharing): server-side encryption
2024-10-03 17:48:12 +04:00
Meet Shah
09c1a5f778 Merge pull request #2516 from Infisical/meet/eng-1610-ldap-like-engine-for-infisical
feat: add LDAP support for dynamic secrets
2024-10-03 16:59:55 +05:30
Meet
73a9cf01f3 feat: add better error message 2024-10-03 16:44:57 +05:30
Meet
97e860cf21 feat: add better error on invalid LDIF 2024-10-03 16:37:21 +05:30
Meet
25f694bbdb feat: Improve docs and add docs button 2024-10-03 09:56:27 +05:30
Maidul Islam
fd254fbeec Merge pull request #2484 from Infisical/daniel/fix-operator-crd-update
fix(k8-operator): updating CRD does not reflect in operator
2024-10-02 17:33:52 -04:00
Meet
859c556425 feat: Add documentation and refactor 2024-10-02 22:55:48 +05:30
Daniel Hougaard
a3cad030e5 Merge pull request #2522 from Infisical/daniel/integration-router-fixes
fix: made all update fields optional
2024-10-02 20:27:53 +04:00
Scott Wilson
342e9f99d3 Merge pull request #2519 from scott-ray-wilson/folder-navigation-filter-behavior
Improvement: Store and Clear Filters on Secret Dashboard Navigation
2024-10-02 09:21:14 -07:00
Daniel Hougaard
8ed04d0b75 fix: made all update fields optional 2024-10-02 20:09:31 +04:00
Meet
5b5a8ff03f chore: switch to bad request error 2024-10-02 21:20:42 +05:30
Meet
e0199084ad fix: refactor and handle modify 2024-10-02 20:51:02 +05:30
Scott Wilson
67a6deed72 Merge pull request #2521 from akhilmhdh/fix/create-identity
feat: added a default empty array for create-identity
2024-10-02 07:40:25 -07:00
=
355113e15d fix: changed least privilege check for identity for action array consideration 2024-10-02 19:52:27 +05:30
=
40c589eced fix: update not getting the tag in identity modal 2024-10-02 19:21:44 +05:30
=
ec4f175f73 feat: added a default empty array for create-identity 2024-10-02 19:06:02 +05:30
Tuan Dang
2273c21eb2 Clean PR 2024-10-02 09:10:22 -04:00
Daniel Hougaard
97c2b15e29 fix: secret sharing view count 2024-10-02 15:20:06 +04:00
Daniel Hougaard
2f90ee067b Merge pull request #2520 from Infisical/daniel/better-k8-auth-logs
fix(k8-auth): better errors
2024-10-02 14:27:37 +04:00
Daniel Hougaard
7b64288019 Update identity-kubernetes-auth-service.ts 2024-10-02 13:39:15 +04:00
Sheen
e6e1ed7ca9 Merge pull request #2512 from Infisical/feat/enforce-oidc-sso
feat: enforce oidc sso
2024-10-02 11:42:31 +08:00
Sheen Capadngan
73838190fd Merge remote-tracking branch 'origin/main' into feat/enforce-oidc-sso 2024-10-02 11:01:03 +08:00
Maidul Islam
d32fad87d1 Merge pull request #2485 from akhilmhdh/feat/permission-ui
New project permission ui
2024-10-01 15:24:55 -04:00
=
67db9679fa feat: removed not needed tooltip 2024-10-02 00:39:45 +05:30
=
3edd48a8b3 feat: updated plus button 2024-10-02 00:39:45 +05:30
=
a4091bfcdd feat: removed console in test 2024-10-02 00:39:44 +05:30
=
24483631a0 feat: removed discard icon 2024-10-02 00:39:44 +05:30
=
0f74a1a011 feat: updated layout and fixed item not getting removed 2024-10-02 00:39:44 +05:30
=
62d6e3763b feat: added validation to check dedupe operators, loading indicator, string required rhs 2024-10-02 00:39:44 +05:30
=
39ea7a032f feat: added empty state for empty policy 2024-10-02 00:39:44 +05:30
=
3ac125f9c7 feat: fixed test, resolved another edgecase in dashboard and added label to conditions in secrets 2024-10-02 00:39:44 +05:30
=
7667a7e665 feat: resolved review comments: metadata overflow, save not working on first policy etc 2024-10-02 00:39:44 +05:30
=
d7499fc5c5 feat: removed console from overview 2024-10-02 00:39:43 +05:30
=
f6885b239b feat: small text changes in kms permission 2024-10-02 00:39:43 +05:30
=
4928322cdb feat: added saml parsing attributes and injecting to metadata of a user in org scoped 2024-10-02 00:39:43 +05:30
=
77e191d63e feat: implemented ui and api for managing user,identity metadata 2024-10-02 00:39:43 +05:30
=
15c98a1d2e feat: added template based permission 2024-10-02 00:39:43 +05:30
=
ed757bdeff fix: broken import due to merge conflict fix 2024-10-02 00:39:43 +05:30
=
65241ad8bf feat: updated backend permission request definition 2024-10-02 00:39:43 +05:30
=
6a7760f33f feat: updated ui for new permission 2024-10-02 00:39:42 +05:30
Sheen Capadngan
fdc62e21ef misc: addressed review comments 2024-10-02 02:10:46 +08:00
Sheen Capadngan
32f866f834 Merge remote-tracking branch 'origin/main' into feat/enforce-oidc-sso 2024-10-02 02:06:39 +08:00
Scott Wilson
fbf52850e8 feature: clear filters when navigating down and restore filters when navigating up folders in secrets dashboard 2024-10-01 09:26:25 -07:00
Maidul Islam
ab9b207f96 Merge pull request #2477 from meetcshah19/meet/eng-1519-allow-users-to-change-auth-method-in-the-ui-easily
feat: allow users to replace auth methods
2024-09-30 23:38:02 -04:00
Maidul Islam
5532b9cfea Merge pull request #2518 from akhilmhdh/fix/ui-select-long-text
feat: increase select width in org access control page and added overflow bounding for select
2024-09-30 22:47:55 -04:00
Maidul Islam
449d3f0304 Merge pull request #2490 from Infisical/meet/eng-1588-auto-migration-from-envkey
feat: add migration service to import from envkey
2024-09-30 21:48:53 -04:00
Daniel Hougaard
f0210c2607 feat: fixed UI and added permissions check to backend 2024-10-01 05:17:46 +04:00
Scott Wilson
ad88aaf17f fix: address changes 2024-09-30 16:53:42 -07:00
Daniel Hougaard
0485b56e8d fix: improvements 2024-10-01 03:51:55 +04:00
Daniel Hougaard
b65842f5c1 fix: requested changes 2024-10-01 00:16:18 +04:00
Meet
22b6e0afcd chore: refactor 2024-10-01 01:34:24 +05:30
Meet
b0e536e576 fix: improve UI and lint fix 2024-10-01 01:34:24 +05:30
Meet
54e4314e88 feat: add documentation 2024-10-01 01:34:24 +05:30
Meet
d00b1847cc feat: add UI for migration from EnvKey 2024-10-01 01:34:24 +05:30
Meet
be02617855 feat: add migration service to import from envkey 2024-10-01 01:34:18 +05:30
=
b5065f13c9 feat: increase select width in org access control page and added overflow bounding for select 2024-10-01 00:35:11 +05:30
Maidul Islam
659b6d5d19 Merge pull request #2515 from scott-ray-wilson/region-select
Feature: Add Data Region Select
2024-09-30 14:56:47 -04:00
Daniel Hougaard
9c33251c44 Update secret-sharing-service.ts 2024-09-30 22:51:42 +04:00
Daniel Hougaard
1a0896475c fix: added new identifier field for non-uuid IDs 2024-09-30 22:51:42 +04:00
Daniel Hougaard
7e820745a4 Update 20240930134623_secret-sharing-string-id.ts 2024-09-30 22:51:02 +04:00
Daniel Hougaard
fa63c150dd requested changes 2024-09-30 22:51:02 +04:00
Daniel Hougaard
1a2495a95c fix: improved root kms encryption methods 2024-09-30 22:51:02 +04:00
Daniel Hougaard
d79099946a feat(secret-sharing): server-side encryption 2024-09-30 22:51:02 +04:00
Meet
27afad583b fix: missed file 2024-10-01 00:03:47 +05:30
Maidul Islam
acde0867a0 Merge pull request #2517 from Infisical/revert-2505-revert-2494-daniel/api-errors
feat(api): better errors and documentation
2024-09-30 14:21:59 -04:00
Daniel Hougaard
d44f99bac2 Merge branch 'revert-2505-revert-2494-daniel/api-errors' of https://github.com/Infisical/infisical into revert-2505-revert-2494-daniel/api-errors 2024-09-30 22:16:32 +04:00
Daniel Hougaard
2b35e20b1d chore: rolled back bot not found errors 2024-09-30 22:16:00 +04:00
Scott Wilson
da15957c3f Merge pull request #2507 from scott-ray-wilson/integration-sync-retry-fix
Fix: Integration Sync Retry on Error Patch
2024-09-30 11:12:54 -07:00
Meet Shah
208fc3452d Merge pull request #2504 from meetcshah19/meet/add-column-exists-check
fix: check if column exists in migration
2024-09-30 23:42:22 +05:30
Maidul Islam
ba1db870a4 Merge pull request #2502 from Infisical/daniel/error-fixes
fix(api): error improvements
2024-09-30 13:51:03 -04:00
Scott Wilson
0741058c1d Merge pull request #2498 from scott-ray-wilson/various-ui-improvements
Fix: Various UI Improvements, Fixes and Backend Refactoring
2024-09-30 10:19:25 -07:00
Maidul Islam
3a6e79c575 Revert "Revert "feat(api): better errors and documentation"" 2024-09-30 12:58:57 -04:00
Scott Wilson
70aa73482e fix: only display region select for cloud 2024-09-30 09:58:49 -07:00
Scott Wilson
2fa30bdd0e improvement: add info about migrating regions 2024-09-30 07:08:33 -07:00
Scott Wilson
b28fe30bba chore: add region select component 2024-09-30 07:05:23 -07:00
Scott Wilson
9ba39e99c6 feature: add region select to login/signup and improve login layout 2024-09-30 07:03:02 -07:00
Meet
0e6aed7497 feat: add LDAP support for dynamic secrets 2024-09-30 19:32:24 +05:30
Sheen
7e11fbe7a3 Merge pull request #2501 from Infisical/misc/added-proper-notif-for-changes-with-policies
misc: added proper notifs for paths with policies in overview
2024-09-30 21:15:18 +08:00
Sheen Capadngan
23abab987f feat: enforce oidc sso 2024-09-30 20:59:48 +08:00
Scott Wilson
a44b3efeb7 fix: allow errors to propogate in integration sync to facilitate retries unless final attempt 2024-09-27 17:02:20 -07:00
Meet
1992a09ac2 chore: lint fix 2024-09-28 03:20:02 +05:30
Maidul Islam
efa54e0c46 Merge pull request #2506 from Infisical/maidul-wdjhwedj
remove health checks for rds and redis
2024-09-27 17:31:19 -04:00
Maidul Islam
bde2d5e0a6 Merge pull request #2505 from Infisical/revert-2494-daniel/api-errors
Revert "feat(api): better errors and documentation"
2024-09-27 17:26:01 -04:00
Maidul Islam
4090c894fc Revert "feat(api): better errors and documentation" 2024-09-27 17:25:11 -04:00
Maidul Islam
221bde01f8 remove health checks for rds and redis 2024-09-27 17:24:09 -04:00
Meet
b191a3c2f4 fix: check if column exists in migration 2024-09-28 02:35:10 +05:30
Scott Wilson
e7f1980b80 improvement: switch slug to use badge 2024-09-27 09:46:16 -07:00
Scott Wilson
cd09f03f0b chore: swap to boolean cast instead of !! 2024-09-27 07:19:57 -07:00
Sheen Capadngan
bc475e0f08 misc: added proper notifs for paths with policies in overview 2024-09-27 22:18:47 +08:00
Scott Wilson
afd6dd5257 improvement: improve query param boolean handling for dashboard queries and move dashboard router to v1 2024-09-26 17:50:57 -07:00
Scott Wilson
3a43d7c5d5 improvement: add tooltip to secret table resource count and match secret icon color 2024-09-26 16:40:33 -07:00
Scott Wilson
65375886bd fix: handle overflow on dropdown content 2024-09-26 16:22:41 -07:00
Scott Wilson
8495107849 improvement: display slug for aws regions 2024-09-26 16:14:23 -07:00
Daniel Hougaard
1fcfab7efa feat: remove finalizers 2024-09-26 02:40:30 +04:00
Daniel Hougaard
499334eef1 fixed finalizers 2024-09-26 02:35:16 +04:00
Daniel Hougaard
9fd76b8729 chore: updated helm 2024-09-25 18:29:55 +04:00
Daniel Hougaard
80d450e980 fix(k8-operator): updating CRD does not reflect in operator 2024-09-25 18:26:50 +04:00
Meet
f63c6b725b feat: allow users to replace auth methods 2024-09-24 21:07:43 +05:30
177 changed files with 7614 additions and 3358 deletions

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -3,34 +3,74 @@ import { Knex } from "knex";
import { TableName } from "../schemas"; import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> { 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)) { if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
// add column approverGroupId to AccessApprovalPolicyApprover
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => { await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
// make nullable // add column approverGroupId to AccessApprovalPolicyApprover
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE"); if (!hasAccessApproverGroupId) {
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
}
// make approverUserId nullable // make approverUserId nullable
table.uuid("approverUserId").nullable().alter(); if (hasAccessApproverUserId) {
table.uuid("approverUserId").nullable().alter();
}
}); });
// add column approverGroupId to SecretApprovalPolicyApprover
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => { await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
table.uuid("approverGroupId").references("id").inTable(TableName.Groups).onDelete("CASCADE"); // add column approverGroupId to SecretApprovalPolicyApprover
table.uuid("approverUserId").nullable().alter(); 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> { 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)) { if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
// remove
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => { await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
table.dropColumn("approverGroupId"); if (hasAccessApproverGroupId) {
table.uuid("approverUserId").notNullable().alter(); table.dropColumn("approverGroupId");
}
// make approverUserId not nullable
if (hasAccessApproverUserId) {
table.uuid("approverUserId").notNullable().alter();
}
}); });
// remove // remove
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => { await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
table.dropColumn("approverGroupId"); if (hasSecretApproverGroupId) {
table.uuid("approverUserId").notNullable().alter(); 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

@@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const IdentityMetadataSchema = z.object({
id: z.string().uuid(),
key: z.string(),
value: z.string(),
orgId: z.string().uuid(),
userId: z.string().uuid().nullable().optional(),
identityId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TIdentityMetadata = z.infer<typeof IdentityMetadataSchema>;
export type TIdentityMetadataInsert = Omit<z.input<typeof IdentityMetadataSchema>, TImmutableDBKeys>;
export type TIdentityMetadataUpdate = Partial<Omit<z.input<typeof IdentityMetadataSchema>, TImmutableDBKeys>>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,18 +34,12 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
// TODO(akhilmhdh): move this to more centralized // TODO(akhilmhdh): move this to more centralized
export const UnpackedPermissionSchema = z.object({ export const UnpackedPermissionSchema = z.object({
subject: z.union([z.string().min(1), z.string().array()]).optional(), subject: z
action: z.union([z.string().min(1), z.string().array()]), .union([z.string().min(1), z.string().array()])
conditions: z .transform((el) => (typeof el !== "string" ? el[0] : el))
.object({ .optional(),
environment: z.string().optional(), action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
secretPath: z conditions: z.unknown().optional()
.object({
$glob: z.string().min(1)
})
.optional()
})
.optional()
}); });
const unpackPermissions = (permissions: unknown) => const unpackPermissions = (permissions: unknown) =>

View File

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

View File

@@ -314,6 +314,8 @@ export const oidcConfigServiceFactory = ({
} }
); );
await oidcConfigDAL.update({ orgId }, { lastUsed: new Date() });
if (user.email && !user.isEmailVerified) { if (user.email && !user.isEmailVerified) {
const token = await tokenService.createTokenForUser({ const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION, type: TokenType.TOKEN_EMAIL_VERIFICATION,
@@ -395,7 +397,8 @@ export const oidcConfigServiceFactory = ({
tokenEndpoint, tokenEndpoint,
userinfoEndpoint, userinfoEndpoint,
jwksUri, jwksUri,
isActive isActive,
lastUsed: null
}; };
if (clientId !== undefined) { if (clientId !== undefined) {
@@ -418,6 +421,7 @@ export const oidcConfigServiceFactory = ({
} }
const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery); const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery);
await orgDAL.updateById(org.id, { authEnforced: false, scimEnabled: false });
return ssoConfig; return ssoConfig;
}; };

View File

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

View File

@@ -14,14 +14,19 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
].includes(actorAuthMethod); ].includes(actorAuthMethod);
} }
function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) { function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"]) {
if (actorAuthMethod === undefined) { if (actorAuthMethod === undefined) {
throw new UnauthorizedError({ name: "No auth method defined" }); throw new UnauthorizedError({ name: "No auth method defined" });
} }
if (isSamlEnforced && actorAuthMethod !== null && !isAuthMethodSaml(actorAuthMethod)) { if (
throw new ForbiddenRequestError({ name: "SAML auth enforced, cannot access org-scoped resource" }); isOrgSsoEnforced &&
actorAuthMethod !== null &&
!isAuthMethodSaml(actorAuthMethod) &&
actorAuthMethod !== AuthMethod.OIDC
) {
throw new ForbiddenRequestError({ name: "Org auth enforced. Cannot access org-scoped resource" });
} }
} }
export { isAuthMethodSaml, validateOrgSAML }; export { isAuthMethodSaml, validateOrgSSO };

View File

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

View File

@@ -1,6 +1,7 @@
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability"; import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, unpackRules } from "@casl/ability/extra"; import { PackRule, unpackRules } from "@casl/ability/extra";
import { MongoQuery } from "@ucast/mongo2js"; import { MongoQuery } from "@ucast/mongo2js";
import handlebars from "handlebars";
import { import {
OrgMembershipRole, OrgMembershipRole,
@@ -11,6 +12,7 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { conditionsMatcher } from "@app/lib/casl"; import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { objectify } from "@app/lib/fn";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -19,8 +21,8 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission"; import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
import { TPermissionDALFactory } from "./permission-dal"; import { TPermissionDALFactory } from "./permission-dal";
import { validateOrgSAML } from "./permission-fns"; import { validateOrgSSO } from "./permission-fns";
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-types"; import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types";
import { import {
buildServiceTokenProjectPermission, buildServiceTokenProjectPermission,
projectAdminPermissions, projectAdminPermissions,
@@ -72,7 +74,7 @@ export const permissionServiceFactory = ({
}); });
}; };
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => { const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
const rules = projectUserRoles const rules = projectUserRoles
.map(({ role, permissions }) => { .map(({ role, permissions }) => {
switch (role) { switch (role) {
@@ -98,9 +100,7 @@ export const permissionServiceFactory = ({
}) })
.reduce((curr, prev) => prev.concat(curr), []); .reduce((curr, prev) => prev.concat(curr), []);
return createMongoAbility<ProjectPermissionSet>(rules, { return rules;
conditionsMatcher
});
}; };
/* /*
@@ -130,7 +130,7 @@ export const permissionServiceFactory = ({
throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
} }
validateOrgSAML(authMethod, membership.orgAuthEnforced); validateOrgSSO(authMethod, membership.orgAuthEnforced);
const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat( const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat(
membership?.groups?.map(({ role, customRolePermission }) => ({ membership?.groups?.map(({ role, customRolePermission }) => ({
@@ -213,7 +213,7 @@ export const permissionServiceFactory = ({
throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
} }
validateOrgSAML(authMethod, userProjectPermission.orgAuthEnforced); validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced);
// join two permissions and pass to build the final permission set // join two permissions and pass to build the final permission set
const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
@@ -223,8 +223,32 @@ export const permissionServiceFactory = ({
permissions permissions
})) || []; })) || [];
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true });
const metadataKeyValuePair = objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
);
const interpolateRules = templatedRules(
{
identity: {
id: userProjectPermission.userId,
username: userProjectPermission.username,
metadata: metadataKeyValuePair
}
},
{ data: false }
);
const permission = createMongoAbility<ProjectPermissionSet>(
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
{
conditionsMatcher
}
);
return { return {
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)), permission,
membership: userProjectPermission, membership: userProjectPermission,
hasRole: (role: string) => hasRole: (role: string) =>
userProjectPermission.roles.findIndex( userProjectPermission.roles.findIndex(
@@ -262,8 +286,32 @@ export const permissionServiceFactory = ({
permissions permissions
})) || []; })) || [];
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true });
const metadataKeyValuePair = objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
);
const interpolateRules = templatedRules(
{
identity: {
id: identityProjectPermission.identityId,
username: identityProjectPermission.username,
metadata: metadataKeyValuePair
}
},
{ data: false }
);
const permission = createMongoAbility<ProjectPermissionSet>(
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
{
conditionsMatcher
}
);
return { return {
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)), permission,
membership: identityProjectPermission, membership: identityProjectPermission,
hasRole: (role: string) => hasRole: (role: string) =>
identityProjectPermission.roles.findIndex( identityProjectPermission.roles.findIndex(
@@ -346,14 +394,22 @@ export const permissionServiceFactory = ({
if (isCustomRole) { if (isCustomRole) {
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId }); const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
if (!projectRole) throw new NotFoundError({ message: `Specified role was not found: ${role}` }); if (!projectRole) throw new NotFoundError({ message: `Specified role was not found: ${role}` });
const rules = buildProjectPermissionRules([
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
]);
return { return {
permission: buildProjectPermission([ permission: createMongoAbility<ProjectPermissionSet>(rules, {
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions } conditionsMatcher
]), }),
role: projectRole role: projectRole
}; };
} }
return { permission: buildProjectPermission([{ role, permissions: [] }]) };
const rules = buildProjectPermissionRules([{ role, permissions: [] }]);
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
conditionsMatcher
});
return { permission };
}; };
return { return {
@@ -364,6 +420,6 @@ export const permissionServiceFactory = ({
getOrgPermissionByRole, getOrgPermissionByRole,
getProjectPermissionByRole, getProjectPermissionByRole,
buildOrgPermission, buildOrgPermission,
buildProjectPermission buildProjectPermissionRules
}; };
}; };

View File

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

View File

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

View File

@@ -23,6 +23,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
import { AuthTokenType } from "@app/services/auth/auth-type"; import { AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
@@ -51,6 +52,8 @@ type TSamlConfigServiceFactoryDep = {
TOrgDALFactory, TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
>; >;
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">; orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
@@ -71,7 +74,8 @@ export const samlConfigServiceFactory = ({
permissionService, permissionService,
licenseService, licenseService,
tokenService, tokenService,
smtpService smtpService,
identityMetadataDAL
}: TSamlConfigServiceFactoryDep) => { }: TSamlConfigServiceFactoryDep) => {
const createSamlCfg = async ({ const createSamlCfg = async ({
cert, cert,
@@ -332,7 +336,8 @@ export const samlConfigServiceFactory = ({
lastName, lastName,
authProvider, authProvider,
orgId, orgId,
relayState relayState,
metadata
}: TSamlLoginDTO) => { }: TSamlLoginDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
@@ -386,6 +391,21 @@ export const samlConfigServiceFactory = ({
); );
} }
if (metadata && foundUser.id) {
await identityMetadataDAL.delete({ userId: foundUser.id, orgId }, tx);
if (metadata.length) {
await identityMetadataDAL.insertMany(
metadata.map(({ key, value }) => ({
userId: foundUser.id,
orgId,
key,
value
})),
tx
);
}
}
return foundUser; return foundUser;
}); });
} else { } else {
@@ -474,6 +494,20 @@ export const samlConfigServiceFactory = ({
); );
} }
if (metadata && newUser.id) {
await identityMetadataDAL.delete({ userId: newUser.id, orgId }, tx);
if (metadata.length) {
await identityMetadataDAL.insertMany(
metadata.map(({ key, value }) => ({
userId: newUser?.id,
orgId,
key,
value
})),
tx
);
}
}
return newUser; return newUser;
}); });
} }

View File

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

View File

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

View File

@@ -360,7 +360,11 @@ export const ORGANIZATIONS = {
organizationId: "The ID of the organization to update the membership for.", organizationId: "The ID of the organization to update the membership for.",
membershipId: "The ID of the membership to update.", membershipId: "The ID of the membership to update.",
role: "The new role of the membership.", role: "The new role of the membership.",
isActive: "The active status of the membership" isActive: "The active status of the membership",
metadata: {
key: "The key for user metadata tag.",
value: "The value for user metadata tag."
}
}, },
DELETE_USER_MEMBERSHIP: { DELETE_USER_MEMBERSHIP: {
organizationId: "The ID of the organization to delete the membership from.", organizationId: "The ID of the organization to delete the membership from.",

View File

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

View File

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

View File

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

View File

@@ -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

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

View File

@@ -17,6 +17,20 @@ import { AuthMode } from "@app/services/auth/auth-type";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
// handle querystring boolean values
const booleanSchema = z
.union([z.boolean(), z.string().trim()])
.transform((value) => {
if (typeof value === "string") {
// ie if not empty, 0 or false, return true
return Boolean(value) && Number(value) !== 0 && value.toLowerCase() !== "false";
}
return value;
})
.optional()
.default(true);
export const registerDashboardRouter = async (server: FastifyZodProvider) => { export const registerDashboardRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
@@ -57,21 +71,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.orderDirection) .describe(DASHBOARD.SECRET_OVERVIEW_LIST.orderDirection)
.optional(), .optional(),
search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(), search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(),
includeSecrets: z.coerce includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
.boolean() includeFolders: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
.optional() includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
.default(true)
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
includeFolders: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
includeDynamicSecrets: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -198,7 +200,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
) )
); );
if (includeDynamicSecrets) { if (includeDynamicSecrets && permissiveEnvs.length) {
// this is the unique count, ie duplicate secrets across envs only count as 1 // this is the unique count, ie duplicate secrets across envs only count as 1
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({ totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
actor: req.permission.type, actor: req.permission.type,
@@ -239,7 +241,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
} }
if (includeSecrets) { if (includeSecrets && permissiveEnvs.length) {
// this is the unique count, ie duplicate secrets across envs only count as 1 // this is the unique count, ie duplicate secrets across envs only count as 1
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({ totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
actorId: req.permission.id, actorId: req.permission.id,
@@ -354,26 +356,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(), .optional(),
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(), search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(), tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
includeSecrets: z.coerce includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
.boolean() includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
.optional() includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
.default(true) includeImports: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports)
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
includeFolders: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
includeDynamicSecrets: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
includeImports: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports)
}), }),
response: { response: {
200: z.object({ 200: z.object({

View File

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

View File

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

View File

@@ -131,9 +131,9 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
.default("/") .default("/")
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(INTEGRATION.UPDATE.secretPath), .describe(INTEGRATION.UPDATE.secretPath),
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment), targetEnvironment: z.string().trim().optional().describe(INTEGRATION.UPDATE.targetEnvironment),
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner), owner: z.string().trim().optional().describe(INTEGRATION.UPDATE.owner),
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment), environment: z.string().trim().optional().describe(INTEGRATION.UPDATE.environment),
metadata: IntegrationMetadataSchema.optional() metadata: IntegrationMetadataSchema.optional()
}), }),
response: { response: {

View File

@@ -28,7 +28,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
response: { response: {
200: z.object({ 200: z.object({
organizations: OrganizationsSchema.array() organizations: OrganizationsSchema.extend({
orgAuthMethod: z.string()
}).array()
}) })
} }
}, },

View File

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

View File

@@ -130,18 +130,24 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
membership: OrgMembershipsSchema.merge( membership: OrgMembershipsSchema.extend({
z.object({ metadata: z
user: UsersSchema.pick({ .object({
username: true, key: z.string().trim().min(1),
email: true, id: z.string().trim().min(1),
isEmailVerified: true, value: z.string().trim().min(1)
firstName: true, })
lastName: true, .array()
id: true .optional(),
}).merge(z.object({ publicKey: z.string().nullable() })) user: UsersSchema.pick({
}) username: true,
).omit({ createdAt: true, updatedAt: true }) email: true,
isEmailVerified: true,
firstName: true,
lastName: true,
id: true
}).extend({ publicKey: z.string().nullable() })
}).omit({ createdAt: true, updatedAt: true })
}) })
} }
}, },
@@ -178,7 +184,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}), }),
body: z.object({ body: z.object({
role: z.string().trim().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.role), role: z.string().trim().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.role),
isActive: z.boolean().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.isActive) isActive: z.boolean().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.isActive),
metadata: z
.object({
key: z.string().trim().min(1).describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.metadata.key),
value: z.string().trim().min(1).describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.metadata.value)
})
.array()
.optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({

View File

@@ -0,0 +1,35 @@
import { z } from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerExternalMigrationRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/env-key",
config: {
rateLimit: readLimit
},
schema: {
body: z.object({
decryptionKey: z.string().trim().min(1),
encryptedJson: z.object({
nonce: z.string().trim().min(1),
data: z.string().trim().min(1)
})
})
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
await server.services.migration.importEnvKeyData({
decryptionKey: req.body.decryptionKey,
encryptedJson: req.body.encryptedJson,
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
}
});
};

View File

@@ -1,4 +1,4 @@
import { registerDashboardRouter } from "./dashboard-router"; import { registerExternalMigrationRouter } from "./external-migration-router";
import { registerLoginRouter } from "./login-router"; import { registerLoginRouter } from "./login-router";
import { registerSecretBlindIndexRouter } from "./secret-blind-index-router"; import { registerSecretBlindIndexRouter } from "./secret-blind-index-router";
import { registerSecretRouter } from "./secret-router"; import { registerSecretRouter } from "./secret-router";
@@ -11,5 +11,5 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerUserRouter, { prefix: "/users" });
await server.register(registerSecretRouter, { prefix: "/secrets" }); await server.register(registerSecretRouter, { prefix: "/secrets" });
await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" }); await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" });
await server.register(registerDashboardRouter, { prefix: "/dashboard" }); await server.register(registerExternalMigrationRouter, { prefix: "/migrate" });
}; };

View File

@@ -0,0 +1,197 @@
import slugify from "@sindresorhus/slugify";
import { randomUUID } from "crypto";
import sjcl from "sjcl";
import tweetnacl from "tweetnacl";
import tweetnaclUtil from "tweetnacl-util";
import { OrgMembershipRole, ProjectMembershipRole, SecretType } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgServiceFactory } from "../org/org-service";
import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TSecretServiceFactory } from "../secret/secret-service";
import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "./external-migration-types";
export type TImportDataIntoInfisicalDTO = {
projectService: TProjectServiceFactory;
orgService: TOrgServiceFactory;
projectEnvService: TProjectEnvServiceFactory;
secretService: TSecretServiceFactory;
input: TImportInfisicalDataCreate;
};
const { codec, hash } = sjcl;
const { secretbox } = tweetnacl;
export const decryptEnvKeyDataFn = async (decryptionKey: string, encryptedJson: { nonce: string; data: string }) => {
const key = tweetnaclUtil.decodeBase64(codec.base64.fromBits(hash.sha256.hash(decryptionKey)));
const nonce = tweetnaclUtil.decodeBase64(encryptedJson.nonce);
const encryptedData = tweetnaclUtil.decodeBase64(encryptedJson.data);
const decrypted = secretbox.open(encryptedData, nonce, key);
if (!decrypted) {
throw new BadRequestError({ message: "Decryption failed, please check the entered encryption key" });
}
const decryptedJson = tweetnaclUtil.encodeUTF8(decrypted);
return decryptedJson;
};
export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<InfisicalImportData> => {
const parsedJson: TEnvKeyExportJSON = JSON.parse(decryptedJson) as TEnvKeyExportJSON;
const infisicalImportData: InfisicalImportData = {
projects: new Map<string, { name: string; id: string }>(),
environments: new Map<string, { name: string; id: string; projectId: string }>(),
secrets: new Map<string, { name: string; id: string; projectId: string; environmentId: string; value: string }>()
};
parsedJson.apps.forEach((app: { name: string; id: string }) => {
infisicalImportData.projects.set(app.id, { name: app.name, id: app.id });
});
// string to string map for env templates
const envTemplates = new Map<string, string>();
for (const env of parsedJson.defaultEnvironmentRoles) {
envTemplates.set(env.id, env.defaultName);
}
// environments
for (const env of parsedJson.baseEnvironments) {
infisicalImportData.environments?.set(env.id, {
id: env.id,
name: envTemplates.get(env.environmentRoleId)!,
projectId: env.envParentId
});
}
// secrets
for (const env of Object.keys(parsedJson.envs)) {
if (!env.includes("|")) {
const envData = parsedJson.envs[env];
for (const secret of Object.keys(envData.variables)) {
const id = randomUUID();
infisicalImportData.secrets?.set(id, {
id,
name: secret,
environmentId: env,
value: envData.variables[secret].val
});
}
}
}
return infisicalImportData;
};
export const importDataIntoInfisicalFn = async ({
projectService,
orgService,
projectEnvService,
secretService,
input: { data, actor, actorId, actorOrgId, actorAuthMethod }
}: TImportDataIntoInfisicalDTO) => {
// Import data to infisical
if (!data || !data.projects) {
throw new BadRequestError({ message: "No projects found in data" });
}
const originalToNewProjectId = new Map<string, string>();
const originalToNewEnvironmentId = new Map<string, string>();
for await (const [id, project] of data.projects) {
const newProject = await projectService
.createProject({
actor,
actorId,
actorOrgId,
actorAuthMethod,
workspaceName: project.name,
createDefaultEnvs: false
})
.catch(() => {
throw new BadRequestError({ message: `Failed to import to project [name:${project.name}] [id:${id}]` });
});
originalToNewProjectId.set(project.id, newProject.id);
}
// Invite user importing projects
const invites = await orgService.inviteUserToOrganization({
actorAuthMethod,
actorId,
actorOrgId,
actor,
inviteeEmails: [],
orgId: actorOrgId,
organizationRoleSlug: OrgMembershipRole.NoAccess,
projects: Array.from(originalToNewProjectId.values()).map((project) => ({
id: project,
projectRoleSlug: [ProjectMembershipRole.Member]
}))
});
if (!invites) {
throw new BadRequestError({ message: `Failed to invite user to projects: [userId:${actorId}]` });
}
// Import environments
if (data.environments) {
for await (const [id, environment] of data.environments) {
try {
const newEnvironment = await projectEnvService.createEnvironment({
actor,
actorId,
actorOrgId,
actorAuthMethod,
name: environment.name,
projectId: originalToNewProjectId.get(environment.projectId)!,
slug: slugify(`${environment.name}-${alphaNumericNanoId(4)}`)
});
if (!newEnvironment) {
logger.error(`Failed to import environment: [name:${environment.name}] [id:${id}]`);
throw new BadRequestError({
message: `Failed to import environment: [name:${environment.name}] [id:${id}]`
});
}
originalToNewEnvironmentId.set(id, newEnvironment.slug);
} catch (error) {
throw new BadRequestError({
message: `Failed to import environment: ${environment.name}]`,
name: "EnvKeyMigrationImportEnvironment"
});
}
}
}
// Import secrets
if (data.secrets) {
for await (const [id, secret] of data.secrets) {
const dataProjectId = data.environments?.get(secret.environmentId)?.projectId;
if (!dataProjectId) {
throw new BadRequestError({ message: `Failed to import secret "${secret.name}", project not found` });
}
const projectId = originalToNewProjectId.get(dataProjectId);
const newSecret = await secretService.createSecretRaw({
actorId,
actor,
actorOrgId,
environment: originalToNewEnvironmentId.get(secret.environmentId)!,
actorAuthMethod,
projectId: projectId!,
secretPath: "/",
secretName: secret.name,
type: SecretType.Shared,
secretValue: secret.value
});
if (!newSecret) {
throw new BadRequestError({ message: `Failed to import secret: [name:${secret.name}] [id:${id}]` });
}
}
}
};

View File

@@ -0,0 +1,64 @@
import { OrgMembershipRole } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ForbiddenRequestError } from "@app/lib/errors";
import { TOrgServiceFactory } from "../org/org-service";
import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TSecretServiceFactory } from "../secret/secret-service";
import { decryptEnvKeyDataFn, importDataIntoInfisicalFn, parseEnvKeyDataFn } from "./external-migration-fns";
import { TImportEnvKeyDataCreate } from "./external-migration-types";
type TExternalMigrationServiceFactoryDep = {
projectService: TProjectServiceFactory;
orgService: TOrgServiceFactory;
projectEnvService: TProjectEnvServiceFactory;
secretService: TSecretServiceFactory;
permissionService: TPermissionServiceFactory;
};
export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>;
export const externalMigrationServiceFactory = ({
projectService,
orgService,
projectEnvService,
permissionService,
secretService
}: TExternalMigrationServiceFactoryDep) => {
const importEnvKeyData = async ({
decryptionKey,
encryptedJson,
actor,
actorId,
actorOrgId,
actorAuthMethod
}: TImportEnvKeyDataCreate) => {
const { membership } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
if (membership.role !== OrgMembershipRole.Admin) {
throw new ForbiddenRequestError({ message: "Only admins can import data" });
}
const json = await decryptEnvKeyDataFn(decryptionKey, encryptedJson);
const envKeyData = await parseEnvKeyDataFn(json);
const response = await importDataIntoInfisicalFn({
input: { data: envKeyData, actor, actorId, actorOrgId, actorAuthMethod },
projectService,
orgService,
projectEnvService,
secretService
});
return response;
};
return {
importEnvKeyData
};
};

View File

@@ -0,0 +1,106 @@
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
export type InfisicalImportData = {
projects: Map<string, { name: string; id: string }>;
environments?: Map<
string,
{
name: string;
id: string;
projectId: string;
}
>;
secrets?: Map<
string,
{
name: string;
id: string;
environmentId: string;
value: string;
}
>;
};
export type TImportEnvKeyDataCreate = {
decryptionKey: string;
encryptedJson: { nonce: string; data: string };
actor: ActorType;
actorId: string;
actorOrgId: string;
actorAuthMethod: ActorAuthMethod;
};
export type TImportInfisicalDataCreate = {
data: InfisicalImportData;
actor: ActorType;
actorId: string;
actorOrgId: string;
actorAuthMethod: ActorAuthMethod;
};
export type TEnvKeyExportJSON = {
schemaVersion: string;
org: {
id: string;
name: string;
settings: {
auth: {
inviteExpirationMs: number;
deviceGrantExpirationMs: number;
tokenExpirationMs: number;
};
crypto: {
requiresPassphrase: boolean;
requiresLockout: boolean;
};
envs: {
autoCaps: boolean;
autoCommitLocals: boolean;
};
};
};
apps: {
id: string;
name: string;
settings: Record<string, unknown>;
}[];
defaultOrgRoles: {
id: string;
defaultName: string;
}[];
defaultAppRoles: {
id: string;
defaultName: string;
}[];
defaultEnvironmentRoles: {
id: string;
defaultName: string;
settings: {
autoCommit: boolean;
};
}[];
baseEnvironments: {
id: string;
envParentId: string;
environmentRoleId: string;
settings: Record<string, unknown>;
}[];
orgUsers: {
id: string;
firstName: string;
lastName: string;
email: string;
provider: string;
orgRoleId: string;
uid: string;
}[];
envs: Record<
string,
{
variables: Record<string, { val: string }>;
inherits: Record<string, unknown>;
}
>;
};

View File

@@ -1,5 +1,5 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import axios from "axios"; import axios, { AxiosError } from "axios";
import https from "https"; import https from "https";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
@@ -107,32 +107,54 @@ export const identityKubernetesAuthServiceFactory = ({
}); });
} }
const { data }: { data: TCreateTokenReviewResponse } = await axios.post( const { data } = await axios
`${identityKubernetesAuth.kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`, .post<TCreateTokenReviewResponse>(
{ `${identityKubernetesAuth.kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`,
apiVersion: "authentication.k8s.io/v1", {
kind: "TokenReview", apiVersion: "authentication.k8s.io/v1",
spec: { kind: "TokenReview",
token: serviceAccountJwt spec: {
} token: serviceAccountJwt
}, }
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
}, },
httpsAgent: new https.Agent({ {
ca: caCert, headers: {
rejectUnauthorized: !!caCert "Content-Type": "application/json",
}) Authorization: `Bearer ${tokenReviewerJwt}`
} },
);
if ("error" in data.status) throw new UnauthorizedError({ message: data.status.error }); // if ca cert, rejectUnauthorized: true
httpsAgent: new https.Agent({
ca: caCert,
rejectUnauthorized: !!caCert
})
}
)
.catch((err) => {
if (err instanceof AxiosError) {
if (err.response) {
const { message } = err?.response?.data as unknown as { message?: string };
if (message) {
throw new UnauthorizedError({
message,
name: "KubernetesTokenReviewRequestError"
});
}
}
}
throw err;
});
if ("error" in data.status)
throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" });
// check the response to determine if the token is valid // check the response to determine if the token is valid
if (!(data.status && data.status.authenticated)) if (!(data.status && data.status.authenticated))
throw new UnauthorizedError({ message: "Kubernetes token not authenticated" }); throw new UnauthorizedError({
message: "Kubernetes token not authenticated",
name: "KubernetesTokenReviewError"
});
const { namespace: targetNamespace, name: targetName } = extractK8sUsername(data.status.user.username); const { namespace: targetNamespace, name: targetName } = extractK8sUsername(data.status.user.username);

View File

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

View File

@@ -3,7 +3,7 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TIdentityOrgMemberships } from "@app/db/schemas"; import { TableName, TIdentityOrgMemberships } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types"; import { TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
@@ -42,10 +42,25 @@ export const identityOrgDALFactory = (db: TDbClient) => {
tx?: Knex tx?: Knex
) => { ) => {
try { try {
const paginatedFetchIdentity = (tx || db.replicaNode())(TableName.Identity)
.where((queryBuilder) => {
if (limit) {
void queryBuilder.offset(offset).limit(limit);
}
})
.as(TableName.Identity);
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership) const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
.where(filter) .where(filter)
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`) .join<Awaited<typeof paginatedFetchIdentity>>(paginatedFetchIdentity, (queryBuilder) => {
queryBuilder.on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`);
})
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) .leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder
.on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`${TableName.IdentityOrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`);
})
.select(selectAllTableCols(TableName.IdentityOrgMembership)) .select(selectAllTableCols(TableName.IdentityOrgMembership))
// cr stands for custom role // cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
@@ -55,12 +70,15 @@ export const identityOrgDALFactory = (db: TDbClient) => {
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)) .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)) .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
.select(db.ref("id").as("identityId").withSchema(TableName.Identity)) .select(db.ref("id").as("identityId").withSchema(TableName.Identity))
.select(db.ref("name").as("identityName").withSchema(TableName.Identity)) .select(
.select(db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity)); db.ref("name").as("identityName").withSchema(TableName.Identity),
db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity)
if (limit) { )
void query.offset(offset).limit(limit); .select(
} db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
);
if (orderBy) { if (orderBy) {
switch (orderBy) { switch (orderBy) {
@@ -80,9 +98,10 @@ export const identityOrgDALFactory = (db: TDbClient) => {
} }
const docs = await query; const docs = await query;
const formattedDocs = sqlNestRelationships({
return docs.map( data: docs,
({ key: "id",
parentMapper: ({
crId, crId,
crDescription, crDescription,
crSlug, crSlug,
@@ -91,16 +110,21 @@ export const identityOrgDALFactory = (db: TDbClient) => {
identityId, identityId,
identityName, identityName,
identityAuthMethod, identityAuthMethod,
...el role,
roleId,
id,
orgId,
createdAt,
updatedAt
}) => ({ }) => ({
...el, role,
roleId,
identityId, identityId,
identity: { id,
id: identityId, orgId,
name: identityName, createdAt,
authMethod: identityAuthMethod updatedAt,
}, customRole: roleId
customRole: el.roleId
? { ? {
id: crId, id: crId,
name: crName, name: crName,
@@ -108,9 +132,27 @@ export const identityOrgDALFactory = (db: TDbClient) => {
permissions: crPermission, permissions: crPermission,
description: crDescription description: crDescription
} }
: undefined : undefined,
}) identity: {
); id: identityId,
name: identityName,
authMethod: identityAuthMethod as string
}
}),
childrenMapper: [
{
key: "metadataId",
label: "metadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
return formattedDocs;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindByOrgId" }); throw new DatabaseError({ error, name: "FindByOrgId" });
} }

View File

@@ -10,6 +10,7 @@ import { TIdentityProjectDALFactory } from "@app/services/identity-project/ident
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TIdentityDALFactory } from "./identity-dal"; import { TIdentityDALFactory } from "./identity-dal";
import { TIdentityMetadataDALFactory } from "./identity-metadata-dal";
import { TIdentityOrgDALFactory } from "./identity-org-dal"; import { TIdentityOrgDALFactory } from "./identity-org-dal";
import { import {
TCreateIdentityDTO, TCreateIdentityDTO,
@@ -22,6 +23,7 @@ import {
type TIdentityServiceFactoryDep = { type TIdentityServiceFactoryDep = {
identityDAL: TIdentityDALFactory; identityDAL: TIdentityDALFactory;
identityMetadataDAL: TIdentityMetadataDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findByIdentityId">; identityProjectDAL: Pick<TIdentityProjectDALFactory, "findByIdentityId">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
@@ -32,6 +34,7 @@ export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
export const identityServiceFactory = ({ export const identityServiceFactory = ({
identityDAL, identityDAL,
identityMetadataDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityProjectDAL, identityProjectDAL,
permissionService, permissionService,
@@ -44,7 +47,8 @@ export const identityServiceFactory = ({
orgId, orgId,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
metadata
}: TCreateIdentityDTO) => { }: TCreateIdentityDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
@@ -78,6 +82,17 @@ export const identityServiceFactory = ({
}, },
tx tx
); );
if (metadata && metadata.length) {
await identityMetadataDAL.insertMany(
metadata.map(({ key, value }) => ({
identityId: newIdentity.id,
orgId,
key,
value
})),
tx
);
}
return newIdentity; return newIdentity;
}); });
await licenseService.updateSubscriptionOrgMemberCount(orgId); await licenseService.updateSubscriptionOrgMemberCount(orgId);
@@ -92,7 +107,8 @@ export const identityServiceFactory = ({
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
metadata
}: TUpdateIdentityDTO) => { }: TUpdateIdentityDTO) => {
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id }); const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` });
@@ -134,8 +150,8 @@ export const identityServiceFactory = ({
const identity = await identityDAL.transaction(async (tx) => { const identity = await identityDAL.transaction(async (tx) => {
const newIdentity = name ? await identityDAL.updateById(id, { name }, tx) : await identityDAL.findById(id, tx); const newIdentity = name ? await identityDAL.updateById(id, { name }, tx) : await identityDAL.findById(id, tx);
if (role) { if (role) {
await identityOrgMembershipDAL.update( await identityOrgMembershipDAL.updateById(
{ identityId: id }, identityOrgMembership.id,
{ {
role: customRole ? OrgMembershipRole.Custom : role, role: customRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id || null roleId: customRole?.id || null
@@ -143,6 +159,20 @@ export const identityServiceFactory = ({
tx tx
); );
} }
if (metadata) {
await identityMetadataDAL.delete({ orgId: identityOrgMembership.orgId, identityId: id }, tx);
if (metadata.length) {
await identityMetadataDAL.insertMany(
metadata.map(({ key, value }) => ({
identityId: newIdentity.id,
orgId: identityOrgMembership.orgId,
key,
value
})),
tx
);
}
}
return newIdentity; return newIdentity;
}); });

View File

@@ -4,12 +4,14 @@ import { OrderByDirection, TOrgPermission } from "@app/lib/types";
export type TCreateIdentityDTO = { export type TCreateIdentityDTO = {
role: string; role: string;
name: string; name: string;
metadata?: { key: string; value: string }[];
} & TOrgPermission; } & TOrgPermission;
export type TUpdateIdentityDTO = { export type TUpdateIdentityDTO = {
id: string; id: string;
role?: string; role?: string;
name?: string; name?: string;
metadata?: { key: string; value: string }[];
} & Omit<TOrgPermission, "orgId">; } & Omit<TOrgPermission, "orgId">;
export type TDeleteIdentityDTO = { export type TDeleteIdentityDTO = {

View File

@@ -150,12 +150,17 @@ export const integrationServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
ForbiddenError.from(permission).throwUnlessCan( const newEnvironment = environment || integration.environment.slug;
ProjectPermissionActions.Read, const newSecretPath = secretPath || integration.secretPath;
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(integration.projectId, environment, secretPath); if (environment || secretPath) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: newEnvironment, secretPath: newSecretPath })
);
}
const folder = await folderDAL.findBySecretPath(integration.projectId, newEnvironment, newSecretPath);
if (!folder) throw new NotFoundError({ message: "Folder path not found" }); if (!folder) throw new NotFoundError({ message: "Folder path not found" });
const updatedIntegration = await integrationDAL.updateById(id, { const updatedIntegration = await integrationDAL.updateById(id, {
@@ -174,7 +179,7 @@ export const integrationServiceFactory = ({
await secretQueueService.syncIntegrations({ await secretQueueService.syncIntegrations({
environment: folder.environment.slug, environment: folder.environment.slug,
secretPath, secretPath: newSecretPath,
projectId: folder.projectId projectId: folder.projectId
}); });
@@ -184,6 +189,12 @@ export const integrationServiceFactory = ({
const getIntegration = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => { const getIntegration = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => {
const integration = await integrationDAL.findById(id); const integration = await integrationDAL.findById(id);
if (!integration) {
throw new NotFoundError({
message: "Integration not found"
});
}
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,

View File

@@ -48,10 +48,10 @@ export type TUpdateIntegrationDTO = {
app?: string; app?: string;
appId?: string; appId?: string;
isActive?: boolean; isActive?: boolean;
secretPath: string; secretPath?: string;
targetEnvironment: string; targetEnvironment?: string;
owner: string; owner?: string;
environment: string; environment?: string;
metadata?: { metadata?: {
secretPrefix?: string; secretPrefix?: string;
secretSuffix?: string; secretSuffix?: string;

View File

@@ -208,20 +208,20 @@ export const kmsServiceFactory = ({
return org.kmsDefaultKeyId; return org.kmsDefaultKeyId;
}; };
const encryptWithRootKey = async () => { const encryptWithRootKey = () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return ({ plainText }: { plainText: Buffer }) => {
const encryptedPlainTextBlob = cipher.encrypt(plainText, ROOT_ENCRYPTION_KEY);
return Promise.resolve({ cipherTextBlob: encryptedPlainTextBlob }); return (plainTextBuffer: Buffer) => {
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
return encryptedBuffer;
}; };
}; };
const decryptWithRootKey = async () => { const decryptWithRootKey = () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return ({ cipherTextBlob }: { cipherTextBlob: Buffer }) => {
const decryptedBlob = cipher.decrypt(cipherTextBlob, ROOT_ENCRYPTION_KEY); return (cipherTextBuffer: Buffer) => {
return Promise.resolve(decryptedBlob); return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
}; };
}; };

View File

@@ -1,7 +1,7 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify, sqlNestRelationships } from "@app/lib/knex";
export type TOrgMembershipDALFactory = ReturnType<typeof orgMembershipDALFactory>; export type TOrgMembershipDALFactory = ReturnType<typeof orgMembershipDALFactory>;
@@ -19,6 +19,11 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
`${TableName.UserEncryptionKey}.userId`, `${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id` `${TableName.Users}.id`
) )
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder
.on(`${TableName.OrgMembership}.userId`, `${TableName.IdentityMetadata}.userId`)
.andOn(`${TableName.OrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`);
})
.select( .select(
db.ref("id").withSchema(TableName.OrgMembership), db.ref("id").withSchema(TableName.OrgMembership),
db.ref("inviteEmail").withSchema(TableName.OrgMembership), db.ref("inviteEmail").withSchema(TableName.OrgMembership),
@@ -33,19 +38,66 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
db.ref("lastName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users),
db.ref("isEmailVerified").withSchema(TableName.Users), db.ref("isEmailVerified").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"), db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey) db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
) )
.where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER .where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER
.first();
if (!member) return undefined; if (!member) return undefined;
const { email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data } = member; const doc = sqlNestRelationships({
data: member,
key: "id",
parentMapper: ({
email,
isEmailVerified,
username,
firstName,
lastName,
userId,
publicKey,
roleId,
orgId,
id,
role,
status,
isActive,
inviteEmail
}) => ({
roleId,
orgId,
id,
role,
status,
isActive,
inviteEmail,
user: {
id: userId,
email,
isEmailVerified,
username,
firstName,
lastName,
userId,
publicKey
}
}),
childrenMapper: [
{
key: "metadataId",
label: "metadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
return { return doc?.[0];
...data,
user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey }
};
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find org membership by id" }); throw new DatabaseError({ error, name: "Find org membership by id" });
} }

View File

@@ -29,13 +29,37 @@ export const orgDALFactory = (db: TDbClient) => {
}; };
// special query // special query
const findAllOrgsByUserId = async (userId: string): Promise<TOrganizations[]> => { const findAllOrgsByUserId = async (userId: string): Promise<(TOrganizations & { orgAuthMethod: string })[]> => {
try { try {
const org = await db const org = (await db
.replicaNode()(TableName.OrgMembership) .replicaNode()(TableName.OrgMembership)
.where({ userId }) .where({ userId })
.join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`) .join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`)
.select(selectAllTableCols(TableName.Organization)); .leftJoin(TableName.SamlConfig, (qb) => {
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
`${TableName.SamlConfig}.isActive`,
"=",
db.raw("true")
);
})
.leftJoin(TableName.OidcConfig, (qb) => {
qb.on(`${TableName.OidcConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
`${TableName.OidcConfig}.isActive`,
"=",
db.raw("true")
);
})
.select(selectAllTableCols(TableName.Organization))
.select(
db.raw(`
CASE
WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN 'saml'
WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN 'oidc'
ELSE ''
END as "orgAuthMethod"
`)
)) as (TOrganizations & { orgAuthMethod: string })[];
return org; return org;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find all org by user id" }); throw new DatabaseError({ error, name: "Find all org by user id" });

View File

@@ -18,6 +18,7 @@ import {
import { TProjects } from "@app/db/schemas/projects"; import { TProjects } from "@app/db/schemas/projects";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@@ -37,6 +38,7 @@ import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types"; import { TokenType } from "../auth-token/auth-token-types";
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers } from "../project/project-fns"; import { assignWorkspaceKeysToMembers } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
@@ -72,14 +74,16 @@ type TOrgServiceFactoryDep = {
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
groupDAL: TGroupDALFactory; groupDAL: TGroupDALFactory;
projectDAL: TProjectDALFactory; projectDAL: TProjectDALFactory;
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
projectMembershipDAL: Pick< projectMembershipDAL: Pick<
TProjectMembershipDALFactory, TProjectMembershipDALFactory,
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction" "findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
>; >;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne" | "findById">;
incidentContactDAL: TIncidentContactsDALFactory; incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">; samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "findEnforceableOidcCfg">;
smtpService: TSmtpService; smtpService: TSmtpService;
tokenService: TAuthTokenServiceFactory; tokenService: TAuthTokenServiceFactory;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
@@ -114,8 +118,10 @@ export const orgServiceFactory = ({
licenseService, licenseService,
projectRoleDAL, projectRoleDAL,
samlConfigDAL, samlConfigDAL,
oidcConfigDAL,
projectBotDAL, projectBotDAL,
projectUserMembershipRoleDAL projectUserMembershipRoleDAL,
identityMetadataDAL
}: TOrgServiceFactoryDep) => { }: TOrgServiceFactoryDep) => {
/* /*
* Get organization details by the organization id * Get organization details by the organization id
@@ -266,10 +272,9 @@ export const orgServiceFactory = ({
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (authEnforced !== undefined) { if (authEnforced !== undefined) {
if (!plan?.samlSSO) if (!plan?.samlSSO || !plan.oidcSSO)
throw new BadRequestError({ throw new BadRequestError({
message: message: "Failed to enforce/un-enforce SSO due to plan restriction. Upgrade plan to enforce/un-enforce SSO."
"Failed to enforce/un-enforce SAML SSO due to plan restriction. Upgrade plan to enforce/un-enforce SAML SSO."
}); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
} }
@@ -285,9 +290,11 @@ export const orgServiceFactory = ({
if (authEnforced) { if (authEnforced) {
const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId); const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId);
if (!samlCfg) const oidcCfg = await oidcConfigDAL.findEnforceableOidcCfg(orgId);
if (!samlCfg && !oidcCfg)
throw new NotFoundError({ throw new NotFoundError({
message: "No enforceable SAML config found" message: "No enforceable SSO config found"
}); });
} }
@@ -404,20 +411,22 @@ export const orgServiceFactory = ({
userId, userId,
membershipId, membershipId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
metadata
}: TUpdateOrgMembershipDTO) => { }: TUpdateOrgMembershipDTO) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
const foundMembership = await orgMembershipDAL.findOne({ const foundMembership = await orgMembershipDAL.findById(membershipId);
id: membershipId,
orgId
});
if (!foundMembership) throw new NotFoundError({ message: "Failed to find organization membership" }); if (!foundMembership) throw new NotFoundError({ message: "Failed to find organization membership" });
if (foundMembership.orgId !== orgId)
throw new UnauthorizedError({ message: "Updated org member doesn't belong to the organization" });
if (foundMembership.userId === userId) if (foundMembership.userId === userId)
throw new UnauthorizedError({ message: "Cannot update own organization membership" }); throw new UnauthorizedError({ message: "Cannot update own organization membership" });
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole); const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
let userRole = role;
let userRoleId: string | null = null;
if (role && isCustomRole) { if (role && isCustomRole) {
const customRole = await orgRoleDAL.findOne({ slug: role, orgId }); const customRole = await orgRoleDAL.findOne({ slug: role, orgId });
if (!customRole) throw new BadRequestError({ name: "UpdateMembership", message: "Organization role not found" }); if (!customRole) throw new BadRequestError({ name: "UpdateMembership", message: "Organization role not found" });
@@ -428,17 +437,31 @@ export const orgServiceFactory = ({
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member." message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
}); });
const [membership] = await orgDAL.updateMembership( userRole = OrgMembershipRole.Custom;
{ id: membershipId, orgId }, userRoleId = customRole.id;
{
role: OrgMembershipRole.Custom,
roleId: customRole.id
}
);
return membership;
} }
const membership = await orgDAL.transaction(async (tx) => {
const [updatedOrgMembership] = await orgDAL.updateMembership(
{ id: membershipId, orgId },
{ role: userRole, roleId: userRoleId, isActive }
);
const [membership] = await orgDAL.updateMembership({ id: membershipId, orgId }, { role, roleId: null, isActive }); if (metadata) {
await identityMetadataDAL.delete({ userId: updatedOrgMembership.userId, orgId }, tx);
if (metadata.length) {
await identityMetadataDAL.insertMany(
metadata.map(({ key, value }) => ({
userId: updatedOrgMembership.userId,
orgId,
key,
value
})),
tx
);
}
}
return updatedOrgMembership;
});
return membership; return membership;
}; };
/* /*

View File

@@ -9,6 +9,7 @@ export type TUpdateOrgMembershipDTO = {
role?: string; role?: string;
isActive?: boolean; isActive?: boolean;
actorOrgId: string | undefined; actorOrgId: string | undefined;
metadata?: { key: string; value: string }[];
actorAuthMethod: ActorAuthMethod; actorAuthMethod: ActorAuthMethod;
}; };

View File

@@ -3,7 +3,9 @@ import { ForbiddenError } from "@casl/ability";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
@@ -16,6 +18,7 @@ type TProjectEnvServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem" | "waitTillReady">;
}; };
export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>; export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>;
@@ -24,6 +27,7 @@ export const projectEnvServiceFactory = ({
projectEnvDAL, projectEnvDAL,
permissionService, permissionService,
licenseService, licenseService,
keyStore,
projectDAL, projectDAL,
folderDAL folderDAL
}: TProjectEnvServiceFactoryDep) => { }: TProjectEnvServiceFactoryDep) => {
@@ -45,32 +49,56 @@ export const projectEnvServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
const envs = await projectEnvDAL.find({ projectId }); const lock = await keyStore
const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug); .acquireLock([KeyStorePrefixes.ProjectEnvironmentLock(projectId)], 5000)
if (existingEnv) .catch(() => null);
throw new BadRequestError({
message: "Environment with slug already exist", try {
name: "CreateEnvironment" if (!lock) {
await keyStore.waitTillReady({
key: KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
keyCheckCb: (val) => val === "true",
waitingCb: () => logger.debug("Create project environment. Waiting for "),
delay: 500
});
}
const envs = await projectEnvDAL.find({ projectId });
const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug);
if (existingEnv)
throw new BadRequestError({
message: "Environment with slug already exist",
name: "CreateEnvironment"
});
const project = await projectDAL.findById(projectId);
const plan = await licenseService.getPlan(project.orgId);
if (plan.environmentLimit !== null && envs.length >= plan.environmentLimit) {
// case: limit imposed on number of environments allowed
// case: number of environments used exceeds the number of environments allowed
throw new BadRequestError({
message:
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
});
}
const env = await projectEnvDAL.transaction(async (tx) => {
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
const doc = await projectEnvDAL.create({ slug, name, projectId, position: lastPos + 1 }, tx);
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
return doc;
}); });
const project = await projectDAL.findById(projectId); await keyStore.setItemWithExpiry(
const plan = await licenseService.getPlan(project.orgId); KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
if (plan.environmentLimit !== null && envs.length >= plan.environmentLimit) { 10,
// case: limit imposed on number of environments allowed "true"
// case: number of environments used exceeds the number of environments allowed );
throw new BadRequestError({
message: return env;
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments." } finally {
}); await lock?.release();
} }
const env = await projectEnvDAL.transaction(async (tx) => {
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
const doc = await projectEnvDAL.create({ slug, name, projectId, position: lastPos + 1 }, tx);
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
return doc;
});
return env;
}; };
const updateEnvironment = async ({ const updateEnvironment = async ({
@@ -93,26 +121,50 @@ export const projectEnvServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
const oldEnv = await projectEnvDAL.findOne({ id, projectId }); const lock = await keyStore
if (!oldEnv) throw new NotFoundError({ message: "Environment not found" }); .acquireLock([KeyStorePrefixes.ProjectEnvironmentLock(projectId)], 5000)
.catch(() => null);
if (slug) { try {
const existingEnv = await projectEnvDAL.findOne({ slug, projectId }); if (!lock) {
if (existingEnv && existingEnv.id !== id) { await keyStore.waitTillReady({
throw new BadRequestError({ key: KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
message: "Environment with slug already exist", keyCheckCb: (val) => val === "true",
name: "UpdateEnvironment" waitingCb: () => logger.debug("Update project environment. Waiting for project environment update"),
delay: 500
}); });
} }
}
const env = await projectEnvDAL.transaction(async (tx) => { const oldEnv = await projectEnvDAL.findOne({ id, projectId });
if (position) { if (!oldEnv) throw new NotFoundError({ message: "Environment not found", name: "UpdateEnvironment" });
await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx);
if (slug) {
const existingEnv = await projectEnvDAL.findOne({ slug, projectId });
if (existingEnv && existingEnv.id !== id) {
throw new BadRequestError({
message: "Environment with slug already exist",
name: "UpdateEnvironment"
});
}
} }
return projectEnvDAL.updateById(oldEnv.id, { name, slug, position }, tx);
}); const env = await projectEnvDAL.transaction(async (tx) => {
return { environment: env, old: oldEnv }; if (position) {
await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx);
}
return projectEnvDAL.updateById(oldEnv.id, { name, slug, position }, tx);
});
await keyStore.setItemWithExpiry(
KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
10,
"true"
);
return { environment: env, old: oldEnv };
} finally {
await lock?.release();
}
}; };
const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => { const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => {
@@ -125,18 +177,42 @@ export const projectEnvServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
const env = await projectEnvDAL.transaction(async (tx) => { const lock = await keyStore
const [doc] = await projectEnvDAL.delete({ id, projectId }, tx); .acquireLock([KeyStorePrefixes.ProjectEnvironmentLock(projectId)], 5000)
if (!doc) .catch(() => null);
throw new NotFoundError({
message: "Env doesn't exist",
name: "DeleteEnvironment"
});
await projectEnvDAL.updateAllPosition(projectId, doc.position, -1, tx); try {
return doc; if (!lock) {
}); await keyStore.waitTillReady({
return env; key: KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
keyCheckCb: (val) => val === "true",
waitingCb: () => logger.debug("Delete project environment. Waiting for "),
delay: 500
});
}
const env = await projectEnvDAL.transaction(async (tx) => {
const [doc] = await projectEnvDAL.delete({ id, projectId }, tx);
if (!doc)
throw new NotFoundError({
message: "Environment doesn't exist",
name: "DeleteEnvironment"
});
await projectEnvDAL.updateAllPosition(projectId, doc.position, -1, tx);
return doc;
});
await keyStore.setItemWithExpiry(
KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
10,
"true"
);
return env;
} finally {
await lock?.release();
}
}; };
const getEnvironmentById = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TGetEnvDTO) => { const getEnvironmentById = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TGetEnvDTO) => {

View File

@@ -7,8 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSet, ProjectPermissionSet,
ProjectPermissionSub, ProjectPermissionSub
validateProjectPermissions
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -60,8 +59,6 @@ export const projectRoleServiceFactory = ({
throw new BadRequestError({ name: "Create Role", message: "Project role with same slug already exists" }); throw new BadRequestError({ name: "Create Role", message: "Project role with same slug already exists" });
} }
validateProjectPermissions(data.permissions);
const role = await projectRoleDAL.create({ const role = await projectRoleDAL.create({
...data, ...data,
projectId projectId
@@ -127,10 +124,6 @@ export const projectRoleServiceFactory = ({
throw new BadRequestError({ name: "Update Role", message: "Project role with the same slug already exists" }); throw new BadRequestError({ name: "Update Role", message: "Project role with the same slug already exists" });
} }
if (data.permissions) {
validateProjectPermissions(data.permissions);
}
const [updatedRole] = await projectRoleDAL.update( const [updatedRole] = await projectRoleDAL.update(
{ id: roleId, projectId }, { id: roleId, projectId },
{ {

View File

@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion, TProjectEnvironments } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -146,7 +146,8 @@ export const projectServiceFactory = ({
actorAuthMethod, actorAuthMethod,
workspaceName, workspaceName,
slug: projectSlug, slug: projectSlug,
kmsKeyId kmsKeyId,
createDefaultEnvs = true
}: TCreateProjectDTO) => { }: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId }); const organization = await orgDAL.findOne({ id: actorOrgId });
@@ -207,14 +208,17 @@ export const projectServiceFactory = ({
); );
// set default environments and root folder for provided environments // set default environments and root folder for provided environments
const envs = await projectEnvDAL.insertMany( let envs: TProjectEnvironments[] = [];
DEFAULT_PROJECT_ENVS.map((el, i) => ({ ...el, projectId: project.id, position: i + 1 })), if (createDefaultEnvs) {
tx envs = await projectEnvDAL.insertMany(
); DEFAULT_PROJECT_ENVS.map((el, i) => ({ ...el, projectId: project.id, position: i + 1 })),
await folderDAL.insertMany( tx
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })), );
tx await folderDAL.insertMany(
); envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
tx
);
}
// 3. Create a random key that we'll use as the project key. // 3. Create a random key that we'll use as the project key.
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({ const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({

View File

@@ -29,6 +29,7 @@ export type TCreateProjectDTO = {
workspaceName: string; workspaceName: string;
slug?: string; slug?: string;
kmsKeyId?: string; kmsKeyId?: string;
createDefaultEnvs?: boolean;
}; };
export type TDeleteProjectBySlugDTO = { export type TDeleteProjectBySlugDTO = {

View File

@@ -548,7 +548,7 @@ export const secretImportServiceFactory = ({
if (!botKey) if (!botKey)
throw new NotFoundError({ throw new NotFoundError({
message: "Project bot not found. Please upgrade your project.", message: "Project bot not found. Please upgrade your project.",
name: "BotNotFoundError" name: "bot_not_found_error"
}); });
const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL }); const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });

View File

@@ -1,10 +1,14 @@
import crypto from "node:crypto";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { z } from "zod";
import { TSecretSharing } from "@app/db/schemas"; import { TSecretSharing } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { SecretSharingAccessType } from "@app/lib/types"; import { SecretSharingAccessType } from "@app/lib/types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TSecretSharingDALFactory } from "./secret-sharing-dal"; import { TSecretSharingDALFactory } from "./secret-sharing-dal";
import { import {
@@ -19,14 +23,18 @@ type TSecretSharingServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
secretSharingDAL: TSecretSharingDALFactory; secretSharingDAL: TSecretSharingDALFactory;
orgDAL: TOrgDALFactory; orgDAL: TOrgDALFactory;
kmsService: TKmsServiceFactory;
}; };
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>; export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success;
export const secretSharingServiceFactory = ({ export const secretSharingServiceFactory = ({
permissionService, permissionService,
secretSharingDAL, secretSharingDAL,
orgDAL orgDAL,
kmsService
}: TSecretSharingServiceFactoryDep) => { }: TSecretSharingServiceFactoryDep) => {
const createSharedSecret = async ({ const createSharedSecret = async ({
actor, actor,
@@ -34,10 +42,7 @@ export const secretSharingServiceFactory = ({
orgId, orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
encryptedValue, secretValue,
hashedHex,
iv,
tag,
name, name,
password, password,
accessType, accessType,
@@ -59,19 +64,25 @@ export const secretSharingServiceFactory = ({
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" }); throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
} }
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext) if (secretValue.length > 10_000) {
if (encryptedValue.length > 13000) {
throw new BadRequestError({ message: "Shared secret value too long" }); throw new BadRequestError({ message: "Shared secret value too long" });
} }
const encryptWithRoot = kmsService.encryptWithRootKey();
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
const id = crypto.randomBytes(32).toString("hex");
const hashedPassword = password ? await bcrypt.hash(password, 10) : null; const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
const newSharedSecret = await secretSharingDAL.create({ const newSharedSecret = await secretSharingDAL.create({
identifier: id,
iv: null,
tag: null,
encryptedValue: null,
encryptedSecret,
name, name,
password: hashedPassword, password: hashedPassword,
encryptedValue,
hashedHex,
iv,
tag,
expiresAt: new Date(expiresAt), expiresAt: new Date(expiresAt),
expiresAfterViews, expiresAfterViews,
userId: actorId, userId: actorId,
@@ -79,15 +90,14 @@ export const secretSharingServiceFactory = ({
accessType accessType
}); });
return { id: newSharedSecret.id }; const idToReturn = `${Buffer.from(newSharedSecret.identifier!, "hex").toString("base64url")}`;
return { id: idToReturn };
}; };
const createPublicSharedSecret = async ({ const createPublicSharedSecret = async ({
password, password,
encryptedValue, secretValue,
hashedHex,
iv,
tag,
expiresAt, expiresAt,
expiresAfterViews, expiresAfterViews,
accessType accessType
@@ -104,24 +114,25 @@ export const secretSharingServiceFactory = ({
throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" }); throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" });
} }
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext) const encryptWithRoot = kmsService.encryptWithRootKey();
if (encryptedValue.length > 13000) { const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
throw new BadRequestError({ message: "Shared secret value too long" });
}
const id = crypto.randomBytes(32).toString("hex");
const hashedPassword = password ? await bcrypt.hash(password, 10) : null; const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
const newSharedSecret = await secretSharingDAL.create({ const newSharedSecret = await secretSharingDAL.create({
identifier: id,
encryptedValue: null,
iv: null,
tag: null,
encryptedSecret,
password: hashedPassword, password: hashedPassword,
encryptedValue,
hashedHex,
iv,
tag,
expiresAt: new Date(expiresAt), expiresAt: new Date(expiresAt),
expiresAfterViews, expiresAfterViews,
accessType accessType
}); });
return { id: newSharedSecret.id }; return { id: `${Buffer.from(newSharedSecret.identifier!, "hex").toString("base64url")}` };
}; };
const getSharedSecrets = async ({ const getSharedSecrets = async ({
@@ -162,25 +173,30 @@ export const secretSharingServiceFactory = ({
}; };
}; };
const $decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => { const $decrementSecretViewCount = async (sharedSecret: TSecretSharing) => {
const { expiresAfterViews } = sharedSecret; const { expiresAfterViews } = sharedSecret;
if (expiresAfterViews) { if (expiresAfterViews) {
// decrement view count if view count expiry set // decrement view count if view count expiry set
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } }); await secretSharingDAL.updateById(sharedSecret.id, { $decr: { expiresAfterViews: 1 } });
} }
await secretSharingDAL.updateById(sharedSecretId, { await secretSharingDAL.updateById(sharedSecret.id, {
lastViewedAt: new Date() lastViewedAt: new Date()
}); });
}; };
/** Get's passwordless secret. validates all secret's requested (must be fresh). */ /** Get's password-less secret. validates all secret's requested (must be fresh). */
const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => { const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => {
const sharedSecret = await secretSharingDAL.findOne({ const sharedSecret = isUuidV4(sharedSecretId)
id: sharedSecretId, ? await secretSharingDAL.findOne({
hashedHex id: sharedSecretId,
}); hashedHex
})
: await secretSharingDAL.findOne({
identifier: Buffer.from(sharedSecretId, "base64url").toString("hex")
});
if (!sharedSecret) if (!sharedSecret)
throw new NotFoundError({ throw new NotFoundError({
message: "Shared secret not found" message: "Shared secret not found"
@@ -222,13 +238,23 @@ export const secretSharingServiceFactory = ({
} }
} }
// If encryptedSecret is set, we know that this secret has been encrypted using KMS, and we can therefore do server-side decryption.
let decryptedSecretValue: Buffer | undefined;
if (sharedSecret.encryptedSecret) {
const decryptWithRoot = kmsService.decryptWithRootKey();
decryptedSecretValue = decryptWithRoot(sharedSecret.encryptedSecret);
}
// decrement when we are sure the user will view secret. // decrement when we are sure the user will view secret.
await $decrementSecretViewCount(sharedSecret, sharedSecretId); await $decrementSecretViewCount(sharedSecret);
return { return {
isPasswordProtected, isPasswordProtected,
secret: { secret: {
...sharedSecret, ...sharedSecret,
...(decryptedSecretValue && {
secretValue: Buffer.from(decryptedSecretValue).toString()
}),
orgName: orgName:
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
? orgName ? orgName
@@ -241,7 +267,16 @@ export const secretSharingServiceFactory = ({
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput; const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" }); if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" });
const sharedSecret = isUuidV4(sharedSecretId)
? await secretSharingDAL.findById(sharedSecretId)
: await secretSharingDAL.findOne({ identifier: sharedSecretId });
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId); const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
if (sharedSecret.orgId && sharedSecret.orgId !== orgId)
throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" });
return deletedSharedSecret; return deletedSharedSecret;
}; };

View File

@@ -19,10 +19,7 @@ export type TSharedSecretPermission = {
}; };
export type TCreatePublicSharedSecretDTO = { export type TCreatePublicSharedSecretDTO = {
encryptedValue: string; secretValue: string;
hashedHex: string;
iv: string;
tag: string;
expiresAt: string; expiresAt: string;
expiresAfterViews?: number; expiresAfterViews?: number;
password?: string; password?: string;
@@ -31,7 +28,7 @@ export type TCreatePublicSharedSecretDTO = {
export type TGetActiveSharedSecretByIdDTO = { export type TGetActiveSharedSecretByIdDTO = {
sharedSecretId: string; sharedSecretId: string;
hashedHex: string; hashedHex?: string;
orgId?: string; orgId?: string;
password?: string; password?: string;
}; };

View File

@@ -835,7 +835,7 @@ export const createManySecretsRawFnFactory = ({
if (!botKey) if (!botKey)
throw new NotFoundError({ throw new NotFoundError({
message: "Project bot not found. Please upgrade your project.", message: "Project bot not found. Please upgrade your project.",
name: "BotNotFoundError" name: "bot_not_found_error"
}); });
const inputSecrets = secrets.map((secret) => { const inputSecrets = secrets.map((secret) => {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey); const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
@@ -1000,7 +1000,7 @@ export const updateManySecretsRawFnFactory = ({
if (!botKey) if (!botKey)
throw new NotFoundError({ throw new NotFoundError({
message: "Project bot not found. Please upgrade your project.", message: "Project bot not found. Please upgrade your project.",
name: "BotNotFoundError" name: "bot_not_found_error"
}); });
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
if (!blindIndexCfg) throw new NotFoundError({ message: "Blind index not found", name: "Update secret" }); if (!blindIndexCfg) throw new NotFoundError({ message: "Blind index not found", name: "Update secret" });

View File

@@ -930,6 +930,11 @@ export const secretQueueFactory = ({
} }
}); });
// re-throw error to re-run job unless final attempt, then log and send fail email
if (job.attemptsStarted !== job.opts.attempts) {
throw err;
}
await integrationDAL.updateById(integration.id, { await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id, lastSyncJobId: job.id,
syncMessage: message, syncMessage: message,

View File

@@ -1105,7 +1105,7 @@ export const secretServiceFactory = ({
if (!botKey) if (!botKey)
throw new NotFoundError({ throw new NotFoundError({
message: "Project bot not found. Please upgrade your project.", message: "Project bot not found. Please upgrade your project.",
name: "BotNotFoundError" name: "bot_not_found_error"
}); });
const { secrets, imports } = await getSecrets({ const { secrets, imports } = await getSecrets({
@@ -1269,7 +1269,7 @@ export const secretServiceFactory = ({
if (!botKey) if (!botKey)
throw new NotFoundError({ throw new NotFoundError({
message: "Project bot not found. Please upgrade your project.", message: "Project bot not found. Please upgrade your project.",
name: "BotNotFoundError" name: "bot_not_found_error"
}); });
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey); const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
@@ -1365,7 +1365,7 @@ export const secretServiceFactory = ({
if (!botKey) if (!botKey)
throw new NotFoundError({ throw new NotFoundError({
message: "Project bot not found. Please upgrade your project.", message: "Project bot not found. Please upgrade your project.",
name: "BotNotFoundError" name: "bot_not_found_error"
}); });
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey); const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
@@ -1507,7 +1507,7 @@ export const secretServiceFactory = ({
if (!botKey) if (!botKey)
throw new NotFoundError({ throw new NotFoundError({
message: "Project bot not found. Please upgrade your project.", message: "Project bot not found. Please upgrade your project.",
name: "BotNotFoundError" name: "bot_not_found_error"
}); });
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
@@ -1633,7 +1633,7 @@ export const secretServiceFactory = ({
if (!botKey) if (!botKey)
throw new NotFoundError({ throw new NotFoundError({
message: "Project bot not found. Please upgrade your project.", message: "Project bot not found. Please upgrade your project.",
name: "BotNotFoundError" name: "bot_not_found_error"
}); });
if (policy) { if (policy) {
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({ const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
@@ -1737,7 +1737,7 @@ export const secretServiceFactory = ({
if (!botKey) if (!botKey)
throw new NotFoundError({ throw new NotFoundError({
message: "Project bot not found. Please upgrade your project.", message: "Project bot not found. Please upgrade your project.",
name: "BotNotFoundError" name: "bot_not_found_error"
}); });
const sanitizedSecrets = inputSecrets.map( const sanitizedSecrets = inputSecrets.map(
({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => { ({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => {
@@ -1863,7 +1863,7 @@ export const secretServiceFactory = ({
if (!botKey) if (!botKey)
throw new NotFoundError({ throw new NotFoundError({
message: "Project bot not found. Please upgrade your project.", message: "Project bot not found. Please upgrade your project.",
name: "BotNotFoundError" name: "bot_not_found_error"
}); });
const sanitizedSecrets = inputSecrets.map( const sanitizedSecrets = inputSecrets.map(
({ ({
@@ -1995,7 +1995,7 @@ export const secretServiceFactory = ({
if (!botKey) if (!botKey)
throw new NotFoundError({ throw new NotFoundError({
message: "Project bot not found. Please upgrade your project.", message: "Project bot not found. Please upgrade your project.",
name: "BotNotFoundError" name: "bot_not_found_error"
}); });
if (policy) { if (policy) {
@@ -2332,7 +2332,7 @@ export const secretServiceFactory = ({
if (!botKey) if (!botKey)
throw new NotFoundError({ throw new NotFoundError({
message: "Project bot not found. Please upgrade your project.", message: "Project bot not found. Please upgrade your project.",
name: "BotNotFoundError" name: "bot_not_found_error"
}); });
await secretDAL.transaction(async (tx) => { await secretDAL.transaction(async (tx) => {
@@ -2418,7 +2418,7 @@ export const secretServiceFactory = ({
if (!botKey) { if (!botKey) {
throw new NotFoundError({ throw new NotFoundError({
message: "Project bot not found. Please upgrade your project.", message: "Project bot not found. Please upgrade your project.",
name: "BotNotFoundError" name: "bot_not_found_error"
}); });
} }

View File

@@ -141,16 +141,14 @@ export const slackServiceFactory = ({
let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string; let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string; let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
const decrypt = await kmsService.decryptWithRootKey(); const decrypt = kmsService.decryptWithRootKey();
if (serverCfg.encryptedSlackClientId) { if (serverCfg.encryptedSlackClientId) {
slackClientId = (await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientId) })).toString(); slackClientId = decrypt(Buffer.from(serverCfg.encryptedSlackClientId)).toString();
} }
if (serverCfg.encryptedSlackClientSecret) { if (serverCfg.encryptedSlackClientSecret) {
slackClientSecret = ( slackClientSecret = decrypt(Buffer.from(serverCfg.encryptedSlackClientSecret)).toString();
await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientSecret) })
).toString();
} }
if (!slackClientId || !slackClientSecret) { if (!slackClientId || !slackClientSecret) {

View File

@@ -122,20 +122,16 @@ export const superAdminServiceFactory = ({
} }
} }
const encryptWithRoot = await kmsService.encryptWithRootKey(); const encryptWithRoot = kmsService.encryptWithRootKey();
if (data.slackClientId) { if (data.slackClientId) {
const { cipherTextBlob: encryptedClientId } = await encryptWithRoot({ const encryptedClientId = encryptWithRoot(Buffer.from(data.slackClientId));
plainText: Buffer.from(data.slackClientId)
});
updatedData.encryptedSlackClientId = encryptedClientId; updatedData.encryptedSlackClientId = encryptedClientId;
updatedData.slackClientId = undefined; updatedData.slackClientId = undefined;
} }
if (data.slackClientSecret) { if (data.slackClientSecret) {
const { cipherTextBlob: encryptedClientSecret } = await encryptWithRoot({ const encryptedClientSecret = encryptWithRoot(Buffer.from(data.slackClientSecret));
plainText: Buffer.from(data.slackClientSecret)
});
updatedData.encryptedSlackClientSecret = encryptedClientSecret; updatedData.encryptedSlackClientSecret = encryptedClientSecret;
updatedData.slackClientSecret = undefined; updatedData.slackClientSecret = undefined;
@@ -270,14 +266,14 @@ export const superAdminServiceFactory = ({
let clientId = ""; let clientId = "";
let clientSecret = ""; let clientSecret = "";
const decrypt = await kmsService.decryptWithRootKey(); const decrypt = kmsService.decryptWithRootKey();
if (serverCfg.encryptedSlackClientId) { if (serverCfg.encryptedSlackClientId) {
clientId = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientId })).toString(); clientId = decrypt(serverCfg.encryptedSlackClientId).toString();
} }
if (serverCfg.encryptedSlackClientSecret) { if (serverCfg.encryptedSlackClientSecret) {
clientSecret = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientSecret })).toString(); clientSecret = decrypt(serverCfg.encryptedSlackClientSecret).toString();
} }
return { return {

View File

@@ -12,11 +12,29 @@ Infisical is used by 10,000+ organizations across all industries including First
## Migrating from EnvKey ## Migrating from EnvKey
To facilitate customer transition from EnvKey to Infisical, we have been working closely with the EnvKey team to provide a simple migration path for all EnvKey customers. <Steps>
<Step>
Open the EnvKey dashboard and go to My Org.
![EnvKey Dashboard](../../images/guides/import-envkey/envkey-dashboard.png)
</Step>
<Step>
Go to Import/Export on the top right corner, Click on Export Org and save the exported file.
![Export organization](../../images/guides/import-envkey/envkey-export.png)
</Step>
<Step>
Click on copy to copy the encryption key and save it.
![Copy encryption key](../../images/guides/import-envkey/copy-encryption-key.png)
</Step>
<Step>
Open the Infisical dashboard and go to Organization Settings > Import.
![Infisical Organization settings](../../images/guides/import-envkey/infisical-import-dashboard.png)
</Step>
<Step>
Upload the exported file from EnvKey, paste the encryption key and click Import.
![Infisical Import EnvKey](../../images/guides/import-envkey/infisical-import-envkey.png)
</Step>
</Steps>
## Automated migration
Our team is currently working on creating an automated migration process that would include secrets, policies, and other important resources. If you are interested in that, please [reach out to our team](mailto:support@infisical.com) with any questions.
## Talk to our team ## Talk to our team

View File

@@ -0,0 +1,168 @@
---
title: "LDAP"
description: "Learn how to dynamically generate user credentials via LDAP."
---
The Infisical LDAP dynamic secret allows you to generate user credentials on demand via LDAP. The integration is general to any LDAP implementation but has been tested with OpenLDAP and Active directory as of now.
## Prerequisites
1. Create a user with the necessary permissions to create users in your LDAP server.
2. Ensure your LDAP server is reachable via Infisical instance.
## Set up Dynamic Secrets with LDAP
<Steps>
<Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select 'LDAP'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-ldap-select.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret.
</ParamField>
<ParamField path="URL" type="string" required>
LDAP url to connect to. _(Example: ldap://your-ldap-ip:389 or ldaps://domain:636)_
</ParamField>
<ParamField path="BIND DN" type="string" required>
DN to bind to. This should have permissions to create a new users.
</ParamField>
<ParamField path="BIND Password" type="string" required>
Password for the given DN.
</ParamField>
<ParamField path="CA" type="text">
CA certificate to use for TLS in case of a secure connection.
</ParamField>
<ParamField path="Creation LDIF" type="text" required>
LDIF to run while creating a user in LDAP. This can include extra steps to assign the user to groups or set permissions.
Here `{{Username}}`, `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the username and password generated by the dynamic secret.
`{{EncodedPassword}}` is the encoded password required for the `unicodePwd` field in Active Directory as described [here](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/change-windows-active-directory-user-password).
**OpenLDAP** Example:
```
dn: uid={{Username}},dc=infisical,dc=com
changetype: add
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
cn: John Doe
sn: Doe
uid: jdoe
mail: jdoe@infisical.com
userPassword: {{Password}}
```
**Active Directory** Example:
```
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
changetype: add
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
userPrincipalName: {{Username}}@infisical.com
sAMAccountName: {{Username}}
unicodePwd::{{EncodedPassword}}
userAccountControl: 66048
dn: CN=test-group,OU=Test Create,DC=infisical,DC=com
changetype: modify
add: member
member: CN={{Username}},OU=Test Create,DC=infisical,DC=com
-
```
</ParamField>
<ParamField path="Revocation LDIF" type="text" required>
LDIF to run while revoking a user in LDAP. This can include extra steps to remove the user from groups or set permissions.
Here `{{Username}}` is a templatized variable for the username generated by the dynamic secret.
**OpenLDAP / Active Directory** Example:
```
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
changetype: delete
```
</ParamField>
<ParamField path="Rollback LDIF" type="text">
LDIF to run incase Creation LDIF fails midway.
For the creation example shown above, if the user is created successfully but not added to a group, this LDIF can be used to remove the user.
Here `{{Username}}`, `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the username generated by the dynamic secret.
**OpenLDAP / Active Directory** Example:
```
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
changetype: delete
```
</ParamField>
</Step>
<Step title="Click `Submit`">
After submitting the form, you will see a dynamic secret created in the dashboard.
</Step>
<Step title="Generate dynamic secrets">
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png)
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
</Tip>
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you with an array of DN's altered depending on the Creation LDIF.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-ldap-lease.png)
</Step>
</Steps>
## Active Directory Integration
- Passwords in Active Directory are set using the `unicodePwd` field. This must be proceeded by two colons `::` as shown in the example. [Source](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/change-windows-active-directory-user-password)
- Active directory uses the `userAccountControl` field to enable account. [Read More](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/useraccountcontrol-manipulate-account-properties)
- `userAccountControl` set to `512` enables a user.
- To disable AD's password expiration for this dynamic user account. The `userAccountControl` value for this is: `65536`.
- Since `userAccountControl` flag is cumulative set it to `512 + 65536` = `66048` to do both.
- Active Directory does not permit direct modification of a user's `memberOf` attribute. The member attribute of a group and the `memberOf` attribute of a user are [linked attributes](https://learn.microsoft.com/en-us/windows/win32/ad/linked-attributes), where the member attribute represents the forward link, which can be modified. In the context of AD group membership, the group's `member` attribute serves as the forward link. Therefore, to add a newly created dynamic user to a group, a modification request must be issued to the desired group, updating its membership to include the new user.
## LDIF Entries
User account management is handled through **LDIF entries**.
#### Things to Remember
- **No trailing spaces:** Ensure there are no trailing spaces on any line, including blank lines.
- **Empty lines before modify blocks:** Every modify block must be preceded by an empty line.
- **Multiple modifications:** You can define multiple modifications for a DN within a single modify block. Each modification should end with a single dash (`-`).

View File

@@ -39,7 +39,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO."
</Step> </Step>
<Step title="Finish configuring OIDC in Infisical"> <Step title="Finish configuring OIDC in Infisical">
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Manage**. 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Connect**.
![OIDC auth0 manage org Infisical](../../../images/sso/auth0-oidc/org-oidc-overview.png) ![OIDC auth0 manage org Infisical](../../../images/sso/auth0-oidc/org-oidc-overview.png)
3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **Client ID**, and **Client Secret** from step 2.1 and 2.2. 3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **Client ID**, and **Client Secret** from step 2.1 and 2.2.
@@ -54,6 +54,19 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO."
![OIDC auth0 enable OIDC](../../../images/sso/auth0-oidc/enable-oidc.png) ![OIDC auth0 enable OIDC](../../../images/sso/auth0-oidc/enable-oidc.png)
</Step> </Step>
<Step title="Enforce OIDC SSO in Infisical">
Enforcing OIDC SSO ensures that members in your organization can only access Infisical
by logging into the organization via Auth0.
To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Auth0 user with Infisical.
Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO.
<Warning>
We recommend ensuring that your account is provisioned using the application in Auth0
prior to enforcing OIDC SSO to prevent any unintended issues.
</Warning>
</Step>
</Steps> </Steps>
<Note> <Note>

View File

@@ -28,7 +28,7 @@ Prerequisites:
1.4. Access the IdPs OIDC discovery document (usually located at `https://<idp-domain>/.well-known/openid-configuration`). This document contains important endpoints such as authorization, token, userinfo, and keys. 1.4. Access the IdPs OIDC discovery document (usually located at `https://<idp-domain>/.well-known/openid-configuration`). This document contains important endpoints such as authorization, token, userinfo, and keys.
</Step> </Step>
<Step title="Finish configuring OIDC in Infisical"> <Step title="Finish configuring OIDC in Infisical">
2.1. Back in Infisical, in the Organization settings > Security > OIDC, click Manage 2.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect.
![OIDC general manage org Infisical](../../../images/sso/general-oidc/org-oidc-manage.png) ![OIDC general manage org Infisical](../../../images/sso/general-oidc/org-oidc-manage.png)
2.2. You can configure OIDC either through the Discovery URL (Recommended) or by inputting custom endpoints. 2.2. You can configure OIDC either through the Discovery URL (Recommended) or by inputting custom endpoints.
@@ -53,9 +53,20 @@ Prerequisites:
<Step title="Enable OIDC SSO in Infisical"> <Step title="Enable OIDC SSO in Infisical">
Enabling OIDC SSO allows members in your organization to log into Infisical via the configured Identity Provider Enabling OIDC SSO allows members in your organization to log into Infisical via the configured Identity Provider
![OIDC general enable OIDC](../../../images/sso/general-oidc/org-oidc-enable.png) ![OIDC general enable OIDC](../../../images/sso/general-oidc/org-oidc-enable.png)
</Step>
</Step> <Step title="Enforce OIDC SSO in Infisical">
Enforcing OIDC SSO ensures that members in your organization can only access Infisical
by logging into the organization via the Identity provider.
To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one IdP user with Infisical.
Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO.
<Warning>
We recommend ensuring that your account is provisioned using the identity provider prior to enforcing OIDC SSO to prevent any unintended issues.
</Warning>
</Step>
</Steps> </Steps>

View File

@@ -65,7 +65,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
</Step> </Step>
<Step title="Finish configuring OIDC in Infisical"> <Step title="Finish configuring OIDC in Infisical">
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Manage 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect.
![OIDC keycloak manage org Infisical](../../../images/sso/keycloak-oidc/manage-org-oidc.png) ![OIDC keycloak manage org Infisical](../../../images/sso/keycloak-oidc/manage-org-oidc.png)
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**. 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**.
@@ -80,6 +80,19 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
![OIDC keycloak enable OIDC](../../../images/sso/keycloak-oidc/enable-oidc.png) ![OIDC keycloak enable OIDC](../../../images/sso/keycloak-oidc/enable-oidc.png)
</Step> </Step>
<Step title="Enforce OIDC SSO in Infisical">
Enforcing OIDC SSO ensures that members in your organization can only access Infisical
by logging into the organization via Keycloak.
To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Keycloak user with Infisical.
Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO.
<Warning>
We recommend ensuring that your account is provisioned using the application in Keycloak
prior to enforcing OIDC SSO to prevent any unintended issues.
</Warning>
</Step>
</Steps> </Steps>
<Note> <Note>

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 754 KiB

After

Width:  |  Height:  |  Size: 797 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 746 KiB

After

Width:  |  Height:  |  Size: 780 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 743 KiB

After

Width:  |  Height:  |  Size: 797 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 741 KiB

After

Width:  |  Height:  |  Size: 780 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 751 KiB

After

Width:  |  Height:  |  Size: 797 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 744 KiB

After

Width:  |  Height:  |  Size: 780 KiB

View File

@@ -168,7 +168,8 @@
"documentation/platform/dynamic-secrets/aws-iam", "documentation/platform/dynamic-secrets/aws-iam",
"documentation/platform/dynamic-secrets/mongo-atlas", "documentation/platform/dynamic-secrets/mongo-atlas",
"documentation/platform/dynamic-secrets/mongo-db", "documentation/platform/dynamic-secrets/mongo-db",
"documentation/platform/dynamic-secrets/azure-entra-id" "documentation/platform/dynamic-secrets/azure-entra-id",
"documentation/platform/dynamic-secrets/ldap"
] ]
}, },
{ {

View File

@@ -0,0 +1,141 @@
import { useRouter } from "next/router";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Modal, ModalContent, ModalTrigger, Select, SelectItem } from "@app/components/v2";
enum Region {
US = "us",
EU = "eu"
}
const regions = [
{
value: Region.US,
label: "United States",
location: "Virginia, USA",
flag: (
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-us" viewBox="0 0 640 480">
<path fill="#bd3d44" d="M0 0h640v480H0" />
<path
stroke="#fff"
strokeWidth="37"
d="M0 55.3h640M0 129h640M0 203h640M0 277h640M0 351h640M0 425h640"
/>
<path fill="#192f5d" d="M0 0h364.8v258.5H0" />
<marker id="us-a" markerHeight="30" markerWidth="30">
<path fill="#fff" d="m14 0 9 27L0 10h28L5 27z" />
</marker>
<path
fill="none"
markerMid="url(#us-a)"
d="m0 0 16 11h61 61 61 61 60L47 37h61 61 60 61L16 63h61 61 61 61 60L47 89h61 61 60 61L16 115h61 61 61 61 60L47 141h61 61 60 61L16 166h61 61 61 61 60L47 192h61 61 60 61L16 218h61 61 61 61 60z"
/>
</svg>
)
},
{
value: Region.EU,
label: "Europe",
location: "Frankfurt, Germany",
flag: (
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-eu" viewBox="0 0 512 512">
<defs>
<g id="eu-d">
<g id="eu-b">
<path id="eu-a" d="m0-1-.3 1 .5.1z" />
<use xlinkHref="#eu-a" transform="scale(-1 1)" />
</g>
<g id="eu-c">
<use xlinkHref="#eu-b" transform="rotate(72)" />
<use xlinkHref="#eu-b" transform="rotate(144)" />
</g>
<use xlinkHref="#eu-c" transform="scale(-1 1)" />
</g>
</defs>
<path fill="#039" d="M0 0h512v512H0z" />
<g fill="#fc0" transform="translate(256 258.4)scale(25.28395)">
<use xlinkHref="#eu-d" width="100%" height="100%" y="-6" />
<use xlinkHref="#eu-d" width="100%" height="100%" y="6" />
<g id="eu-e">
<use xlinkHref="#eu-d" width="100%" height="100%" x="-6" />
<use xlinkHref="#eu-d" width="100%" height="100%" transform="rotate(-144 -2.3 -2.1)" />
<use xlinkHref="#eu-d" width="100%" height="100%" transform="rotate(144 -2.1 -2.3)" />
<use xlinkHref="#eu-d" width="100%" height="100%" transform="rotate(72 -4.7 -2)" />
<use xlinkHref="#eu-d" width="100%" height="100%" transform="rotate(72 -5 .5)" />
</g>
<use xlinkHref="#eu-e" width="100%" height="100%" transform="scale(-1 1)" />
</g>
</svg>
)
}
];
export const RegionSelect = () => {
const router = useRouter();
const handleRegionSelect = (value: Region) => {
router.push(`https://${value}.infisical.com/${router.pathname}`);
};
const [subdomain, domain] = window.location.host.split(".");
// only display region select for cloud
if (!domain?.match(/infisical/)) return null;
// default to US if not eu
const currentRegion = subdomain === Region.EU ? regions[1] : regions[0];
return (
<div className="mb-8 flex flex-col items-center">
<Select
className="w-44"
onValueChange={handleRegionSelect}
defaultValue={currentRegion.value}
>
{regions.map(({ value, label, flag }) => (
<SelectItem value={value} key={value}>
<div className="flex items-center gap-2">
<div className="w-4">{flag}</div>
{label}
</div>
</SelectItem>
))}
</Select>
<Modal>
<ModalTrigger>
<button type="button" className="mt-1 text-right text-xs text-mineshaft-400 underline">
Help me pick a data region
</button>
</ModalTrigger>
<ModalContent
title="Infisical Cloud data regions"
subTitle="Select the closest region to you and your team. Contact Infisical if you need to migrate regions."
>
{regions.map(({ value, label, location, flag }) => (
<div className="mb-6" key={value}>
<p className="font-medium">
<span className="mr-2 inline-block w-4">{flag}</span>
{value.toUpperCase()} Region
</p>
<ul className="ml-6 mt-2 flex flex-col gap-1">
<li>
<FontAwesomeIcon size="xs" className="mr-0.5 text-green" icon={faCheck} /> Fastest
option if you are based in {value === Region.US ? "the" : ""} {label}
</li>
<li>
<FontAwesomeIcon size="xs" className="mr-0.5 text-green" icon={faCheck} /> Data
storage compliance for this region
</li>
<li>
<FontAwesomeIcon size="xs" className="mr-0.5 text-green" icon={faCheck} /> Hosted
in {location}
</li>
</ul>
</div>
))}
</ModalContent>
</Modal>
</div>
);
};

View File

@@ -4,6 +4,7 @@ import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons
import { faEnvelope } from "@fortawesome/free-regular-svg-icons"; import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { RegionSelect } from "@app/components/navigation/RegionSelect";
import { useServerConfig } from "@app/context"; import { useServerConfig } from "@app/context";
import { LoginMethod } from "@app/hooks/api/admin/types"; import { LoginMethod } from "@app/hooks/api/admin/types";
@@ -25,6 +26,7 @@ export default function InitialSignupStep({
<h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent"> <h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
{t("signup.initial-title")} {t("signup.initial-title")}
</h1> </h1>
<RegionSelect />
{shouldDisplaySignupMethod(LoginMethod.GOOGLE) && ( {shouldDisplaySignupMethod(LoginMethod.GOOGLE) && (
<div className="w-1/4 min-w-[20rem] rounded-md lg:w-1/6"> <div className="w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button <Button

View File

@@ -24,7 +24,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
{...props} {...props}
ref={forwardedRef} ref={forwardedRef}
className={twMerge( className={twMerge(
"z-30 min-w-[220px] rounded-md border border-mineshaft-600 bg-mineshaft-900 text-bunker-300 shadow will-change-auto data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade", "z-30 min-w-[220px] overflow-y-auto rounded-md border border-mineshaft-600 bg-mineshaft-900 text-bunker-300 shadow will-change-auto data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
className className
)} )}
> >

View File

@@ -77,7 +77,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const debouncedValue = useDebounce(value, 500); const [debouncedValue] = useDebounce(value, 500);
const [highlightedIndex, setHighlightedIndex] = useState(-1); const [highlightedIndex, setHighlightedIndex] = useState(-1);

View File

@@ -33,7 +33,7 @@ export const SecretPathInput = ({
const [suggestions, setSuggestions] = useState<string[]>([]); const [suggestions, setSuggestions] = useState<string[]>([]);
const [isInputFocused, setIsInputFocus] = useState(false); const [isInputFocused, setIsInputFocus] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState(-1); const [highlightedIndex, setHighlightedIndex] = useState(-1);
const debouncedInputValue = useDebounce(inputValue, 200); const [debouncedInputValue] = useDebounce(inputValue, 200);
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";

View File

@@ -56,7 +56,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
isDisabled && "cursor-not-allowed opacity-50" isDisabled && "cursor-not-allowed opacity-50"
)} )}
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2 overflow-hidden text-ellipsis whitespace-nowrap">
{props.icon && <FontAwesomeIcon icon={props.icon} />} {props.icon && <FontAwesomeIcon icon={props.icon} />}
<SelectPrimitive.Value placeholder={placeholder} /> <SelectPrimitive.Value placeholder={placeholder} />
</div> </div>
@@ -72,7 +72,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
className={twMerge( className={twMerge(
"relative top-1 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md", "relative top-1 z-[100] max-w-sm overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md",
position === "popper" && "max-h-72", position === "popper" && "max-h-72",
dropdownContainerClassName dropdownContainerClassName
)} )}
@@ -122,8 +122,8 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
{...props} {...props}
className={twMerge( className={twMerge(
`relative mb-0.5 flex `relative mb-0.5 flex
cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm cursor-pointer select-none items-center overflow-hidden text-ellipsis whitespace-nowrap rounded-md py-2
outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`, pl-10 pr-4 text-sm outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
isSelected && "bg-primary", isSelected && "bg-primary",
isDisabled && isDisabled &&
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600", "cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",

View File

@@ -7,6 +7,30 @@ export enum ProjectPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
$REGEX = "$regex",
$EQ = "$eq",
$NEQ = "$neq",
$GLOB = "$glob"
}
export type TPermissionConditionOperators = {
[PermissionConditionOperators.$IN]: string[];
[PermissionConditionOperators.$ALL]: string[];
[PermissionConditionOperators.$EQ]: string;
[PermissionConditionOperators.$NEQ]: string;
[PermissionConditionOperators.$REGEX]: string;
[PermissionConditionOperators.$GLOB]: string;
};
export type TPermissionCondition = Record<
string,
| string
| { $in: string[]; $all: string[]; $regex: string; $eq: string; $neq: string; $glob: string }
>;
export enum ProjectPermissionSub { export enum ProjectPermissionSub {
Role = "role", Role = "role",
Member = "member", Member = "member",

View File

@@ -48,21 +48,15 @@ export const dashboardKeys = {
}; };
export const fetchProjectSecretsOverview = async ({ export const fetchProjectSecretsOverview = async ({
includeFolders,
includeSecrets,
includeDynamicSecrets,
environments, environments,
...params ...params
}: TGetDashboardProjectSecretsOverviewDTO) => { }: TGetDashboardProjectSecretsOverviewDTO) => {
const { data } = await apiRequest.get<DashboardProjectSecretsOverviewResponse>( const { data } = await apiRequest.get<DashboardProjectSecretsOverviewResponse>(
"/api/v3/dashboard/secrets-overview", "/api/v1/dashboard/secrets-overview",
{ {
params: { params: {
...params, ...params,
environments: encodeURIComponent(environments.join(",")), environments: encodeURIComponent(environments.join(","))
includeFolders: includeFolders ? "1" : "",
includeSecrets: includeSecrets ? "1" : "",
includeDynamicSecrets: includeDynamicSecrets ? "1" : ""
} }
} }
); );
@@ -71,22 +65,14 @@ export const fetchProjectSecretsOverview = async ({
}; };
export const fetchProjectSecretsDetails = async ({ export const fetchProjectSecretsDetails = async ({
includeFolders,
includeImports,
includeSecrets,
includeDynamicSecrets,
tags, tags,
...params ...params
}: TGetDashboardProjectSecretsDetailsDTO) => { }: TGetDashboardProjectSecretsDetailsDTO) => {
const { data } = await apiRequest.get<DashboardProjectSecretsDetailsResponse>( const { data } = await apiRequest.get<DashboardProjectSecretsDetailsResponse>(
"/api/v3/dashboard/secrets-details", "/api/v1/dashboard/secrets-details",
{ {
params: { params: {
...params, ...params,
includeImports: includeImports ? "1" : "",
includeFolders: includeFolders ? "1" : "",
includeSecrets: includeSecrets ? "1" : "",
includeDynamicSecrets: includeDynamicSecrets ? "1" : "",
tags: encodeURIComponent( tags: encodeURIComponent(
Object.entries(tags) Object.entries(tags)
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars

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