Compare commits

...

349 Commits

Author SHA1 Message Date
9fc5303a97 Merge pull request #1053 from Infisical/update-docs
Update Platform Documentation
2023-10-04 11:01:47 +01:00
97a5b509b7 Point getting started SDK to SDK repos to avoid docs sprawl 2023-10-04 10:57:11 +01:00
7660119584 Merge remote-tracking branch 'origin' into update-docs 2023-10-04 10:44:38 +01:00
34273b30f2 Finish update for platform docs 2023-10-04 10:25:54 +01:00
49098b7693 update folder name of build tool integrations 2023-10-03 20:12:03 -04:00
501d940558 add Gradle docs 2023-10-03 20:00:50 -04:00
7234c014c8 Merge pull request #1048 from akhilmhdh:fix/remove-import-permission-token
fix: resolved permission check on imported secrets when using service token
2023-10-03 12:38:12 -07:00
f3908e6b2a fix: resolved permission check on imported secrets when using service token 2023-10-03 20:14:43 +05:30
cf4eb629f2 Start revising project docs 2023-10-03 08:58:13 +01:00
bd8c17d720 resolve merge conflict 2023-10-02 18:58:38 -04:00
d3bc95560c Merge remote-tracking branch 'origin' into update-docs 2023-10-02 20:57:03 +01:00
4a838b788f Start comb docs, integrations, intro, organization 2023-10-02 20:26:16 +01:00
01c655699c Merge pull request #1045 from Infisical/proper-server-cleanup
handle siginit and sigterm
2023-10-02 11:44:36 -07:00
3456dfbd86 handle siginit and sigterm 2023-10-02 11:40:00 -07:00
560bde297c Merge pull request #1040 from akhilmhdh/fix/path-wrong-name
fix: resolved dashboard showing text folderName in nesting folders
2023-10-02 07:45:01 -07:00
3b3f78ee3c Start revising docs 2023-10-01 16:42:40 +01:00
04c74293ed fix: resolved dashboard showing text folderName in nesting folders 2023-09-30 23:47:38 +05:30
66aa218ad9 Merge pull request #1033 from Infisical/snyk-fix-f1bf0685a5c66fa5416b87a6ef8520e1
[Snyk] Security upgrade sharp from 0.32.1 to 0.32.6
2023-09-29 15:05:46 -07:00
fb3a386aa3 Merge pull request #1034 from akhilmhdh/feat/patch-dashboardv3
feat(dashboard-v3): patched dashboard copy sec bug
2023-09-29 08:21:12 -07:00
2cf5fd80ca feat(dashboard-v3): removed a line at top on empty state 2023-09-29 14:31:31 +05:30
74534cfbaa feat(dashboard-v3): patched dashboard copy sec bug and add secret in empty state 2023-09-29 13:34:44 +05:30
66787b1f93 fix secret scanning zod error for installationId 2023-09-28 23:09:12 -07:00
890082acbc Update service-token.mdx 2023-09-28 22:11:24 -07:00
a364b174e0 add expire time to service token create 2023-09-28 19:31:33 -07:00
2bb2ccc19e patch crypto in create service token in cli 2023-09-28 19:27:38 -07:00
3bbf770027 bug fixes for v3 secret apis 2023-09-28 12:11:26 -07:00
2610356d45 Merge pull request #1018 from akhilmhdh/feat/dashboard-v3
Feat/dashboard v3
2023-09-28 10:35:35 -07:00
67e164e2bb feat(dashboard-v3): z-index change in tooltip for drawer 2023-09-28 22:29:50 +05:30
84fcb82116 fix: frontend/package.json & frontend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-SHARP-5922108
2023-09-28 16:52:33 +00:00
4502d12e46 feat(dashboard-v3): typo fix 2023-09-28 21:28:43 +05:30
ef6ee6b2e6 feat(dashboard-v3): resolved create secret issue and allow empty secret values in create secret 2023-09-28 21:28:43 +05:30
e902a54af0 remove deprecated EELogService 2023-09-28 21:28:43 +05:30
50efb8b8bd feat: resolved minor issues with dashboard v3 on feedback 2023-09-28 21:28:43 +05:30
5450c1126a minor style updates to the dashboard 2023-09-28 21:28:43 +05:30
4929022523 fix: resolved trimming keys but keeping last line break for ssh keys and added skip encoding on integration sync 2023-09-28 21:28:43 +05:30
85378e25aa feat: updated input create secret style and some more updates on style 2023-09-28 21:28:43 +05:30
b54c29fc48 feat(dashboard-v3): implemented new the dashboard with v3 support 2023-09-28 21:28:43 +05:30
fcf3f2837e feat(dashboard-v3): updated ui components and hooks for new migrated apis and v3 apis 2023-09-28 21:28:43 +05:30
0ada343b6f feat(dashboard-v3): migrated folder, imports and snapshots to use only secret path and not folder id 2023-09-28 21:28:06 +05:30
d0b8aba990 Merge pull request #1030 from G3root/update-other
fix: renaming environments not updated in some models
2023-09-28 07:58:05 -07:00
4365be9b75 Merge pull request #1031 from akhilmhdh/feat/secret-approval
Secret approval policies feature
2023-09-27 23:56:16 -07:00
b0c398688b feat(secret-approval): updated names to secret policy and fixed approval number bug 2023-09-28 12:23:01 +05:30
1141408d5b add exit codes for errors 2023-09-27 21:42:34 -07:00
b24bff5af6 Update service-token.mdx 2023-09-27 21:17:28 -07:00
a1dc405516 Merge pull request #1032 from Infisical/service-token-v2-create-cli
add create service token to cli + docs for it
2023-09-27 21:09:24 -07:00
896a34eb65 add create service token to cli + docs for it 2023-09-27 21:07:54 -07:00
c67432a56f feat(secret-approval): implemented frontend ui for secret policies 2023-09-27 23:10:45 +05:30
edeb6bbc66 feat(secret-approval): implemented backend api for secret policies 2023-09-27 23:10:28 +05:30
77ec17ccd4 fix: update many query 2023-09-27 17:01:02 +05:30
6e992858aa fix: add renamed fields to other models 2023-09-27 15:12:32 +05:30
9cda85f03e checkpoint 2023-09-27 11:50:52 +05:30
ddae305fdb Merge pull request #1026 from akhilmhdh/feat/get-import-sec
feat: added support for getting imported secrets in v3 getSecret api
2023-09-26 22:01:35 -07:00
8265d18934 nit: add lean option 2023-09-26 22:00:00 -07:00
4c1324baa9 feat: added support for getting imported secrets in v3 getSecret api 2023-09-26 12:25:23 +05:30
5128466233 update go.mod after Infisical/go-keyring update 2023-09-25 15:28:13 -07:00
e11abb619a use v1.0.2 of internal keyring in cli 2023-09-25 15:10:59 -07:00
f51e9ba8ff add back role migration 2023-09-25 11:51:25 -07:00
a255af6ad8 Merge pull request #1022 from Infisical/debug-vercel-integration
Patch Vercel integration for custom preview branches
2023-09-23 11:42:38 +01:00
30da2e50b1 Patch Vercel integration for custom preview branches 2023-09-23 11:38:49 +01:00
7f9bd93382 Merge pull request #1004 from vwbusguy/bugfix/no-auto-capitalization-support
Fix no Auto-Capitalization for secrets get/set. Fixes #1003
2023-09-22 13:28:15 -07:00
e81ea314e1 update go minor version 2023-09-22 13:26:32 -07:00
f19aca2904 fix zod type for ToggleAutoCapitalizationV2 2023-09-22 13:10:11 -07:00
763bdabd60 Merge pull request #998 from Infisical/qovery-integration
Added Qovery integration
2023-09-22 12:44:06 -07:00
7ec708b71d Merge pull request #1019 from akhilmhdh/fix/audit-log
feat: made audit log options back
2023-09-22 12:24:42 -07:00
3c6c1891a8 feat: made audit log options back 2023-09-22 22:07:48 +05:30
01d3d84b40 Merge pull request #1017 from Infisical/debug-self-hosted-gitlab
Patch self-hosted gitlab integration
2023-09-22 15:47:05 +01:00
32bec03adf Patch self-hosted gitlab integration 2023-09-22 15:19:26 +01:00
5b6c2e05f2 Merge pull request #1016 from Infisical/fix-teamcity
Fix TeamCity integration blank screen issue
2023-09-22 12:11:59 +01:00
c623f572b7 Fix TeamCity integration 2023-09-22 12:06:42 +01:00
48f7bd146f added bun docs 2023-09-20 23:53:31 -07:00
da6fa6d8ce added bun docs 2023-09-20 23:44:15 -07:00
cf8e597c7d Update bun.mdx 2023-09-20 23:34:12 -07:00
43c31332e4 added bun docs 2023-09-20 19:15:58 -07:00
88fbf6f88e added bun docs 2023-09-20 19:06:45 -07:00
119730ac1a Update build-docker-image-to-prod.yml 2023-09-20 17:06:12 -04:00
1d66dbbce3 Merge pull request #1010 from akhilmhdh:code-editor-fix
Stable multiline input
2023-09-20 16:26:05 -04:00
b0991c33b0 Merge pull request #1013 from Infisical:fix-github-sso-email
Update method to obtain email for GitHub SSO
2023-09-20 16:09:36 -04:00
d863dece79 Merge pull request #1012 from akhilmhdh/feat/accordion-component
fix: resolved broken style of accordion component and added storybook
2023-09-20 17:46:20 +01:00
96fbc6c5a0 Update method to obtain email for GitHub SSO 2023-09-20 17:38:00 +01:00
a93631d41c fix: resolved broken style of accordion component and added storybook 2023-09-20 20:59:34 +05:30
2c7aac37a2 feat: resolved trailing whitespace not showing up 2023-09-20 17:05:57 +05:30
6b8d4c2fea fix: padding 2023-09-20 17:05:57 +05:30
f84235eea3 fix: scroll 2023-09-20 17:05:57 +05:30
63e8ecce5b fix: break 2023-09-20 17:05:57 +05:30
ef7bf09398 fix: trimming 2023-09-20 17:05:57 +05:30
3be3867579 fix: font size 2023-09-20 17:05:57 +05:30
7f753b23f8 fix: secret input 2023-09-20 17:05:57 +05:30
81827e2deb chore: remove content editable 2023-09-20 17:05:57 +05:30
f02ea8d9b8 Merge pull request #1006 from vwbusguy/bugfix/helm-chart-frontend-resources
Update cpu default frontend value.  Fixes #1005.
2023-09-19 12:46:13 -04:00
1609bd4652 update chart version and frontend cpu 2023-09-19 12:45:07 -04:00
a620f1c924 add workspace index for SecretBlindIndexData 2023-09-19 11:46:37 -04:00
0a3e7731d9 Merge pull request #1009 from Infisical/google-github-sso-docs
Add Google and GitHub SSO configuration docs
2023-09-19 16:00:52 +01:00
0ca8425965 Add enterprise notice to SAML SSO docs 2023-09-19 15:54:56 +01:00
14a260b785 Finish adding docs for Google SSO and GitHub SSO configuration 2023-09-19 15:53:19 +01:00
b6219e14f0 Finish optimizing qovery integration, add docs for it 2023-09-19 14:43:20 +01:00
663c4869b9 Merge pull request #1008 from Infisical/fix-vercel-integration
Patch integrations involving teamId
2023-09-19 14:09:14 +01:00
3103075c3f Bring back missing teamId when fetching integrationAuth 2023-09-19 14:06:06 +01:00
e3ef826f52 Update qovery integration 2023-09-19 13:50:01 +01:00
215ef0bb29 Merge pull request #975 from JanetEne/update-use-secret-path-across-app
update and use secret path examples where applicable across app
2023-09-18 21:36:46 -07:00
9cc220e51f Merge branch 'main' into update-use-secret-path-across-app 2023-09-18 21:31:34 -07:00
8fa90d94ac Deleted console.log 2023-09-18 21:29:49 -07:00
609204f7f6 Merge pull request #987 from Infisical/permissioning-style-improvements
change certain permissioning text
2023-09-18 21:17:35 -07:00
d501130e64 Update secret-reference.mdx 2023-09-18 19:30:33 -07:00
45734d78c0 Update cpu default frontend value. Fixed #1005.
Signed-off-by: Scott Williams <scottwilliams@ucsb.edu>
2023-09-18 19:14:14 -07:00
dd9a2dd345 Update secret-reference.mdx 2023-09-18 18:44:08 -07:00
80bec24219 Fix no Auto-Capitalization for secrets get/set. Fixes https://github.com/Infisical/infisical/issues/1003
Signed-off-by: Scott Williams <scottwilliams@ucsb.edu>
2023-09-18 15:07:58 -07:00
4765dd0696 remove backfillPermission (no longer needed) 2023-09-18 12:50:40 -04:00
0d4cacdc3e remove old ValidateEnvironmentName check 2023-09-18 12:26:09 -04:00
0b59a92dfb Added Qovery integration 2023-09-17 18:15:47 -07:00
64d5a82e1b Merge pull request #988 from Infisical/integrations-uiux-updates
improved ui/ux for checkly, gcp, and vault integrations
2023-09-16 18:40:00 +01:00
a89ed40dcd Update Fly.io integration authorization page to use react-form 2023-09-16 18:13:55 +01:00
1634f9ec49 Merge pull request #996 from akhilmhdh/fix/integration-revoke
fix: integration bot deactive resolved and redis type error
2023-09-16 14:26:43 +01:00
dd1bb84361 fix: patched auto bot removal for integration 2023-09-16 18:03:13 +05:30
0f003e8ab6 Add type assertion for GitLab selfhosted URL 2023-09-16 12:42:23 +01:00
14d253d01a Update GCP SM and GitLab integrations to have overwrite popup warning 2023-09-16 12:36:29 +01:00
750c1b46da fix: integration bot deactive resolved and redis type error 2023-09-16 16:47:41 +05:30
2e07512bae Merge remote-tracking branch 'origin' into integrations-uiux-updates 2023-09-16 09:27:49 +01:00
20a6497218 Fixed UI for TeamCity, Fly.io, Render integrations 2023-09-15 22:18:25 -07:00
72839719fd Fixed UI for the AWS integrations 2023-09-15 21:43:46 -07:00
d8d480f2bc edge case: read write for non existing env in deniedPermissions 2023-09-15 22:32:42 -04:00
58c3a4ebc1 Merge branch 'main' into integrations-uiux-updates 2023-09-15 18:22:29 -07:00
0d83954c39 fixed merged conflicts for gitlab and added animation for tabs 2023-09-15 18:18:40 -07:00
80cee40b39 Updated UI for CircleCI integration 2023-09-15 17:53:58 -07:00
6059070d29 Updated UI for Vercel, GCP, GitLab, Checkly, Vault integrations 2023-09-15 17:24:30 -07:00
f16944024b Update build-staging-img.yml 2023-09-15 20:18:05 -04:00
29da8843a3 add prefix for role name 2023-09-15 20:11:06 -04:00
8cd6a1f564 no release lock after backfill complete 2023-09-15 18:56:41 -04:00
e8fd3c8045 update lock time for permission backfill 2023-09-15 18:14:31 -04:00
59cd8580d5 bring back tests for CI 2023-09-15 17:56:12 -04:00
859cec49d1 make redis client conditional 2023-09-15 17:55:43 -04:00
fccbf9810f fixed gcp integration UIUX 2023-09-15 14:30:37 -07:00
5494bc6c3c Update build-staging-img.yml 2023-09-15 17:22:54 -04:00
95385b1f45 Merge pull request #991 from akhilmhdh/feat/rbac-migratio
feat(rbac): migration script for permission from old permission to new
2023-09-15 17:09:51 -04:00
b88a319582 add lock mechanism for backfillPermission script 2023-09-15 16:39:59 -04:00
db5883ae56 fix merge conflicts 2023-09-15 13:28:11 -07:00
26229b07bc Merge pull request #994 from Infisical/integration-options
Finish integration options/react form refactor for GitLab and GCP SM …
2023-09-15 21:00:55 +01:00
3ab5db9b2a Finish integration options/react form refactor for GitLab and GCP SM integrations, add docs for it 2023-09-15 20:53:31 +01:00
717b831e94 Merge pull request #992 from serin0837/parse-env-file
fix hyphen env variable import bug
2023-09-15 11:45:36 -07:00
336b5897f0 update role description 2023-09-15 12:40:40 -04:00
0ce5aaf61c add role deduplication logic 2023-09-15 11:44:48 -04:00
adfa90340d remove unsetting deniedPermissions 2023-09-15 09:53:17 -04:00
444aca0070 fix hyphen env variable import bug 2023-09-15 14:52:30 +01:00
029766c534 feat(rbac): migration script for permission from old permission to new 2023-09-15 16:20:21 +05:30
bde788c4f6 improved ui/ux for checkly, gcp, and vault integrations 2023-09-14 22:21:59 -07:00
9b14b64ec2 Merge pull request #983 from ragnarbull/main
Docs: Update FAQ for Alpine CDN error
2023-09-15 00:31:14 -04:00
0a72dccdcf add back defaultOpen="true" 2023-09-15 00:30:15 -04:00
7fe94d66cd Create new FAQ page under developer setup docs 2023-09-15 11:59:15 +10:00
f503f8c76d Merge pull request #985 from xphyr/main
changing CMD for Dockerfile  to address issue #984
2023-09-14 19:32:09 -04:00
7982b1d668 replace owner role for local dev user 2023-09-14 19:27:23 -04:00
7a78209613 Merge pull request #977 from akhilmhdh:feat/permission-patch-2
feat(rbac): removed owner role and changed member permissions
2023-09-14 18:57:02 -04:00
019024e4ae remove the use of owner everywhere else 2023-09-14 18:23:59 -04:00
4d6895a793 Merge pull request #933 from MohamadTahir:add_resource_probs_to_deployments
Add resource specification to frontend and backend deployment containers
2023-09-14 16:36:51 -04:00
36b5ba2855 remove change log bc will get replaced by auto generated one soon 2023-09-14 16:35:13 -04:00
44d2a6c553 clearing npm cache to save space 2023-09-14 13:57:58 -04:00
a073a746f2 changing CMD for Dockerfile to use node instead of npm to address issue #984 2023-09-14 13:33:15 -04:00
edb3e66267 fix(integrations): resolved integration bot deactive revoke bug 2023-09-14 21:56:57 +05:30
75be302166 Merge pull request #969 from quinton11:feat/secrets-get-cmd-path
fix: `path` option for `secret get` sub cmd
2023-09-14 12:22:16 -04:00
b459d2d5f5 Merge pull request #954 from jessebot/patch-1
Update values.yaml - add `mongodb.auth.existingSecret` parameter to helm chart
2023-09-14 12:08:03 -04:00
942e1a82c2 feat(rbac): removed audit log option for time being, v3 secret patch and reload permission flash screen fix 2023-09-14 21:34:13 +05:30
9d2bc25cb4 Merge pull request #978 from hcourdent/patch-1
Backlink to Windmill.dev in windmill.mdx
2023-09-14 11:35:52 -04:00
13083d7676 remove link from description - anti pattern in docs 2023-09-14 11:35:33 -04:00
18d843f3e6 feat(rbac): fixed ip allow list api 2023-09-14 20:40:57 +05:30
ee96325034 Update FAQ for Alpine CDN error 2023-09-14 23:24:32 +10:00
954f15e4df Merge pull request #980 from Infisical/fix-gitlab-integration-self-hosted
Update all GitLab integration URLs to point to self-hosted instances if applicable (else GitLab Cloud)
2023-09-14 11:42:00 +01:00
88842951cb Change +metadata to metadata 2023-09-14 11:16:59 +01:00
8e88a3a25f Point getTeamsGitLab method to self-hosted GitLab instance if applicable 2023-09-14 10:59:09 +01:00
f1e1ca07df Backlink to Windmill.dev in windmill.mdx 2023-09-14 11:20:44 +02:00
5bf2c2f52b Point getAppsGitLab method to self-hosted GitLab instance if applicable, fix integration revocation issue related to missing metadata param partly 2023-09-14 10:14:10 +01:00
3d2a2651b8 feat(rbac): removed owner role and changed member permissions 2023-09-14 13:20:42 +05:30
0f02ef701e change some permissioning text 2023-09-13 19:02:24 -07:00
1c5e80e68a update file naming 2023-09-14 02:33:08 +01:00
c30381edbc uchange component name to globpatternexample 2023-09-14 02:17:33 +01:00
2554ad2b3c update and use secret path examples where applicable across app 2023-09-14 01:51:05 +01:00
c3696bdbbc Merge pull request #972 from akhilmhdh/feat/permission-patch
feat(rbac): grouped folder and imports permission into secret permission
2023-09-13 13:33:26 -04:00
1be924d210 Update kubernetes.mdx 2023-09-13 10:10:45 -07:00
2333675262 feat(rbac): grouped folder and imports permission into secret permission 2023-09-13 22:30:56 +05:30
c3c16f4e42 Merge pull request #966 from serin0837/gcp-secret-manager-add-labels
add label managed by infisical
2023-09-13 10:12:16 +01:00
99a2203b38 Update GCP SM docs to include note on Cloud Resource Manager API enabled requirement and label 2023-09-13 10:10:50 +01:00
92b64d3553 Merge remote-tracking branch 'origin' into gcp-secret-manager-add-labels 2023-09-13 09:50:26 +01:00
533e628183 Merge pull request #973 from Infisical/expose-folders-api
Implement workaround for swagger-autogen edgecase and expose folders,…
2023-09-13 09:44:35 +01:00
3b7096710c Fix merge conflicts 2023-09-13 09:42:36 +01:00
97b7a5ebdf Fix merge conflicts 2023-09-13 09:35:53 +01:00
4f69257595 Implement workaround for swagger-autogen edgecase and expose folders, secret imports API 2023-09-13 09:28:29 +01:00
8f93141d54 Merge pull request #962 from G3root/fix-login
fix: overflow issue login page
2023-09-12 16:57:07 -07:00
2bbba8a43a Merge pull request #970 from zenorocha/resend
Add Resend instructions
2023-09-12 16:34:45 -07:00
4da251bdfc Add Resend instructions 2023-09-12 16:21:04 -07:00
8520ae8d43 Update .infisicalignore 2023-09-12 19:05:29 -04:00
e38abb128a Update .infisicalignore 2023-09-12 19:05:03 -04:00
d46a6f7270 fix: path option for secret get sub cmd 2023-09-12 22:11:15 +00:00
33adbc0f24 add label managed by infisical 2023-09-12 09:57:03 +01:00
fc3b0e1de9 Merge pull request #963 from akhilmhdh/feat/move-roles-ee
refactor: moved role folder to ee
2023-09-11 21:38:33 -04:00
ba225dd504 Added card tiles to security page 2023-09-11 16:37:17 -07:00
d221bf8ae9 Merge pull request #964 from Infisical/update-security-docs
Add/update docs to include internals
2023-09-12 00:04:49 +01:00
d7354e1aca Finish docs for internals 2023-09-12 00:02:43 +01:00
86b2b95d11 update readme file and values.yaml documentations 2023-09-11 22:10:47 +03:00
e7c5e6a789 refactor: moved role folder to ee 2023-09-11 23:53:05 +05:30
1ca106279e fix: overflow login page 2023-09-11 22:42:02 +05:30
49d6f85f42 Merge pull request #961 from akhilmhdh/fix/folder-bug
fix: resovled batch function not working with folders
2023-09-11 15:25:34 +01:00
c894952e84 fix: resovled batch function not working with folders 2023-09-11 13:01:49 +05:30
4ce0eccfa1 Merge pull request #959 from JanetEne/helper-text-for-secret-path
Add helper text and tooltip for secret path
2023-09-09 18:17:24 -07:00
ad710f4860 fixed the style of the webhook tooltip 2023-09-09 18:10:02 -07:00
45117ba1f4 show tooltip on hover of icon and not onclick 2023-09-09 22:35:55 +01:00
1b2b1ca30b Update README.md 2023-09-09 13:24:16 -07:00
b89a90066a Merge pull request #958 from Infisical/check-gcp-sm-integration
Patch broken integrations due to permission update
2023-09-09 17:59:31 +01:00
83d2a39fb1 Patch broken integratins due to permission update 2023-09-09 17:29:19 +01:00
412b1123af Begin security docs revamp 2023-09-09 10:00:09 +01:00
dc91615b43 add helper text and tooltip for secret path 2023-09-09 04:06:16 +01:00
110153385b permissioning style update 2023-09-08 20:05:02 -07:00
e37810f302 Update Chart.yaml - bump helm chart version after adding mongodb.auth.existingSecret parameter to values.yaml 2023-09-08 18:52:16 +02:00
f02e39e7e3 Merge branch 'Infisical:main' into patch-1 2023-09-08 18:50:24 +02:00
305ddd3813 Merge pull request #860 from akhilmhdh/feat/new-permission-system
New Permission System
2023-09-08 11:59:49 -04:00
b023bb1df2 feat(rbac): removed unused variables 2023-09-08 21:26:10 +05:30
63ff669612 feat(rbac): applied subscription banner in custom roles 2023-09-08 21:22:36 +05:30
8003273b2c feat(rbac): changed withpermission hoc text and removed loading on overview pageg 2023-09-08 21:22:36 +05:30
2f9b35b2f9 feat(rbac): resolved submit button getting disabled 2023-09-08 21:22:36 +05:30
91c1aca588 add role index 2023-09-08 21:22:36 +05:30
e8f7b0c181 feat(rbac): resolved batch bug in permission check 2023-09-08 21:22:36 +05:30
43735b8183 feat(rbac): updated role controller to check permissions and batch v2 selectively permission check 2023-09-08 21:22:36 +05:30
988bb4ffb6 remove created & justify delete buttons to right 2023-09-08 21:22:36 +05:30
0f46f53a7d update denied text 2023-09-08 21:22:36 +05:30
9c9d46824c only backfill roles for non empty permissions array 2023-09-08 21:22:36 +05:30
43b97b411b feat(rbac): updated helper text on glob pattern in multi env permission field 2023-09-08 21:22:36 +05:30
38c044f9a7 feat(rbac): put back condition matcher for workspace permission check and added migration function 2023-09-08 21:22:36 +05:30
c91a93ef2a feat(rbac): resolved secret import not listing 2023-09-08 21:22:36 +05:30
a4ef829046 feat(rbac): added glob support in permission and revealed settings 2023-09-08 21:22:36 +05:30
2ed079830a make org permissions more readable 2023-09-08 21:22:36 +05:30
98893a40f1 feat(rbac): made changes from testing with maidul 2023-09-08 21:22:36 +05:30
252042fb20 feat(rbac): resolved viewer permission in list 2023-09-08 21:22:36 +05:30
4ca95f4d79 feat(rbac): resolved zod validator issues and trim in frontend 2023-09-08 21:22:36 +05:30
26028e7312 feat(rbac): fixed broken view when clicking editing predefined role 2023-09-08 21:22:36 +05:30
6bbdc4a405 feat(rbac): fixed broken invite and role missing in dropdown 2023-09-08 21:22:36 +05:30
cb9ee00ed3 feat(rbac): made org role section permission into one component and fixed some small issues 2023-09-08 21:22:36 +05:30
f1a291a52a feat(rbac): fixed merge conflicts and resolved some more issues with permission checks 2023-09-08 21:22:36 +05:30
bcfe1bda84 feat(rbac): made new permission check for v3 secrets and v2 batch 2023-09-08 21:22:36 +05:30
82d4c8f000 Split requireBlindIndicesEnabled, E2EEOff, requireIPAllowlistCheck away from requireWorkspaceAuth 2023-09-08 21:22:36 +05:30
7c698e755a feat(rbac): added frontend permission validation for missing pages and merged new features 2023-09-08 21:22:36 +05:30
4b0bc238fc feat(rbac): changed the action names for org 2023-09-08 21:22:36 +05:30
ea9e638d03 feat(rbac): resolved merge conflict 2023-09-08 21:22:36 +05:30
6671699867 feat(rbac): added new permission check for workspace in frontend 2023-09-08 21:22:36 +05:30
549121f44e feat(rbac): added new validation to all routes and permission check to most 2023-09-08 21:22:36 +05:30
520a553ea1 feat(rbac): implemented project based permission loading and role management 2023-09-08 21:22:36 +05:30
aac3168c80 feat(rbac): implemented granular blocking of actions based on permissions on org level ui 2023-09-08 21:21:43 +05:30
34fb7be1c4 feat(rbac): added org permission context for pulling permission from api 2023-09-08 21:21:43 +05:30
e342e88499 feat(rbac): added new zod validation and permission check for all org level api 2023-09-08 21:20:29 +05:30
96437fd1b7 feat(rbac): added rest of permissions in ui and backend api for org level 2023-09-08 21:20:29 +05:30
c5f76b1e6f feat(rbac): base ui for org rbac management 2023-09-08 21:20:29 +05:30
1167b1bc60 feat(rbac): ui components and hooks for rbac 2023-09-08 21:20:29 +05:30
1bf9041ac9 feat(rbac): implemented backend base apis for rbac permission system 2023-09-08 21:20:29 +05:30
6bca7dcc58 update k8 self host image versions examples 2023-09-07 22:33:57 -04:00
a9f062b469 Merge branch 'main' of https://github.com/Infisical/infisical 2023-09-07 17:35:38 -07:00
43caccad9f removed intercom 2023-09-07 17:35:26 -07:00
9cef1d3b10 Merge pull request #943 from RezaRahemtola/main
docs: Fixing multiple typos
2023-09-07 14:18:05 -04:00
6c126606da Update README.md for helm chart parameters to include mongodb.auth.existingSecret 2023-09-07 20:02:15 +02:00
ae37d80891 Update values.yaml - add mongodb.auth.existingSecret parameter 2023-09-07 19:55:21 +02:00
8e6b0ca0a9 Merge pull request #953 from serin0837/patch-1
Update gcp-secret-manager.mdx
2023-09-07 13:58:55 +01:00
a288a0bf2a Update gcp-secret-manager.mdx
Just found out that there is a typo for the redirect URL.
2023-09-07 11:03:57 +01:00
9dd77ae36e Merge pull request #951 from Infisical/gitlab-integration-selfhosted
Extend GitLab integration to support syncing to self-hosted instances of GitLab
2023-09-06 11:15:39 +01:00
1e20715511 Add self-hosted gitlab url to sync function 2023-09-06 11:11:11 +01:00
d07b2dafc3 Finish adding support for self-hosted GitLab integration 2023-09-06 10:57:27 +01:00
04548313ab Merge remote-tracking branch 'origin' into gitlab-integration-selfhosted 2023-09-06 09:55:19 +01:00
86bf2ddd89 Merge pull request #949 from Infisical/aws-ps-integration
Fix null appId argument for creating PAT integration
2023-09-05 20:15:26 +01:00
2ad663c021 Update integration app and appId values 2023-09-05 20:10:50 +01:00
56317a3f53 Fix null appId argument for creating PAT integration 2023-09-05 19:55:54 +01:00
ad0bc4efdc Continue progress on self-hosted gitlab integration 2023-09-05 19:46:59 +01:00
bf74b75c4a Merge pull request #932 from daninge98/secret-scan-whole-repo
Secret scanning: scan for leaked secrets in historical commits
2023-09-04 15:49:21 -04:00
7f543b635c Merge pull request #944 from Infisical/gcp-sm-integration-auth
Add support for service account JSON authentication method for GCP Secret Manager integration
2023-09-04 14:20:15 +01:00
353dfeb2a9 Fix lint issues 2023-09-04 14:15:46 +01:00
16196e6343 Fix lint issues 2023-09-04 14:13:16 +01:00
3f2b74a28a Merge 2023-09-04 12:49:11 +01:00
4a603da425 Finished adding support for service account JSON auth method for GCP secret manager integration 2023-09-04 12:48:15 +01:00
0d9ce70000 docs: Fixing multiple typos 2023-09-03 23:28:19 +02:00
9fa1e415c8 Merge pull request #942 from omahs/patch-1
Fix typos
2023-09-03 14:24:37 +01:00
09b22f36c0 fix typo 2023-09-03 15:12:13 +02:00
43c8c42249 fix typo 2023-09-03 15:11:26 +02:00
d4a7ad713c fix typos 2023-09-03 15:10:19 +02:00
9afad7df32 fix typos 2023-09-03 15:09:18 +02:00
3f61a24ef1 fix typos 2023-09-03 15:06:53 +02:00
f342c345b7 Merge pull request #934 from Infisical/aws-ps-integration-fix
Add support for syncing secrets containing / to AWS parameter store integration
2023-09-02 11:17:06 +01:00
6dd46885f8 Add support for syncing secrets containing / to AWS parameter store integration 2023-09-02 11:11:27 +01:00
5704dfb35f add resource specification to frontend and backend deployment containers 2023-09-02 09:29:23 +03:00
41774fa97c fixed the bug with reading secrets from json 2023-09-01 19:34:38 -07:00
32b26c331c Fix gitignore 2023-09-01 23:10:12 +01:00
011507b8e0 Small fixes 2023-09-01 23:08:38 +01:00
4adb2a623e Merge remote-tracking branch 'origin/main' into secret-scan-whole-repo 2023-09-01 22:43:20 +01:00
1d410c8420 Fix type 2023-09-01 22:40:19 +01:00
35f3d6c776 Initial implementation 2023-09-01 22:36:52 +01:00
5e24b517cc Merge pull request #928 from davidhonig/fix-webhook-status-date-formatting
Fix date-fn format string
2023-09-01 14:05:50 -04:00
bf95415a0d Update k8 operator Chart version 2023-09-01 13:33:51 -04:00
4025063732 Merge pull request #923 from xphyr/main
Fix for issue#922 - helm chart for secrets-operator
2023-09-01 13:33:13 -04:00
e3ecfbaaa5 Fix date-fn format string 2023-09-01 12:16:44 +02:00
a7b3d12844 only capture non sign up secret events 2023-08-31 20:58:07 -04:00
f5145c6c39 fixing deployment.yaml file in secrets-operator helm chart. removed extra LF that was breaking the template 2023-08-31 17:43:15 -04:00
d9abe671af Start GCP SM integration update 2023-08-31 15:22:26 +01:00
37ae05fa2a Update changelog 2023-08-31 14:46:22 +01:00
086ce0d2a6 Merge pull request #918 from Infisical/revert-917-snyk-fix-29828c58f69ea88c3d50dad65d7767d2
Revert "[Snyk] Fix for 1 vulnerabilities"
2023-08-30 16:36:22 -04:00
06dec29773 Revert "[Snyk] Fix for 1 vulnerabilities" 2023-08-30 16:35:44 -04:00
ed8e942a5d Update low entropy password error message 2023-08-30 21:28:35 +01:00
e770bdde24 Update low entropy password error message 2023-08-30 21:27:31 +01:00
a84dab1219 Merge pull request #917 from Infisical/snyk-fix-29828c58f69ea88c3d50dad65d7767d2
[Snyk] Fix for 1 vulnerabilities
2023-08-30 16:26:20 -04:00
02d9d7b6a4 fix: backend/package.json & backend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-MONGODB-5871303
2023-08-30 20:05:26 +00:00
f21eb3b7c8 Patch GCP secret manager integration edge-case 2023-08-30 21:04:39 +01:00
219e3884e7 Merge pull request #912 from Infisical/integration-suffixes
Added suffixes to the Checkly integration
2023-08-30 10:29:08 +01:00
41cd8b7408 Move secretSuffix to separate metadata field 2023-08-30 10:04:44 +01:00
f6be86a26b Added suffixes to integrations 2023-08-29 22:17:48 -07:00
85e5822ece Merge pull request #908 from akhilmhdh/fix/sec-override-fail
fix: resolved personal override not showing up
2023-08-29 14:07:09 -04:00
5c9e89a8e2 Merge pull request #904 from Infisical/dashboard-get-secrets
Rewire dashboard to pull from v3/secrets with folderId support
2023-08-29 13:54:37 -04:00
46a77d5e58 Merge pull request #909 from Infisical/team-city-branch-config
Add support for build-configuration environment variable sync for TeamCity integration
2023-08-29 14:43:17 +01:00
a6e9643464 Finish adding support for build-configuration level syncs for TeamCity integration 2023-08-29 14:37:58 +01:00
affa2ee695 fix: resolved personal override not showing up 2023-08-29 12:23:12 +05:30
dc0d577cbb Patch TeamCity integration 2023-08-29 07:46:11 +01:00
9e8ddd2956 Merge pull request #907 from ragnarbull/patch-1
Update overview.mdx
2023-08-28 17:41:26 -07:00
b40b876fb2 Update overview.mdx
New password criteria + keep formatting consistent
2023-08-29 10:20:15 +10:00
2ba6a65da4 Change order of password check 2023-08-28 11:43:40 +01:00
76cf79d201 Merge pull request #885 from ragnarbull/ragnarbull-auth-pwd-fixes
Password fixes - enforce max length, add checks (pwd breach, PII, low entropy), improved UX, deprecate common-passwords api
2023-08-28 11:33:57 +01:00
a79c6227b1 Fix frontend lint issues 2023-08-28 11:25:50 +01:00
f1f64e6ff5 Fix flaky regex g flag causing unexpected validation password validation issue 2023-08-28 11:08:00 +01:00
d72ddfe315 Rewire dashboard to pull from v3/secrets with folderId support 2023-08-28 09:12:04 +01:00
f924d0c02c Update kubernetes-helm.mdx 2023-08-27 22:39:19 -07:00
a99751eb72 Moved pwd checks into a subfolder 2023-08-25 12:36:53 +10:00
4d6a8f0476 Fixed form (error messages too long). Consolidated tests & errors. Moved regexes to another file. Added regex to check for PII & reject pwd if true. Confirmed hashing & encryption/decryption works with top 50 languages, emojis etc (screen videos & unit tests to come). 2023-08-25 01:44:02 +10:00
688cf91eb7 Removed unnecessary validator library & @types/validator in favor of yup 2023-08-24 14:08:11 +10:00
14fc78eaaf Switched to crypto.subtle, cleaned up code, added types & properly cleared sensitive data from memory (even if error) 2023-08-24 14:01:26 +10:00
368855a44e >>> yup for email & url validation, fixed minor err in error msgs 2023-08-24 12:59:24 +10:00
7ec00475c6 +maxRetryAttempts, padding & safer error handling. Improved readability & comments. 2023-08-23 12:59:00 +10:00
25fc508d5e Fixed spelling 2023-08-23 02:56:03 +10:00
ea262da505 Added check that password is not an email address 2023-08-23 02:14:22 +10:00
2960f86647 Fix comments explaining "international" password requirements 2023-08-23 01:41:37 +10:00
b2888272f2 Added password criterion support for multiple languages and emojis 2023-08-23 01:27:30 +10:00
e5c87442e5 Changed to use ES2018 rather than load scripts 2023-08-23 01:04:52 +10:00
be08417c8b internationalize password requirements 2023-08-23 00:48:45 +10:00
61e44e152c optimised import 2023-08-22 23:47:33 +10:00
52c4f64655 Removed log and fixed comments 2023-08-22 23:36:24 +10:00
3e36adcf5c Removed all references to commonPasswords & the data file. This api route can be deprecated in favor of the client-side secure call to the haveIBeenPwnd password API. Further the datafile contains no passwords that meet the minimum password criteria. 2023-08-22 23:30:24 +10:00
1f60a3d73e fixed more error handling for password checks & translations 2023-08-22 22:42:02 +10:00
00089a6bba Added breached pwd error translations 2023-08-22 20:57:12 +10:00
026ea29847 further fixes to password check logic 2023-08-22 20:42:07 +10:00
1242d88acb Fixed breached pwd error messages 2023-08-22 20:20:54 +10:00
f47a119474 fixed breached pwd error messages 2023-08-22 20:20:13 +10:00
0b359cd797 Made breached pwd API comments clearer 2023-08-22 19:45:35 +10:00
c5ae402787 Added comments to explain breach passwords API 2023-08-22 18:14:03 +10:00
e288402ec4 Properly added pwndpasswords API to CSP 2023-08-22 17:58:10 +10:00
196beb8355 removed logs & added pwndpasswords.com api to CSP 2023-08-22 17:50:43 +10:00
d6222d5cee attempt to fix crypto.subtle issue 2023-08-22 17:33:35 +10:00
e855d4a0ba added types for crypto 2023-08-22 17:26:00 +10:00
20f34b4764 removed async in crypto.subtle 2023-08-22 17:14:18 +10:00
0eb21919fb Password breach check 2023-08-22 16:49:17 +10:00
fbeb210965 add to pwd length issue 2023-08-22 15:34:45 +10:00
0d1aa713ea added translations for error messges (used Google translate) 2023-08-22 14:57:02 +10:00
534d96ffb6 Set max password length (100 chars) to help prevent DDOS attack 2023-08-22 14:05:00 +10:00
829 changed files with 37576 additions and 23371 deletions

View File

@ -8,7 +8,7 @@ assignees: ''
---
### Feature description
A clear and concise description of what the the feature should be.
A clear and concise description of what the feature should be.
### Why would it be useful?
Why would this feature be useful for Infisical users?

View File

@ -17,9 +17,9 @@ jobs:
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: 🧪 Run tests
run: npm run test:ci
working-directory: backend
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2

View File

@ -11,9 +11,9 @@ jobs:
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: 🧪 Run tests
run: npm run test:ci
working-directory: backend
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2

3
.gitignore vendored
View File

@ -57,3 +57,6 @@ yarn-error.log*
# Infisical init
.infisical.json
# Editor specific
.vscode/*

View File

@ -1 +1 @@
.github/resources/docker-compose.be-test.yml:generic-api-key:16
.github/resources/docker-compose.be-test.yml:generic-api-key:16

View File

@ -34,7 +34,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a>
<a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-821.8k-orange" alt="Cloudsmith downloads" />
<img src="https://img.shields.io/badge/Downloads-1.38M-orange" alt="Cloudsmith downloads" />
</a>
<a href="https://infisical.com/slack">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />

View File

@ -17,17 +17,17 @@ WORKDIR /app
ENV npm_config_cache /home/node/.npm
COPY package*.json ./
RUN npm ci --only-production
RUN npm ci --only-production && npm cache clean --force
COPY --from=build /app .
RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical=0.8.1
&& apk add infisical=0.8.1 && apk add --no-cache git
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js
EXPOSE 4000
CMD ["npm", "run", "start"]
CMD ["node", "build/index.js"]

View File

@ -10,6 +10,8 @@
"license": "ISC",
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.319.0",
"@casl/ability": "^6.5.0",
"@casl/mongoose": "^7.2.1",
"@godaddy/terminus": "^4.12.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^19.0.5",
@ -17,6 +19,7 @@
"@sentry/tracing": "^7.48.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"@ucast/mongo2js": "^1.3.4",
"argon2": "^0.30.3",
"aws-sdk": "^2.1364.0",
"axios": "^1.3.5",
@ -34,6 +37,7 @@
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"infisical-node": "^1.2.1",
"ioredis": "^5.3.2",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
@ -52,14 +56,14 @@
"query-string": "^7.1.3",
"rate-limit-mongo": "^2.3.2",
"rimraf": "^3.0.2",
"swagger-autogen": "^2.22.0",
"swagger-ui-express": "^4.6.2",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.7"
"winston-loki": "^6.0.6",
"zod": "^3.21.4"
},
"devDependencies": {
"@jest/globals": "^29.3.1",
@ -92,6 +96,7 @@
"npm": "^8.19.3",
"smee-client": "^1.2.3",
"supertest": "^6.3.3",
"swagger-autogen": "^2.23.5",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1"
}
@ -3339,6 +3344,26 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"node_modules/@casl/ability": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz",
"integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==",
"dependencies": {
"@ucast/mongo2js": "^1.3.0"
},
"funding": {
"url": "https://github.com/stalniy/casl/blob/master/BACKERS.md"
}
},
"node_modules/@casl/mongoose": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/@casl/mongoose/-/mongoose-7.2.1.tgz",
"integrity": "sha512-pojgSWYKNIwFM6wWDNct1YD0+8nIxhe2jp5jBbK8JGU60dEs2o0Yw3mCo2y7nBwbvRC2oEots/BlLMVb1Wdo8A==",
"peerDependencies": {
"@casl/ability": "^6.3.2",
"mongoose": "^6.0.13 || ^7.0.0"
}
},
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@ -3484,8 +3509,7 @@
"node_modules/@ioredis/commands": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==",
"dev": true
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
@ -6174,6 +6198,37 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@ucast/core": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz",
"integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g=="
},
"node_modules/@ucast/js": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz",
"integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==",
"dependencies": {
"@ucast/core": "^1.0.0"
}
},
"node_modules/@ucast/mongo": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz",
"integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==",
"dependencies": {
"@ucast/core": "^1.4.1"
}
},
"node_modules/@ucast/mongo2js": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz",
"integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==",
"dependencies": {
"@ucast/core": "^1.6.1",
"@ucast/js": "^3.0.0",
"@ucast/mongo": "^2.4.0"
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
@ -6976,39 +7031,6 @@
"node": ">=12"
}
},
"node_modules/bull/node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"dev": true,
"engines": {
"node": ">=0.10"
}
},
"node_modules/bull/node_modules/ioredis": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
"integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
"dev": true,
"dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -9016,30 +9038,36 @@
}
},
"node_modules/ioredis": {
"version": "4.28.5",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz",
"integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
"integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
"dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.1",
"denque": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.flatten": "^4.4.0",
"lodash.isarguments": "^3.1.0",
"p-map": "^2.1.0",
"redis-commands": "1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=6"
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/ioredis/node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/ip": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
@ -9968,6 +9996,7 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"bin": {
"json5": "lib/cli.js"
},
@ -14389,6 +14418,31 @@
"node": ">=10"
}
},
"node_modules/probot/node_modules/ioredis": {
"version": "4.28.5",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz",
"integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==",
"dependencies": {
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.1",
"denque": "^1.1.0",
"lodash.defaults": "^4.2.0",
"lodash.flatten": "^4.4.0",
"lodash.isarguments": "^3.1.0",
"p-map": "^2.1.0",
"redis-commands": "1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/probot/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
@ -15504,6 +15558,7 @@
"version": "2.23.5",
"resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.5.tgz",
"integrity": "sha512-4Tl2+XhZMyHoBYkABnScHtQE0lKPKUD3NBt09mClrI6UKOUYljKlYw1xiFVwsHCTGR2hAXmhT4PpgjruCtt1ZA==",
"dev": true,
"dependencies": {
"acorn": "^7.4.1",
"deepmerge": "^4.2.2",
@ -15515,6 +15570,7 @@
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
@ -16626,6 +16682,14 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.21.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
},
"dependencies": {
@ -19324,6 +19388,20 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"@casl/ability": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz",
"integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==",
"requires": {
"@ucast/mongo2js": "^1.3.0"
}
},
"@casl/mongoose": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/@casl/mongoose/-/mongoose-7.2.1.tgz",
"integrity": "sha512-pojgSWYKNIwFM6wWDNct1YD0+8nIxhe2jp5jBbK8JGU60dEs2o0Yw3mCo2y7nBwbvRC2oEots/BlLMVb1Wdo8A==",
"requires": {}
},
"@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@ -19437,8 +19515,7 @@
"@ioredis/commands": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==",
"dev": true
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
},
"@istanbuljs/load-nyc-config": {
"version": "1.1.0",
@ -21575,6 +21652,37 @@
"eslint-visitor-keys": "^3.3.0"
}
},
"@ucast/core": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz",
"integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g=="
},
"@ucast/js": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz",
"integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==",
"requires": {
"@ucast/core": "^1.0.0"
}
},
"@ucast/mongo": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz",
"integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==",
"requires": {
"@ucast/core": "^1.4.1"
}
},
"@ucast/mongo2js": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz",
"integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==",
"requires": {
"@ucast/core": "^1.6.1",
"@ucast/js": "^3.0.0",
"@ucast/mongo": "^2.4.0"
}
},
"@xmldom/xmldom": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
@ -22191,31 +22299,6 @@
"msgpackr": "^1.5.2",
"semver": "^7.3.2",
"uuid": "^8.3.0"
},
"dependencies": {
"denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"dev": true
},
"ioredis": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
"integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
"dev": true,
"requires": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
}
}
}
},
"bytes": {
@ -23698,21 +23781,26 @@
"dev": true
},
"ioredis": {
"version": "4.28.5",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz",
"integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
"integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
"requires": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.1",
"denque": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.flatten": "^4.4.0",
"lodash.isarguments": "^3.1.0",
"p-map": "^2.1.0",
"redis-commands": "1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"dependencies": {
"denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="
}
}
},
"ip": {
@ -24413,7 +24501,8 @@
"json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true
},
"jsonwebtoken": {
"version": "9.0.1",
@ -27679,6 +27768,24 @@
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
"integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="
},
"ioredis": {
"version": "4.28.5",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz",
"integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==",
"requires": {
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.1",
"denque": "^1.1.0",
"lodash.defaults": "^4.2.0",
"lodash.flatten": "^4.4.0",
"lodash.isarguments": "^3.1.0",
"p-map": "^2.1.0",
"redis-commands": "1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
}
},
"js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
@ -28506,6 +28613,7 @@
"version": "2.23.5",
"resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.5.tgz",
"integrity": "sha512-4Tl2+XhZMyHoBYkABnScHtQE0lKPKUD3NBt09mClrI6UKOUYljKlYw1xiFVwsHCTGR2hAXmhT4PpgjruCtt1ZA==",
"dev": true,
"requires": {
"acorn": "^7.4.1",
"deepmerge": "^4.2.2",
@ -28516,7 +28624,8 @@
"acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true
}
}
},
@ -29273,6 +29382,11 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
},
"zod": {
"version": "3.21.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw=="
}
}
}

View File

@ -1,6 +1,8 @@
{
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.319.0",
"@casl/ability": "^6.5.0",
"@casl/mongoose": "^7.2.1",
"@godaddy/terminus": "^4.12.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^19.0.5",
@ -8,6 +10,7 @@
"@sentry/tracing": "^7.48.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"@ucast/mongo2js": "^1.3.4",
"argon2": "^0.30.3",
"aws-sdk": "^2.1364.0",
"axios": "^1.3.5",
@ -25,6 +28,7 @@
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"infisical-node": "^1.2.1",
"ioredis": "^5.3.2",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
@ -43,14 +47,14 @@
"query-string": "^7.1.3",
"rate-limit-mongo": "^2.3.2",
"rimraf": "^3.0.2",
"swagger-autogen": "^2.22.0",
"swagger-ui-express": "^4.6.2",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.7"
"winston-loki": "^6.0.6",
"zod": "^3.21.4"
},
"name": "infisical-api",
"version": "1.0.0",
@ -110,6 +114,7 @@
"npm": "^8.19.3",
"smee-client": "^1.2.3",
"supertest": "^6.3.3",
"swagger-autogen": "^2.23.5",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1"
},

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +1,24 @@
import { Request, Response } from "express";
import fs from "fs";
import path from "path";
import jwt from "jsonwebtoken";
import * as bigintConversion from "bigint-conversion";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require("jsrp");
import {
LoginSRPDetail,
TokenVersion,
User,
} from "../../models";
import { LoginSRPDetail, TokenVersion, User } from "../../models";
import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth";
import { checkUserDevice } from "../../helpers/user";
import {
ACTION_LOGIN,
ACTION_LOGOUT,
} from "../../variables";
import {
BadRequestError,
UnauthorizedRequestError,
} from "../../utils/errors";
import { ACTION_LOGIN, ACTION_LOGOUT } from "../../variables";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { EELogService } from "../../ee/services";
import { getUserAgentType } from "../../utils/posthog";
import {
getHttpsEnabled,
getJwtAuthLifetime,
getJwtAuthSecret,
getJwtRefreshSecret,
getJwtRefreshSecret
} from "../../config";
import { ActorType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module "jsonwebtoken" {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -45,12 +35,11 @@ declare module "jsonwebtoken" {
*/
export const login1 = async (req: Request, res: Response) => {
const {
email,
clientPublicKey,
}: { email: string; clientPublicKey: string } = req.body;
body: { email, clientPublicKey }
} = await validateRequest(reqValidator.Login1V1, req);
const user = await User.findOne({
email,
email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
@ -59,21 +48,25 @@ export const login1 = async (req: Request, res: Response) => {
server.init(
{
salt: user.salt,
verifier: user.verifier,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: email }, {
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false })
await LoginSRPDetail.findOneAndReplace(
{ email: email },
{
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
},
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({
serverPublicKey,
salt: user.salt,
salt: user.salt
});
}
);
@ -87,17 +80,24 @@ export const login1 = async (req: Request, res: Response) => {
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
const { email, clientProof } = req.body;
const {
body: { email, clientProof }
} = await validateRequest(reqValidator.Login2V1, req);
const user = await User.findOne({
email,
email
}).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag");
if (!user) throw new Error("Failed to find user");
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email })
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email });
if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
return BadRequestError(
Error(
"It looks like some details from the first login are not found. Please try login one again"
)
);
}
const server = new jsrp.server();
@ -105,7 +105,7 @@ export const login2 = async (req: Request, res: Response) => {
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
@ -117,13 +117,13 @@ export const login2 = async (req: Request, res: Response) => {
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgent: req.headers["user-agent"] ?? ""
});
const tokens = await issueAuthTokens({
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie
@ -131,20 +131,21 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled(),
secure: await getHttpsEnabled()
});
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP,
});
loginAction &&
(await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP
}));
// return (access) token in response
return res.status(200).send({
@ -152,12 +153,12 @@ export const login2 = async (req: Request, res: Response) => {
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
tag: user.tag
});
}
return res.status(400).send({
message: "Failed to authenticate. Try again?",
message: "Failed to authenticate. Try again?"
});
}
);
@ -171,7 +172,7 @@ export const login2 = async (req: Request, res: Response) => {
*/
export const logout = async (req: Request, res: Response) => {
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId)
await clearTokens(req.authData.tokenVersionId);
}
// clear httpOnly cookie
@ -179,49 +180,44 @@ export const logout = async (req: Request, res: Response) => {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: (await getHttpsEnabled()) as boolean,
secure: (await getHttpsEnabled()) as boolean
});
const logoutAction = await EELogService.createAction({
name: ACTION_LOGOUT,
userId: req.user._id,
userId: req.user._id
});
logoutAction && await EELogService.createLog({
userId: req.user._id,
actions: [logoutAction],
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP,
});
logoutAction &&
(await EELogService.createLog({
userId: req.user._id,
actions: [logoutAction],
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP
}));
return res.status(200).send({
message: "Successfully logged out.",
message: "Successfully logged out."
});
};
export const getCommonPasswords = async (req: Request, res: Response) => {
const commonPasswords = fs.readFileSync(
path.resolve(__dirname, "../../data/" + "common_passwords.txt"),
"utf8"
).split("\n");
return res.status(200).send(commonPasswords);
}
export const revokeAllSessions = async (req: Request, res: Response) => {
await TokenVersion.updateMany({
user: req.user._id,
}, {
$inc: {
refreshVersion: 1,
accessVersion: 1,
await TokenVersion.updateMany(
{
user: req.user._id
},
});
{
$inc: {
refreshVersion: 1,
accessVersion: 1
}
}
);
return res.status(200).send({
message: "Successfully revoked all sessions.",
});
}
message: "Successfully revoked all sessions."
});
};
/**
* Return user is authenticated
@ -231,9 +227,9 @@ export const revokeAllSessions = async (req: Request, res: Response) => {
*/
export const checkAuth = async (req: Request, res: Response) => {
return res.status(200).send({
message: "Authenticated",
message: "Authenticated"
});
}
};
/**
* Return new JWT access token by first validating the refresh token
@ -244,47 +240,47 @@ export const checkAuth = async (req: Request, res: Response) => {
export const getNewToken = async (req: Request, res: Response) => {
const refreshToken = req.cookies.jid;
if (!refreshToken) throw BadRequestError({
message: "Failed to find refresh token in request cookies"
});
if (!refreshToken)
throw BadRequestError({
message: "Failed to find refresh token in request cookies"
});
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(refreshToken, await getJwtRefreshSecret())
);
const decodedToken = <jwt.UserIDJwtPayload>jwt.verify(refreshToken, await getJwtRefreshSecret());
const user = await User.findOne({
_id: decodedToken.userId,
_id: decodedToken.userId
}).select("+publicKey +refreshVersion +accessVersion");
if (!user) throw new Error("Failed to authenticate unfound user");
if (!user?.publicKey)
throw new Error("Failed to authenticate not fully set up account");
if (!user?.publicKey) throw new Error("Failed to authenticate not fully set up account");
const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId);
if (!tokenVersion) throw UnauthorizedRequestError({
message: "Failed to validate refresh token",
});
if (!tokenVersion)
throw UnauthorizedRequestError({
message: "Failed to validate refresh token"
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({
message: "Failed to validate refresh token",
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion)
throw BadRequestError({
message: "Failed to validate refresh token"
});
const token = createToken({
payload: {
userId: decodedToken.userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.refreshVersion,
accessVersion: tokenVersion.refreshVersion
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret(),
secret: await getJwtAuthSecret()
});
return res.status(200).send({
token,
token
});
};
export const handleAuthProviderCallback = (req: Request, res: Response) => {
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
}
};

View File

@ -2,10 +2,19 @@ import { Request, Response } from "express";
import { Types } from "mongoose";
import { Bot, BotKey } from "../../models";
import { createBot } from "../../helpers/bot";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/bot";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError } from "../../utils/errors";
interface BotKey {
encryptedKey: string;
nonce: string;
encryptedKey: string;
nonce: string;
}
/**
@ -16,23 +25,30 @@ interface BotKey {
* @returns
*/
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetBotByWorkspaceIdV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
let bot = await Bot.findOne({
workspace: workspaceId,
workspace: workspaceId
});
if (!bot) {
// case: bot doesn't exist for workspace with id [workspaceId]
// -> create a new bot and return it
bot = await createBot({
name: "Infisical Bot",
workspaceId: new Types.ObjectId(workspaceId),
});
// case: bot doesn't exist for workspace with id [workspaceId]
// -> create a new bot and return it
bot = await createBot({
name: "Infisical Bot",
workspaceId: new Types.ObjectId(workspaceId)
});
}
return res.status(200).send({
bot,
bot
});
};
@ -43,46 +59,69 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
* @returns
*/
export const setBotActiveState = async (req: Request, res: Response) => {
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
if (isActive) {
// bot state set to active -> share workspace key with bot
if (!botKey?.encryptedKey || !botKey?.nonce) {
return res.status(400).send({
message: "Failed to set bot state to active - missing bot key",
});
}
await BotKey.findOneAndUpdate({
workspace: req.bot.workspace,
}, {
encryptedKey: botKey.encryptedKey,
nonce: botKey.nonce,
sender: req.user._id,
bot: req.bot._id,
workspace: req.bot.workspace,
}, {
upsert: true,
new: true,
});
} else {
// case: bot state set to inactive -> delete bot's workspace key
await BotKey.deleteOne({
bot: req.bot._id,
});
const {
body: { botKey, isActive },
params: { botId }
} = await validateRequest(reqValidator.SetBotActiveStateV1, req);
const bot = await Bot.findById(botId);
if (!bot) {
throw BadRequestError({ message: "Bot not found" });
}
const userId = req.user._id;
const { permission } = await getUserProjectPermissions(userId, bot.workspace.toString());
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
);
if (isActive) {
// bot state set to active -> share workspace key with bot
if (!botKey?.encryptedKey || !botKey?.nonce) {
return res.status(400).send({
message: "Failed to set bot state to active - missing bot key"
});
}
const bot = await Bot.findOneAndUpdate({
_id: req.bot._id,
}, {
isActive,
}, {
new: true,
});
if (!bot) throw new Error("Failed to update bot active state");
return res.status(200).send({
bot,
await BotKey.findOneAndUpdate(
{
workspace: bot.workspace
},
{
encryptedKey: botKey.encryptedKey,
nonce: botKey.nonce,
sender: userId,
bot: bot._id,
workspace: bot.workspace
},
{
upsert: true,
new: true
}
);
} else {
// case: bot state set to inactive -> delete bot's workspace key
await BotKey.deleteOne({
bot: bot._id
});
}
const updatedBot = await Bot.findOneAndUpdate(
{
_id: bot._id
},
{
isActive
},
{
new: true
}
);
if (!updatedBot) throw new Error("Failed to update bot active state");
return res.status(200).send({
bot
});
};

View File

@ -15,7 +15,8 @@ import * as userController from "./userController";
import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController";
import * as secretImportController from "./secretImportController";
import * as secretImpsController from "./secretImpsController";
import * as secretApprovalPolicyController from "./secretApprovalPolicyController";
export {
authController,
@ -35,5 +36,6 @@ export {
workspaceController,
secretScanningController,
webhookController,
secretImportController
secretImpsController,
secretApprovalPolicyController
};

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,21 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Integration } from "../../models";
import { Folder, IWorkspace, Integration, IntegrationAuth } from "../../models";
import { EventService } from "../../services";
import { eventStartIntegration } from "../../events";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/syncSecretsToThirdPartyServices";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/integration";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Create/initialize an (empty) integration for integration authorization
@ -18,23 +25,44 @@ import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/
*/
export const createIntegration = async (req: Request, res: Response) => {
const {
integrationAuthId,
app,
appId,
isActive,
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region,
secretPath
} = req.body;
body: {
isActive,
sourceEnvironment,
secretPath,
app,
path,
appId,
owner,
region,
scope,
targetService,
targetServiceId,
integrationAuthId,
targetEnvironment,
targetEnvironmentId,
metadata
}
} = await validateRequest(reqValidator.CreateIntegrationV1, req);
const integrationAuth = await IntegrationAuth.findById(integrationAuthId)
.populate<{ workspace: IWorkspace }>("workspace")
.select(
"+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt"
);
if (!integrationAuth) throw BadRequestError({ message: "Integration auth not found" });
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace._id.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Integrations
);
const folders = await Folder.findOne({
workspace: req.integrationAuth.workspace._id,
workspace: integrationAuth.workspace._id,
environment: sourceEnvironment
});
@ -42,7 +70,7 @@ export const createIntegration = async (req: Request, res: Response) => {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Path for service token does not exist"
message: "Folder path doesn't exist"
});
}
}
@ -51,7 +79,7 @@ export const createIntegration = async (req: Request, res: Response) => {
// initialize new integration after saving integration access token
const integration = await new Integration({
workspace: req.integrationAuth.workspace._id,
workspace: integrationAuth.workspace._id,
environment: sourceEnvironment,
isActive,
app,
@ -63,9 +91,11 @@ export const createIntegration = async (req: Request, res: Response) => {
owner,
path,
region,
scope,
secretPath,
integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId)
integration: integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId),
metadata
}).save();
if (integration) {
@ -119,17 +149,32 @@ export const updateIntegration = async (req: Request, res: Response) => {
// integration has the correct fields populated in [Integration]
const {
environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
secretPath
} = req.body;
body: {
environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
secretPath
},
params: { integrationId }
} = await validateRequest(reqValidator.UpdateIntegrationV1, req);
const integration = await Integration.findById(integrationId);
if (!integration) throw BadRequestError({ message: "Integration not found" });
const { permission } = await getUserProjectPermissions(
req.user._id,
integration.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
);
const folders = await Folder.findOne({
workspace: req.integration.workspace,
workspace: integration.workspace,
environment
});
@ -142,9 +187,9 @@ export const updateIntegration = async (req: Request, res: Response) => {
}
}
const integration = await Integration.findOneAndUpdate(
const updatedIntegration = await Integration.findOneAndUpdate(
{
_id: req.integration._id
_id: integration._id
},
{
environment,
@ -160,18 +205,18 @@ export const updateIntegration = async (req: Request, res: Response) => {
}
);
if (integration) {
if (updatedIntegration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventStartIntegration({
workspaceId: integration.workspace,
workspaceId: updatedIntegration.workspace,
environment
})
});
}
return res.status(200).send({
integration
integration: updatedIntegration
});
};
@ -182,13 +227,27 @@ export const updateIntegration = async (req: Request, res: Response) => {
* @returns
*/
export const deleteIntegration = async (req: Request, res: Response) => {
const { integrationId } = req.params;
const {
params: { integrationId }
} = await validateRequest(reqValidator.DeleteIntegrationV1, req);
const integration = await Integration.findOneAndDelete({
const integration = await Integration.findById(integrationId);
if (!integration) throw BadRequestError({ message: "Integration not found" });
const { permission } = await getUserProjectPermissions(
req.user._id,
integration.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Integrations
);
const deletedIntegration = await Integration.findOneAndDelete({
_id: integrationId
});
if (!integration) throw new Error("Failed to find integration");
if (!deletedIntegration) throw new Error("Failed to find integration");
await EEAuditLogService.createAuditLog(
req.authData,
@ -220,14 +279,22 @@ export const deleteIntegration = async (req: Request, res: Response) => {
});
};
// Will trigger sync for all integrations within the given env and workspace id
// Will trigger sync for all integrations within the given env and workspace id
export const manualSync = async (req: Request, res: Response) => {
const { workspaceId, environment } = req.body;
const {
body: { workspaceId, environment }
} = await validateRequest(reqValidator.ManualSyncV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
);
syncSecretsToActiveIntegrationsQueue({
workspaceId,
environment
})
});
res.status(200).send()
res.status(200).send();
};

View File

@ -4,6 +4,14 @@ import { Key } from "../../models";
import { findMembership } from "../../helpers/membership";
import { EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/key";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
@ -13,13 +21,21 @@ import { EEAuditLogService } from "../../ee/services";
* @returns
*/
export const uploadKey = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const { key } = req.body;
const {
params: { workspaceId },
body: { key }
} = await validateRequest(reqValidator.UploadKeyV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
// validate membership of receiver
const receiverMembership = await findMembership({
user: key.userId,
workspace: workspaceId,
workspace: workspaceId
});
if (!receiverMembership) {
@ -31,12 +47,12 @@ export const uploadKey = async (req: Request, res: Response) => {
nonce: key.nonce,
sender: req.user._id,
receiver: key.userId,
workspace: workspaceId,
workspace: workspaceId
}).save();
return res.status(200).send({
message: "Successfully uploaded key to workspace",
});
return res.status(200).send({
message: "Successfully uploaded key to workspace"
});
};
/**
@ -46,21 +62,23 @@ export const uploadKey = async (req: Request, res: Response) => {
* @returns
*/
export const getLatestKey = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetLatestKeyV1, req);
// get latest key
const latestKey = await Key.find({
workspace: workspaceId,
receiver: req.user._id,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.limit(1)
.populate("sender", "+publicKey");
const resObj: any = {};
const resObj: any = {};
if (latestKey.length > 0) {
resObj["latestKey"] = latestKey[0];
if (latestKey.length > 0) {
resObj["latestKey"] = latestKey[0];
await EEAuditLogService.createAuditLog(
req.authData,
{
@ -73,7 +91,7 @@ export const getLatestKey = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId)
}
);
}
}
return res.status(200).send(resObj);
return res.status(200).send(resObj);
};

View File

@ -1,12 +1,23 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { IUser, Key, Membership, MembershipOrg, User } from "../../models";
import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models";
import { EventType } from "../../ee/models";
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
import { sendMail } from "../../helpers/nodemailer";
import { ACCEPTED, ADMIN, MEMBER } from "../../variables";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
import { getSiteURL } from "../../config";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/membership";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import Role from "../../ee/models/role";
import { BadRequestError } from "../../utils/errors";
import { InviteUserToWorkspaceV1 } from "../../validation/workspace";
/**
* Check that user is a member of workspace with id [workspaceId]
@ -15,7 +26,10 @@ import { EEAuditLogService } from "../../ee/services";
* @returns
*/
export const validateMembership = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.ValidateMembershipV1, req);
// validate membership
const membership = await findMembership({
user: req.user._id,
@ -38,8 +52,10 @@ export const validateMembership = async (req: Request, res: Response) => {
* @returns
*/
export const deleteMembership = async (req: Request, res: Response) => {
const { membershipId } = req.params;
const {
params: { membershipId }
} = await validateRequest(reqValidator.DeleteMembershipV1, req);
// check if membership to delete exists
const membershipToDelete = await Membership.findOne({
_id: membershipId
@ -49,27 +65,20 @@ export const deleteMembership = async (req: Request, res: Response) => {
throw new Error("Failed to delete workspace membership that doesn't exist");
}
// check if user is a member and admin of the workspace
// whose membership we wish to delete
const membership = await Membership.findOne({
user: req.user._id,
workspace: membershipToDelete.workspace
});
if (!membership) {
throw new Error("Failed to validate workspace membership");
}
if (membership.role !== ADMIN) {
// user is not an admin member of the workspace
throw new Error("Insufficient role for deleting workspace membership");
}
const { permission } = await getUserProjectPermissions(
req.user._id,
membershipToDelete.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Member
);
// delete workspace membership
const deletedMembership = await deleteMember({
membershipId: membershipToDelete._id.toString()
});
await EEAuditLogService.createAuditLog(
req.authData,
{
@ -80,7 +89,7 @@ export const deleteMembership = async (req: Request, res: Response) => {
}
},
{
workspaceId: membership.workspace
workspaceId: membershipToDelete.workspace
}
);
@ -96,43 +105,61 @@ export const deleteMembership = async (req: Request, res: Response) => {
* @returns
*/
export const changeMembershipRole = async (req: Request, res: Response) => {
const { membershipId } = req.params;
const { role } = req.body;
if (![ADMIN, MEMBER].includes(role)) {
throw new Error("Failed to validate role");
}
const {
body: { role },
params: { membershipId }
} = await validateRequest(reqValidator.ChangeMembershipRoleV1, req);
// validate target membership
const membershipToChangeRole = await Membership
.findById(membershipId)
.populate<{ user: IUser }>("user");
const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>(
"user"
);
if (!membershipToChangeRole) {
throw new Error("Failed to find membership to change role");
}
// check if user is a member and admin of target membership's
// workspace
const membership = await findMembership({
user: req.user._id,
workspace: membershipToChangeRole.workspace
});
const { permission } = await getUserProjectPermissions(
req.user._id,
membershipToChangeRole.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
if (!membership) {
throw new Error("Failed to validate membership");
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
if (isCustomRole) {
const wsRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: membershipToChangeRole.workspace
});
if (!wsRole) throw BadRequestError({ message: "Role not found" });
const membership = await Membership.findByIdAndUpdate(membershipId, {
role: CUSTOM,
customRole: wsRole
});
return res.status(200).send({
membership
});
}
if (membership.role !== ADMIN) {
// user is not an admin member of the workspace
throw new Error("Insufficient role for changing member roles");
}
const oldRole = membershipToChangeRole.role;
const membership = await Membership.findByIdAndUpdate(
membershipId,
{
$set: {
role
},
$unset: {
customRole: 1
}
},
{
new: true
}
);
membershipToChangeRole.role = role;
await membershipToChangeRole.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
@ -140,8 +167,8 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
metadata: {
userId: membershipToChangeRole.user._id.toString(),
email: membershipToChangeRole.user.email,
oldRole,
newRole: membershipToChangeRole.role
oldRole: membershipToChangeRole.role,
newRole: role
}
},
{
@ -150,7 +177,7 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
);
return res.status(200).send({
membership: membershipToChangeRole
membership
});
};
@ -161,8 +188,15 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
* @returns
*/
export const inviteUserToWorkspace = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const { email }: { email: string } = req.body;
const {
params: { workspaceId },
body: { email }
} = await validateRequest(InviteUserToWorkspaceV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Member
);
const invitee = await User.findOne({
email
@ -179,11 +213,13 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
if (inviteeMembership) throw new Error("Failed to add existing member of workspace");
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw new Error("Failed to find workspace");
// validate invitee's organization membership - ensure that only
// (accepted) organization members can be added to the workspace
const membershipOrg = await MembershipOrg.findOne({
user: invitee._id,
organization: req.membership.workspace.organization,
organization: workspace.organization,
status: ACCEPTED
});
@ -211,7 +247,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
substitutions: {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
workspaceName: req.membership.workspace.name,
workspaceName: workspace.name,
callback_url: (await getSiteURL()) + "/login"
}
});

View File

@ -8,14 +8,8 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services";
import { EELicenseService } from "../../ee/services";
import {
ACCEPTED,
ADMIN,
INVITED,
MEMBER,
OWNER,
TOKEN_EMAIL_ORG_INVITATION
} from "../../variables";
import { ACCEPTED, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables";
import * as reqValidator from "../../validation/membershipOrg";
import {
getJwtSignupLifetime,
getJwtSignupSecret,
@ -23,6 +17,13 @@ import {
getSmtpConfigured
} from "../../config";
import { validateUserEmail } from "../../validation";
import { validateRequest } from "../../helpers/validation";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Delete organization membership with id [membershipOrgId] from organization
@ -31,7 +32,9 @@ import { validateUserEmail } from "../../validation";
* @returns
*/
export const deleteMembershipOrg = async (req: Request, _res: Response) => {
const { membershipOrgId } = req.params;
const {
params: { membershipOrgId }
} = await validateRequest(reqValidator.DelOrgMembershipv1, req);
// check if organization membership to delete exists
const membershipOrgToDelete = await MembershipOrg.findOne({
@ -42,21 +45,14 @@ export const deleteMembershipOrg = async (req: Request, _res: Response) => {
throw new Error("Failed to delete organization membership that doesn't exist");
}
// check if user is a member and admin of the organization
// whose membership we wish to delete
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: membershipOrgToDelete.organization
});
if (!membershipOrg) {
throw new Error("Failed to validate organization membership");
}
if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) {
// user is not an admin member of the organization
throw new Error("Insufficient role for deleting organization membership");
}
const { permission, membership: membershipOrg } = await getUserOrgPermissions(
req.user._id,
membershipOrgToDelete.organization.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Member
);
// delete organization membership
await deleteMemberFromOrg({
@ -96,22 +92,20 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
*/
export const inviteUserToOrganization = async (req: Request, res: Response) => {
let inviteeMembershipOrg, completeInviteLink;
const { organizationId, inviteeEmail } = req.body;
const {
body: { inviteeEmail, organizationId }
} = await validateRequest(reqValidator.InviteUserToOrgv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Member
);
const host = req.headers.host;
const siteUrl = `${req.protocol}://${host}`;
// validate membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: new Types.ObjectId(organizationId)
});
if (!membershipOrg) {
throw new Error("Failed to validate organization membership");
}
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
const ssoConfig = await SSOConfig.findOne({
organization: new Types.ObjectId(organizationId)
});
@ -119,9 +113,8 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
if (ssoConfig && ssoConfig.isActive) {
// case: SAML SSO is enabled for the organization
return res.status(400).send({
message:
"Failed to invite member due to SAML SSO configured for organization"
});
message: "Failed to invite member due to SAML SSO configured for organization"
});
}
if (plan.memberLimit !== null) {
@ -231,7 +224,10 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
*/
export const verifyUserToOrganization = async (req: Request, res: Response) => {
let user;
const { email, organizationId, code } = req.body;
const {
body: { organizationId, email, code }
} = await validateRequest(reqValidator.VerifyUserToOrgv1, req);
user = await User.findOne({ email }).select("+publicKey");

View File

@ -1,28 +1,37 @@
import { Request, Response } from "express";
import {
IncidentContactOrg,
Membership,
MembershipOrg,
Organization,
Workspace,
IncidentContactOrg,
Membership,
MembershipOrg,
Organization,
Workspace
} from "../../models";
import { createOrganization as create } from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { ACCEPTED, OWNER } from "../../variables";
import { ACCEPTED, ADMIN } from "../../variables";
import { getLicenseServerUrl, getSiteURL } from "../../config";
import { licenseServerKeyRequest } from "../../config/request";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/organization";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../ee/services/RoleService";
import { OrganizationNotFoundError } from "../../utils/errors";
import { ForbiddenError } from "@casl/ability";
export const getOrganizations = async (req: Request, res: Response) => {
const organizations = (
await MembershipOrg.find({
user: req.user._id,
status: ACCEPTED,
status: ACCEPTED
}).populate("organization")
).map((m) => m.organization);
return res.status(200).send({
organizations,
});
return res.status(200).send({
organizations
});
};
/**
@ -33,28 +42,26 @@ export const getOrganizations = async (req: Request, res: Response) => {
* @returns
*/
export const createOrganization = async (req: Request, res: Response) => {
const { organizationName } = req.body;
if (organizationName.length < 1) {
throw new Error("Organization names must be at least 1-character long");
}
const {
body: { organizationName }
} = await validateRequest(reqValidator.CreateOrgv1, req);
// create organization and add user as member
const organization = await create({
email: req.user.email,
name: organizationName,
name: organizationName
});
await addMembershipsOrg({
userIds: [req.user._id.toString()],
organizationId: organization._id.toString(),
roles: [OWNER],
statuses: [ACCEPTED],
roles: [ADMIN],
statuses: [ACCEPTED]
});
return res.status(200).send({
organization,
});
return res.status(200).send({
organization
});
};
/**
@ -64,10 +71,23 @@ export const createOrganization = async (req: Request, res: Response) => {
* @returns
*/
export const getOrganization = async (req: Request, res: Response) => {
const organization = req.organization
return res.status(200).send({
organization,
});
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgv1, req);
// ensure user has membership
await getUserOrgPermissions(req.user._id, organizationId);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
return res.status(200).send({
organization
});
};
/**
@ -77,15 +97,23 @@ export const getOrganization = async (req: Request, res: Response) => {
* @returns
*/
export const getOrganizationMembers = async (req: Request, res: Response) => {
const { organizationId } = req.params;
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Member
);
const users = await MembershipOrg.find({
organization: organizationId,
organization: organizationId
}).populate("user", "+publicKey");
return res.status(200).send({
users,
});
return res.status(200).send({
users
});
};
/**
@ -94,17 +122,22 @@ export const getOrganizationMembers = async (req: Request, res: Response) => {
* @param res
* @returns
*/
export const getOrganizationWorkspaces = async (
req: Request,
res: Response
) => {
const { organizationId } = req.params;
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgWorkspacesv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Workspace
);
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId,
organization: organizationId
},
"_id"
)
@ -113,15 +146,15 @@ export const getOrganizationWorkspaces = async (
const workspaces = (
await Membership.find({
user: req.user._id,
user: req.user._id
}).populate("workspace")
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
return res.status(200).send({
workspaces,
});
return res.status(200).send({
workspaces
});
};
/**
@ -131,25 +164,33 @@ export const getOrganizationWorkspaces = async (
* @returns
*/
export const changeOrganizationName = async (req: Request, res: Response) => {
const { organizationId } = req.params;
const { name } = req.body;
const {
params: { organizationId },
body: { name }
} = await validateRequest(reqValidator.ChangeOrgNamev1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Settings
);
const organization = await Organization.findOneAndUpdate(
{
_id: organizationId,
_id: organizationId
},
{
name,
name
},
{
new: true,
new: true
}
);
return res.status(200).send({
message: "Successfully changed organization name",
organization,
});
return res.status(200).send({
message: "Successfully changed organization name",
organization
});
};
/**
@ -158,19 +199,24 @@ export const changeOrganizationName = async (req: Request, res: Response) => {
* @param res
* @returns
*/
export const getOrganizationIncidentContacts = async (
req: Request,
res: Response
) => {
const { organizationId } = req.params;
export const getOrganizationIncidentContacts = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgIncidentContactv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactsOrg = await IncidentContactOrg.find({
organization: organizationId,
organization: organizationId
});
return res.status(200).send({
incidentContactsOrg,
});
return res.status(200).send({
incidentContactsOrg
});
};
/**
@ -179,12 +225,17 @@ export const getOrganizationIncidentContacts = async (
* @param res
* @returns
*/
export const addOrganizationIncidentContact = async (
req: Request,
res: Response
) => {
const { organizationId } = req.params;
const { email } = req.body;
export const addOrganizationIncidentContact = async (req: Request, res: Response) => {
const {
params: { organizationId },
body: { email }
} = await validateRequest(reqValidator.CreateOrgIncideContact, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
{ email, organization: organizationId },
@ -192,9 +243,9 @@ export const addOrganizationIncidentContact = async (
{ upsert: true, new: true }
);
return res.status(200).send({
incidentContactOrg,
});
return res.status(200).send({
incidentContactOrg
});
};
/**
@ -203,22 +254,27 @@ export const addOrganizationIncidentContact = async (
* @param res
* @returns
*/
export const deleteOrganizationIncidentContact = async (
req: Request,
res: Response
) => {
const { organizationId } = req.params;
const { email } = req.body;
export const deleteOrganizationIncidentContact = async (req: Request, res: Response) => {
const {
params: { organizationId },
body: { email }
} = await validateRequest(reqValidator.DelOrgIncideContact, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
email,
organization: organizationId,
organization: organizationId
});
return res.status(200).send({
message: "Successfully deleted organization incident contact",
incidentContactOrg,
});
return res.status(200).send({
message: "Successfully deleted organization incident contact",
incidentContactOrg
});
};
/**
@ -228,19 +284,41 @@ export const deleteOrganizationIncidentContact = async (
* @param res
* @returns
*/
export const createOrganizationPortalSession = async (
req: Request,
res: Response
) => {
const { data: { pmtMethods } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`,
export const createOrganizationPortalSession = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { pmtMethods }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`
);
if (pmtMethods.length < 1) {
// case: organization has no payment method on file
// -> redirect to add payment method portal
const { data: { url } } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`,
// -> redirect to add payment method portal
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`,
{
success_url: (await getSiteURL()) + "/dashboard",
cancel_url: (await getSiteURL()) + "/dashboard"
@ -250,8 +328,12 @@ export const createOrganizationPortalSession = async (
} else {
// case: organization has payment method on file
// -> redirect to billing portal
const { data: { url } } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/billing-portal`,
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/billing-portal`,
{
return_url: (await getSiteURL()) + "/dashboard"
}
@ -266,36 +348,43 @@ export const createOrganizationPortalSession = async (
* @param res
* @returns
*/
export const getOrganizationMembersAndTheirWorkspaces = async (
req: Request,
res: Response
) => {
const { organizationId } = req.params;
export const getOrganizationMembersAndTheirWorkspaces = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
const workspacesSet = (
await Workspace.find(
{
organization: organizationId,
},
"_id"
)
).map((w) => w._id.toString());
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Member
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Workspace
);
const memberships = (
await Membership.find({
workspace: { $in: workspacesSet },
}).populate("workspace")
);
const userToWorkspaceIds: any = {};
const workspacesSet = (
await Workspace.find(
{
organization: organizationId
},
"_id"
)
).map((w) => w._id.toString());
memberships.forEach(membership => {
const user = membership.user.toString();
if (userToWorkspaceIds[user]) {
userToWorkspaceIds[user].push(membership.workspace);
} else {
userToWorkspaceIds[user] = [membership.workspace];
}
});
const memberships = await Membership.find({
workspace: { $in: workspacesSet }
}).populate("workspace");
const userToWorkspaceIds: any = {};
return res.json(userToWorkspaceIds);
memberships.forEach((membership) => {
const user = membership.user.toString();
if (userToWorkspaceIds[user]) {
userToWorkspaceIds[user].push(membership.workspace);
} else {
userToWorkspaceIds[user] = [membership.workspace];
}
});
return res.json(userToWorkspaceIds);
};

View File

@ -14,6 +14,8 @@ import {
getSiteURL
} from "../../config";
import { ActorType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
/**
* Password reset step 1: Send email verification link to email [email]
@ -23,7 +25,9 @@ import { ActorType } from "../../ee/models";
* @returns
*/
export const emailPasswordReset = async (req: Request, res: Response) => {
const email: string = req.body.email;
const {
body: { email }
} = await validateRequest(reqValidator.EmailPasswordResetV1, req);
const user = await User.findOne({ email }).select("+publicKey");
if (!user || !user?.publicKey) {
@ -62,7 +66,9 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
* @returns
*/
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
const { email, code } = req.body;
const {
body: { email, code }
} = await validateRequest(reqValidator.EmailPasswordResetVerifyV1, req);
const user = await User.findOne({ email }).select("+publicKey");
if (!user || !user?.publicKey) {
@ -103,8 +109,10 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
*/
export const srp1 = async (req: Request, res: Response) => {
// return salt, serverPublicKey as part of first step of SRP protocol
const {
body: { clientPublicKey }
} = await validateRequest(reqValidator.Srp1V1, req);
const { clientPublicKey } = req.body;
const user = await User.findOne({
email: req.user.email
}).select("+salt +verifier");
@ -149,16 +157,18 @@ export const srp1 = async (req: Request, res: Response) => {
*/
export const changePassword = async (req: Request, res: Response) => {
const {
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
} = req.body;
body: {
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
}
} = await validateRequest(reqValidator.ChangePasswordV1, req);
const user = await User.findOne({
email: req.user.email
@ -208,10 +218,7 @@ export const changePassword = async (req: Request, res: Response) => {
}
);
if (
req.authData.actor.type === ActorType.USER &&
req.authData.tokenVersionId
) {
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId);
}
@ -246,8 +253,9 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
// create/change backup private key
// requires verifying [clientProof] as part of second step of SRP protocol
// as initiated in /srp1
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } = req.body;
const {
body: { clientProof, encryptedPrivateKey, salt, verifier, iv, tag }
} = await validateRequest(reqValidator.CreateBackupPrivateKeyV1, req);
const user = await User.findOne({
email: req.user.email
}).select("+salt +verifier");
@ -325,15 +333,17 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => {
export const resetPassword = async (req: Request, res: Response) => {
const {
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
} = req.body;
body: {
encryptedPrivateKey,
protectedKeyTag,
protectedKey,
protectedKeyIV,
salt,
verifier,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag
}
} = await validateRequest(reqValidator.ResetPasswordV1, req);
await User.findByIdAndUpdate(
req.user._id.toString(),

View File

@ -0,0 +1,109 @@
import { ForbiddenError } from "@casl/ability";
import { Request, Response } from "express";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { validateRequest } from "../../helpers/validation";
import { SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
import { BadRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/secretApproval";
const ERR_SECRET_APPROVAL_NOT_FOUND = BadRequestError({ message: "secret approval not found" });
export const createSecretApprovalPolicy = async (req: Request, res: Response) => {
const {
body: { approvals, secretPath, approvers, environment, workspaceId }
} = await validateRequest(reqValidator.CreateSecretApprovalRule, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval
);
const secretApproval = new SecretApprovalPolicy({
workspace: workspaceId,
secretPath,
environment,
approvals,
approvers
});
await secretApproval.save();
return res.send({
approval: secretApproval
});
};
export const updateSecretApprovalPolicy = async (req: Request, res: Response) => {
const {
body: { approvals, approvers, secretPath },
params: { id }
} = await validateRequest(reqValidator.UpdateSecretApprovalRule, req);
const secretApproval = await SecretApprovalPolicy.findById(id);
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
const { permission } = await getUserProjectPermissions(
req.user._id,
secretApproval.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.SecretApproval
);
const updatedDoc = await SecretApprovalPolicy.findByIdAndUpdate(id, {
approvals,
approvers,
...(secretPath === null ? { $unset: { secretPath: 1 } } : { secretPath })
});
return res.send({
approval: updatedDoc
});
};
export const deleteSecretApprovalPolicy = async (req: Request, res: Response) => {
const {
params: { id }
} = await validateRequest(reqValidator.DeleteSecretApprovalRule, req);
const secretApproval = await SecretApprovalPolicy.findById(id);
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
const { permission } = await getUserProjectPermissions(
req.user._id,
secretApproval.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretApproval
);
const deletedDoc = await SecretApprovalPolicy.findByIdAndDelete(id);
return res.send({
approval: deletedDoc
});
};
export const getSecretApprovalPolicy = async (req: Request, res: Response) => {
const {
query: { workspaceId }
} = await validateRequest(reqValidator.GetSecretApprovalRuleList, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretApproval
);
const doc = await SecretApprovalPolicy.find({ workspace: workspaceId });
return res.send({
approvals: doc
});
};

View File

@ -1,354 +0,0 @@
import { Request, Response } from "express";
import { isValidScope, validateMembership } from "../../helpers";
import { ServiceTokenData } from "../../models";
import Folder from "../../models/folder";
import SecretImport from "../../models/secretImports";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { getFolderWithPathFromId } from "../../services/FolderService";
import { BadRequestError, ResourceNotFoundError,UnauthorizedRequestError } from "../../utils/errors";
import { ADMIN, MEMBER } from "../../variables";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
export const createSecretImport = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId, secretImport } = req.body;
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && folderId !== "root") {
throw ResourceNotFoundError({
message: "Failed to find folder"
});
}
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, folderId);
secretPath = folderPath;
}
if (req.authData.authPayload instanceof ServiceTokenData) {
// root check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
const importToSecretPath = folders?getFolderWithPathFromId(folders.nodes, folderId).folderPath:"/";
if (!importSecDoc) {
const doc = new SecretImport({
workspace: workspaceId,
environment,
folderId,
imports: [{ environment: secretImport.environment, secretPath: secretImport.secretPath }]
});
await doc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
secretImportId: doc._id.toString(),
folderId: doc.folderId.toString(),
importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment,
importToSecretPath
}
},
{
workspaceId: doc.workspace
}
);
return res.status(200).json({ message: "successfully created secret import" });
}
const doesImportExist = importSecDoc.imports.find(
(el) => el.environment === secretImport.environment && el.secretPath === secretImport.secretPath
);
if (doesImportExist) {
throw BadRequestError({ message: "Secret import already exist" });
}
importSecDoc.imports.push({
environment: secretImport.environment,
secretPath: secretImport.secretPath
});
await importSecDoc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment,
importToSecretPath
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully created secret import" });
};
// to keep the ordering, you must pass all the imports in here not the only updated one
// this is because the order decide which import gets overriden
export const updateSecretImport = async (req: Request, res: Response) => {
const { id } = req.params;
const { secretImports } = req.body;
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
if (!(req.authData.authPayload instanceof ServiceTokenData)) {
await validateMembership({
userId: req.user._id.toString(),
workspaceId: importSecDoc.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
} else {
// check for service token validity
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const orderBefore = importSecDoc.imports;
importSecDoc.imports = secretImports;
await importSecDoc.save();
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment,
}).lean();
if (!folders) throw ResourceNotFoundError({
message: "Failed to find folder"
});
const importToSecretPath = folders?getFolderWithPathFromId(folders.nodes, importSecDoc.folderId).folderPath:"/";
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_SECRET_IMPORT,
metadata: {
importToEnvironment: importSecDoc.environment,
importToSecretPath,
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
orderBefore,
orderAfter: secretImports
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully updated secret import" });
};
export const deleteSecretImport = async (req: Request, res: Response) => {
const { id } = req.params;
const { secretImportEnv, secretImportPath } = req.body;
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
if (!(req.authData.authPayload instanceof ServiceTokenData)) {
await validateMembership({
userId: req.user._id.toString(),
workspaceId: importSecDoc.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
} else {
// check for service token validity
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
importSecDoc.imports = importSecDoc.imports.filter(
({ environment, secretPath }) =>
!(environment === secretImportEnv && secretPath === secretImportPath)
);
await importSecDoc.save();
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment,
}).lean();
if (!folders) throw ResourceNotFoundError({
message: "Failed to find folder"
});
const importToSecretPath = folders?getFolderWithPathFromId(folders.nodes, importSecDoc.folderId).folderPath:"/";
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_SECRET_IMPORT,
metadata: {
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
importFromEnvironment: secretImportEnv,
importFromSecretPath: secretImportPath,
importToEnvironment: importSecDoc.environment,
importToSecretPath
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully delete secret import" });
};
export const getSecretImports = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId } = req.query;
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secretImport: {} });
}
if (req.authData.authPayload instanceof ServiceTokenData) {
// check for service token validity
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
return res.status(200).json({ secretImport: importSecDoc });
};
export const getAllSecretsFromImport = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId } = req.query as {
workspaceId: string;
environment: string;
folderId: string;
};
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secrets: [] });
}
if (req.authData.authPayload instanceof ServiceTokenData) {
// check for service token validity
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.GET_SECRET_IMPORTS,
metadata: {
environment,
secretImportId: importSecDoc._id.toString(),
folderId,
numberOfImports: importSecDoc.imports.length
}
},
{
workspaceId: importSecDoc.workspace
}
);
const secrets = await getAllImportedSecrets(workspaceId, environment, folderId);
return res.status(200).json({ secrets });
};

View File

@ -0,0 +1,705 @@
import { Request, Response } from "express";
import { isValidScope } from "../../helpers";
import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
import {
BadRequestError,
ResourceNotFoundError,
UnauthorizedRequestError
} from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secretImports";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
export const createSecretImp = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Create secret import'
#swagger.description = 'Create a new secret import for a specified workspace and environment'
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of the workspace where the secret import will be created",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Environment to import to",
"example": "production"
},
"folderId": {
"type": "string",
"description": "Folder ID. Use root for the root folder.",
"example": "my_folder"
},
"secretImport": {
"type": "object",
"properties": {
"environment": {
"type": "string",
"description": "Import from environment",
"example": "development"
},
"secretPath": {
"type": "string",
"description": "Import from secret path",
"example": "/user/oauth"
}
}
}
},
"required": ["workspaceId", "environment", "folderName"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "successfully created secret import"
}
},
"description": "Confirmation of secret import creation"
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. For example, 'Secret import already exist'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
#swagger.responses[404] = {
description: "Resource Not Found. For example, 'Failed to find folder'"
}
*/
const {
body: { workspaceId, environment, directory, secretImport }
} = await validateRequest(reqValidator.CreateSecretImportV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
// root check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/")
throw ResourceNotFoundError({ message: "Failed to find folder" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
const doc = new SecretImport({
workspace: workspaceId,
environment,
folderId,
imports: [{ environment: secretImport.environment, secretPath: secretImport.secretPath }]
});
await doc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
secretImportId: doc._id.toString(),
folderId: doc.folderId.toString(),
importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment,
importToSecretPath: directory
}
},
{
workspaceId: doc.workspace
}
);
return res.status(200).json({ message: "successfully created secret import" });
}
const doesImportExist = importSecDoc.imports.find(
(el) => el.environment === secretImport.environment && el.secretPath === secretImport.secretPath
);
if (doesImportExist) {
throw BadRequestError({ message: "Secret import already exist" });
}
importSecDoc.imports.push({
environment: secretImport.environment,
secretPath: secretImport.secretPath
});
await importSecDoc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment,
importToSecretPath: directory
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully created secret import" });
};
// to keep the ordering, you must pass all the imports in here not the only updated one
// this is because the order decide which import gets overriden
/**
* Update secret import
* @param req
* @param res
* @returns
*/
export const updateSecretImport = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update a secret import'
#swagger.description = 'Updates an existing secret import based on the provided ID and new import details'
#swagger.parameters['id'] = {
in: 'path',
description: 'ID of the secret import to be updated',
required: true,
type: 'string',
example: 'import12345'
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretImports": {
"type": "array",
"description": "List of new secret imports",
"items": {
"type": "object",
"properties": {
"environment": {
"type": "string",
"description": "Environment of the secret import",
"example": "production"
},
"secretPath": {
"type": "string",
"description": "Path of the secret import",
"example": "/path/to/secret"
}
},
"required": ["environment", "secretPath"]
}
}
},
"required": ["secretImports"]
}
}
}
}
#swagger.responses[200] = {
description: 'Successfully updated the secret import',
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "successfully updated secret import"
}
}
}
}
}
}
#swagger.responses[400] = {
description: 'Bad Request - Import not found',
}
#swagger.responses[403] = {
description: 'Forbidden access due to insufficient permissions',
}
#swagger.responses[401] = {
description: 'Unauthorized access due to invalid token or scope',
}
*/
const {
body: { secretImports },
params: { id }
} = await validateRequest(reqValidator.UpdateSecretImportV1, req);
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
// check for service token validity
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
if (req.authData.authPayload instanceof ServiceTokenData) {
// token permission check
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// non token entry check
const { permission } = await getUserProjectPermissions(
req.user._id,
importSecDoc.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
}
const orderBefore = importSecDoc.imports;
importSecDoc.imports = secretImports;
await importSecDoc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_SECRET_IMPORT,
metadata: {
importToEnvironment: importSecDoc.environment,
importToSecretPath: secretPath,
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
orderBefore,
orderAfter: secretImports
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully updated secret import" });
};
/**
* Delete secret import
* @param req
* @param res
* @returns
*/
export const deleteSecretImport = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete secret import'
#swagger.description = 'Delete secret import'
#swagger.parameters['id'] = {
in: 'path',
description: 'ID of the secret import',
required: true,
type: 'string',
example: '12345abcde'
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretImportEnv": {
"type": "string",
"description": "Import from environment",
"example": "someWorkspaceId"
},
"secretImportPath": {
"type": "string",
"description": "Import from secret path",
"example": "production"
}
},
"required": ["id", "secretImportEnv", "secretImportPath"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "successfully delete secret import"
}
},
"description": "Confirmation of secret import deletion"
}
}
}
}
*/
const {
params: { id },
body: { secretImportEnv, secretImportPath }
} = await validateRequest(reqValidator.DeleteSecretImportV1, req);
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
// check for service token validity
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(
req.user._id,
importSecDoc.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
}
importSecDoc.imports = importSecDoc.imports.filter(
({ environment, secretPath }) =>
!(environment === secretImportEnv && secretPath === secretImportPath)
);
await importSecDoc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_SECRET_IMPORT,
metadata: {
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
importFromEnvironment: secretImportEnv,
importFromSecretPath: secretImportPath,
importToEnvironment: importSecDoc.environment,
importToSecretPath: secretPath
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully delete secret import" });
};
/**
* Get secret imports
* @param req
* @param res
* @returns
*/
export const getSecretImports = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Retrieve secret imports'
#swagger.description = 'Fetches the secret imports based on the workspaceId, environment, and folderId'
#swagger.parameters['workspaceId'] = {
in: 'query',
description: 'ID of the workspace of secret imports to get',
required: true,
type: 'string',
example: 'workspace12345'
}
#swagger.parameters['environment'] = {
in: 'query',
description: 'Environment of secret imports to get',
required: true,
type: 'string',
example: 'production'
}
#swagger.parameters['folderId'] = {
in: 'query',
description: 'ID of the folder containing the secret imports. Default: root',
required: false,
type: 'string',
example: 'folder12345'
}
#swagger.responses[200] = {
description: 'Successfully retrieved secret import',
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretImport": {
"type": "object",
"description": "Details of a secret import"
}
}
}
}
}
}
#swagger.responses[403] = {
description: 'Forbidden access due to insufficient permissions',
}
#swagger.responses[401] = {
description: 'Unauthorized access due to invalid token or scope',
}
*/
const {
query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetSecretImportsV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: directory
})
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secretImport: {} });
}
return res.status(200).json({ secretImport: importSecDoc });
};
/**
* Get all secret imports
* @param req
* @param res
* @returns
*/
export const getAllSecretsFromImport = async (req: Request, res: Response) => {
const {
query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
// check for service token validity
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: directory
})
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secrets: [] });
}
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
if (req.authData.authPayload instanceof ServiceTokenData) {
// check for service token validity
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
} else {
const { permission } = await getUserProjectPermissions(
req.user._id,
importSecDoc.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.GET_SECRET_IMPORTS,
metadata: {
environment,
secretImportId: importSecDoc._id.toString(),
folderId,
numberOfImports: importSecDoc.imports.length
}
},
{
workspaceId: importSecDoc.workspace
}
);
const secrets = await getAllImportedSecrets(
workspaceId,
environment,
folderId,
permissionCheckFn
);
return res.status(200).json({ secrets });
};

View File

@ -2,17 +2,49 @@ import { Request, Response } from "express";
import GitAppInstallationSession from "../../ee/models/gitAppInstallationSession";
import crypto from "crypto";
import { Types } from "mongoose";
import { UnauthorizedRequestError } from "../../utils/errors";
import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
import GitAppOrganizationInstallation from "../../ee/models/gitAppOrganizationInstallation";
import { MembershipOrg } from "../../models";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE, STATUS_RESOLVED_NOT_REVOKED, STATUS_RESOLVED_REVOKED } from "../../ee/models/gitRisks";
import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository";
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
import GitRisks, {
STATUS_RESOLVED_FALSE_POSITIVE,
STATUS_RESOLVED_NOT_REVOKED,
STATUS_RESOLVED_REVOKED
} from "../../ee/models/gitRisks";
import { ProbotOctokit } from "probot";
import { Organization } from "../../models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secretScanning";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
export const createInstallationSession = async (req: Request, res: Response) => {
const sessionId = crypto.randomBytes(16).toString("hex");
const {
params: { organizationId }
} = await validateRequest(reqValidator.CreateInstalLSessionv1, req);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.SecretScanning
);
await GitAppInstallationSession.findByIdAndUpdate(
req.organization,
organization,
{
organization: new Types.ObjectId(req.organization),
organization: organization.id,
sessionId: sessionId,
user: new Types.ObjectId(req.user._id)
},
@ -21,71 +53,128 @@ export const createInstallationSession = async (req: Request, res: Response) =>
res.send({
sessionId: sessionId
})
}
});
};
export const linkInstallationToOrganization = async (req: Request, res: Response) => {
const { installationId, sessionId } = req.body
const {
body: { sessionId, installationId }
} = await validateRequest(reqValidator.LinkInstallationToOrgv1, req);
const installationSession = await GitAppInstallationSession.findOneAndDelete({ sessionId: sessionId })
const installationSession = await GitAppInstallationSession.findOneAndDelete({
sessionId: sessionId
});
if (!installationSession) {
throw UnauthorizedRequestError()
throw UnauthorizedRequestError();
}
const userMembership = await MembershipOrg.find({ user: req.user._id, organization: installationSession.organization })
if (!userMembership) {
throw UnauthorizedRequestError()
const { permission } = await getUserOrgPermissions(
req.user._id,
installationSession.organization.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.SecretScanning
);
const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate(
{
organizationId: installationSession.organization
},
{
installationId: installationId,
organizationId: installationSession.organization,
user: installationSession.user
},
{
upsert: true
}
).lean();
const octokit = new ProbotOctokit({
auth: {
appId: await getSecretScanningGitAppId(),
privateKey: await getSecretScanningPrivateKey(),
installationId: installationId.toString()
}
});
const {
data: { repositories }
} = await octokit.apps.listReposAccessibleToInstallation();
for (const repository of repositories) {
scanGithubFullRepoForSecretLeaks({
organizationId: installationSession.organization.toString(),
installationId,
repository: { id: repository.id, fullName: repository.full_name }
});
}
const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate({
organizationId: installationSession.organization,
}, {
installationId: installationId,
organizationId: installationSession.organization,
user: installationSession.user
}, {
upsert: true
}).lean()
res.json(installationLink)
}
res.json(installationLink);
};
export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => {
const { organizationId } = req.params
const { organizationId } = req.params;
try {
const appInstallation = await GitAppOrganizationInstallation.findOne({ organizationId: organizationId }).lean()
const appInstallation = await GitAppOrganizationInstallation.findOne({
organizationId: organizationId
}).lean();
if (!appInstallation) {
res.json({
appInstallationComplete: false
})
});
}
res.json({
appInstallationComplete: true
})
});
} catch {
res.json({
appInstallationComplete: false
})
});
}
}
};
export const getRisksForOrganization = async (req: Request, res: Response) => {
const { organizationId } = req.params
const risks = await GitRisks.find({ organization: organizationId }).sort({ createdAt: -1 }).lean()
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgRisksv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.SecretScanning
);
const risks = await GitRisks.find({ organization: organizationId })
.sort({ createdAt: -1 })
.lean();
res.json({
risks: risks
})
}
});
};
export const updateRisksStatus = async (req: Request, res: Response) => {
const { riskId } = req.params
const { status } = req.body
const isRiskResolved = status == STATUS_RESOLVED_FALSE_POSITIVE || status == STATUS_RESOLVED_REVOKED || status == STATUS_RESOLVED_NOT_REVOKED ? true : false
const {
params: { organizationId, riskId },
body: { status }
} = await validateRequest(reqValidator.UpdateRiskStatusv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.SecretScanning
);
const isRiskResolved =
status == STATUS_RESOLVED_FALSE_POSITIVE ||
status == STATUS_RESOLVED_REVOKED ||
status == STATUS_RESOLVED_NOT_REVOKED
? true
: false;
const risk = await GitRisks.findByIdAndUpdate(riskId, {
status: status,
isResolved: isRiskResolved
}).lean()
}).lean();
res.json(risk)
}
res.json(risk);
};

View File

@ -1,48 +1,138 @@
import { ForbiddenError, subject } from "@casl/ability";
import { Request, Response } from "express";
import { Types } from "mongoose";
import { EventType, FolderVersion } from "../../ee/models";
import { EEAuditLogService, EESecretService } from "../../ee/services";
import { validateMembership } from "../../helpers/membership";
import { isValidScope } from "../../helpers/secrets";
import { validateRequest } from "../../helpers/validation";
import { Secret, ServiceTokenData } from "../../models";
import Folder from "../../models/folder";
import { Folder } from "../../models/folder";
import {
appendFolder,
deleteFolderById,
generateFolderId,
getAllFolderIds,
getFolderByPath,
getFolderWithPathFromId,
getParentFromFolderId,
validateFolderName
} from "../../services/FolderService";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { ADMIN, MEMBER } from "../../variables";
import * as reqValidator from "../../validation/folders";
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "The folder doesn't exist" });
// verify workspace id/environment
export const createFolder = async (req: Request, res: Response) => {
const { workspaceId, environment, folderName, parentFolderId } = req.body;
/*
#swagger.summary = 'Create a folder'
#swagger.description = 'Create a new folder in a specified workspace and environment'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of the workspace where the folder will be created",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Environment where the folder will reside",
"example": "production"
},
"folderName": {
"type": "string",
"description": "Name of the folder to be created",
"example": "my_folder"
},
"parentFolderId": {
"type": "string",
"description": "ID of the parent folder under which this folder will be created. If not specified, it will be created at the root level.",
"example": "someParentFolderId"
}
},
"required": ["workspaceId", "environment", "folderName"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"folder": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "someFolderId"
},
"name": {
"type": "string",
"example": "my_folder"
}
},
"description": "Details of the created folder"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. For example, 'Folder name cannot contain spaces. Only underscore and dashes'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
body: { workspaceId, environment, folderName, directory }
} = await validateRequest(reqValidator.CreateFolderV1, req);
if (!validateFolderName(folderName)) {
throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes"
});
}
if (req.authData.authPayload instanceof ServiceTokenData) {
// token check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// user check
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
// space has no folders initialized
if (!folders) {
if (req.authData.authPayload instanceof ServiceTokenData) {
// root check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/");
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
if (directory !== "/") throw ERR_FOLDER_NOT_FOUND;
const id = generateFolderId();
const folder = new Folder({
@ -63,10 +153,10 @@ export const createFolder = async (req: Request, res: Response) => {
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment
});
await EEAuditLogService.createAuditLog(
req.authData,
{
@ -86,27 +176,10 @@ export const createFolder = async (req: Request, res: Response) => {
return res.json({ folder: { id, name: folderName } });
}
const folder = appendFolder(folders.nodes, { folderName, parentFolderId });
await Folder.findByIdAndUpdate(folders._id, folders);
const { folder: parentFolder, folderPath: parentFolderPath } = getFolderWithPathFromId(
folders.nodes,
parentFolderId || "root"
);
if (req.authData.authPayload instanceof ServiceTokenData) {
// root check
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
environment,
parentFolderPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
const folder = appendFolder(folders.nodes, { folderName, parentFolderId: parentFolder.id });
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
@ -117,13 +190,11 @@ export const createFolder = async (req: Request, res: Response) => {
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolderId
folderId: parentFolder.id
});
const {folderPath} = getFolderWithPathFromId(folders.nodes, folder.id);
await EEAuditLogService.createAuditLog(
req.authData,
{
@ -132,7 +203,7 @@ export const createFolder = async (req: Request, res: Response) => {
environment,
folderId: folder.id,
folderName,
folderPath
folderPath: directory
}
},
{
@ -143,47 +214,130 @@ export const createFolder = async (req: Request, res: Response) => {
return res.json({ folder });
};
/**
* Update folder with id [folderId]
* @param req
* @param res
* @returns
*/
export const updateFolderById = async (req: Request, res: Response) => {
const { folderId } = req.params;
const { name, workspaceId, environment } = req.body;
/*
#swagger.summary = 'Update a folder by ID'
#swagger.description = 'Update the name of a folder in a specified workspace and environment by its ID'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['folderId'] = {
"description": "ID of the folder to be updated",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of the workspace where the folder is located",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Environment where the folder is located",
"example": "production"
},
"name": {
"type": "string",
"description": "New name for the folder",
"example": "updated_folder_name"
}
},
"required": ["workspaceId", "environment", "name"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Successfully updated folder"
},
"folder": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "updated_folder_name"
},
"id": {
"type": "string",
"example": "someFolderId"
}
},
"description": "Details of the updated folder"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. Reasons can include 'The folder doesn't exist' or 'Folder name cannot contain spaces. Only underscore and dashes'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
body: { workspaceId, environment, name, directory },
params: { folderName }
} = await validateRequest(reqValidator.UpdateFolderV1, req);
if (!validateFolderName(name)) {
throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes"
});
}
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
if (!(req.authData.authPayload instanceof ServiceTokenData)) {
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles: [ADMIN, MEMBER]
});
}
const parentFolder = getParentFromFolderId(folders.nodes, folderId);
const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const folder = parentFolder.children.find(({ id }) => id === folderId);
if (!folder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
if (req.authData.authPayload instanceof ServiceTokenData) {
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
// root check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folder = parentFolder.children.find(({ name }) => name === folderName);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
const oldFolderName = folder.name;
parentFolder.version += 1;
@ -198,13 +352,13 @@ export const updateFolderById = async (req: Request, res: Response) => {
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolder.id
});
const {folderPath} = getFolderWithPathFromId(folders.nodes, folder.id);
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
await EEAuditLogService.createAuditLog(
req.authData,
{
@ -228,42 +382,124 @@ export const updateFolderById = async (req: Request, res: Response) => {
});
};
/**
* Delete folder with id [folderId]
* @param req
* @param res
* @returns
*/
export const deleteFolder = async (req: Request, res: Response) => {
const { folderId } = req.params;
const { workspaceId, environment } = req.body;
/*
#swagger.summary = 'Delete a folder by ID'
#swagger.description = 'Delete the specified folder from a specified workspace and environment using its ID'
#swagger.security = [{
"apiKeyAuth": []
}]
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
#swagger.parameters['folderId'] = {
"description": "ID of the folder to be deleted",
"required": true,
"type": "string",
"in": "path"
}
if (!(req.authData.authPayload instanceof ServiceTokenData)) {
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles: [ADMIN, MEMBER]
});
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of the workspace where the folder is located",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Environment where the folder is located",
"example": "production"
}
},
"required": ["workspaceId", "environment"]
}
}
}
}
const {folderPath} = getFolderWithPathFromId(folders.nodes, folderId);
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "successfully deleted folders"
},
"folders": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "someFolderId"
},
"name": {
"type": "string",
"example": "someFolderName"
}
}
},
"description": "List of IDs and names of the deleted folders"
}
}
}
}
}
}
const delOp = deleteFolderById(folders.nodes, folderId);
if (!delOp) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const { deletedNode: delFolder, parent: parentFolder } = delOp;
#swagger.responses[400] = {
description: "Bad Request. Reasons can include 'The folder doesn't exist'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
params: { folderName },
body: { environment, workspaceId, directory }
} = await validateRequest(reqValidator.DeleteFolderV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// check that user is a member of the workspace
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) throw ERR_FOLDER_NOT_FOUND;
const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
const index = parentFolder.children.findIndex(({ name }) => name === folderName);
if (index === -1) throw ERR_FOLDER_NOT_FOUND;
const deletedFolder = parentFolder.children.splice(index, 1)[0];
parentFolder.version += 1;
const delFolderIds = getAllFolderIds(delFolder);
const delFolderIds = getAllFolderIds(deletedFolder);
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
@ -281,7 +517,7 @@ export const deleteFolder = async (req: Request, res: Response) => {
}
await EESecretService.takeSecretSnapshot({
workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolder.id
});
@ -289,12 +525,12 @@ export const deleteFolder = async (req: Request, res: Response) => {
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_FOLDER ,
type: EventType.DELETE_FOLDER,
metadata: {
environment,
folderId,
folderName: delFolder.name,
folderPath
folderId: deletedFolder.id,
folderName: deletedFolder.name,
folderPath: directory
}
},
{
@ -302,84 +538,129 @@ export const deleteFolder = async (req: Request, res: Response) => {
}
);
res.send({ message: "successfully deleted folders", folders: delFolderIds });
return res.send({ message: "successfully deleted folders", folders: delFolderIds });
};
// TODO: validate workspace
/**
* Get folders for workspace with id [workspaceId] and environment [environment]
* considering [parentFolderId] and [parentFolderPath]
* @param req
* @param res
* @returns
*/
export const getFolders = async (req: Request, res: Response) => {
const { workspaceId, environment, parentFolderId, parentFolderPath } = req.query as {
workspaceId: string;
environment: string;
parentFolderId?: string;
parentFolderPath?: string;
};
/*
#swagger.summary = 'Retrieve folders based on specific conditions'
#swagger.description = 'Fetches folders from the specified workspace and environment, optionally providing either a parentFolderId or a parentFolderPath to narrow down results'
#swagger.security = [{
"apiKeyAuth": []
}]
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
res.send({ folders: [], dir: [] });
return;
}
if (!(req.authData.authPayload instanceof ServiceTokenData)) {
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles: [ADMIN, MEMBER]
});
}
// if instead of parentFolderId given a path like /folder1/folder2
if (parentFolderPath) {
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
environment,
parentFolderPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folder = getFolderByPath(folders.nodes, parentFolderPath);
if (!folder) {
res.send({ folders: [], dir: [] });
return;
}
// dir is not needed at present as this is only used in overview section of secrets
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir: [{ name: folder.name, id: folder.id }]
});
}
if (!parentFolderId) {
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/");
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
#swagger.parameters['workspaceId'] = {
"description": "ID of the workspace from which the folders are to be fetched",
"required": true,
"type": "string",
"in": "query"
}
const rootFolders = folders.nodes.children.map(({ id, name }) => ({
id,
name
}));
res.send({ folders: rootFolders });
return;
}
#swagger.parameters['environment'] = {
"description": "Environment where the folder is located",
"required": true,
"type": "string",
"in": "query"
}
#swagger.parameters['parentFolderId'] = {
"description": "ID of the parent folder",
"required": false,
"type": "string",
"in": "query"
}
#swagger.parameters['parentFolderPath'] = {
"description": "Path of the parent folder, like /folder1/folder2",
"required": false,
"type": "string",
"in": "query"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"folders": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "someFolderId"
},
"name": {
"type": "string",
"example": "someFolderName"
}
}
},
"description": "List of folders"
},
"dir": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "parentFolderName"
},
"id": {
"type": "string",
"example": "parentFolderId"
}
}
},
"description": "List of directories"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. For instance, 'The folder doesn't exist'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetFoldersV1, req);
const { folder, folderPath, dir } = getFolderWithPathFromId(folders.nodes, parentFolderId);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, folderPath);
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// check that user is a member of the workspace
await getUserProjectPermissions(req.user._id, workspaceId);
}
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
return res.send({ folders: [], dir: [] });
}
const folder = getFolderByPath(folders.nodes, directory);
return res.send({
folders: folder?.children?.map(({ id, name }) => ({ id, name })) || []
});
};

View File

@ -10,6 +10,8 @@ import {
getSmtpConfigured
} from "../../config";
import { validateUserEmail } from "../../validation";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
/**
* Signup step 1: Initialize account for user under email [email] and send a verification code
@ -19,7 +21,9 @@ import { validateUserEmail } from "../../validation";
* @returns
*/
export const beginEmailSignup = async (req: Request, res: Response) => {
const email: string = req.body.email;
const {
body: { email }
} = await validateRequest(reqValidator.BeginEmailSignUpV1, req);
// validate that email is not disposable
validateUserEmail(email);
@ -50,7 +54,9 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
*/
export const verifyEmailSignup = async (req: Request, res: Response) => {
let user;
const { email, code } = req.body;
const {
body: { email, code }
} = await validateRequest(reqValidator.VerifyEmailSignUpV1, req);
// initialize user account
user = await User.findOne({ email }).select("+publicKey");

View File

@ -1,5 +1,7 @@
import { Request, Response } from "express";
import { validateRequest } from "../../helpers/validation";
import { UserAction } from "../../models";
import * as reqValidator from "../../validation/action";
/**
* Add user action [action]
@ -8,26 +10,27 @@ import { UserAction } from "../../models";
* @returns
*/
export const addUserAction = async (req: Request, res: Response) => {
// add/record new action [action] for user with id [req.user._id]
const { action } = req.body;
// add/record new action [action] for user with id [req.user._id]
const {
body: { action }
} = await validateRequest(reqValidator.AddUserActionV1, req);
const userAction = await UserAction.findOneAndUpdate(
{
user: req.user._id,
action,
action
},
{ user: req.user._id, action },
{
new: true,
upsert: true,
upsert: true
}
);
return res.status(200).send({
message: "Successfully recorded user action",
userAction,
});
return res.status(200).send({
message: "Successfully recorded user action",
userAction
});
};
/**
@ -37,15 +40,17 @@ export const addUserAction = async (req: Request, res: Response) => {
* @returns
*/
export const getUserAction = async (req: Request, res: Response) => {
// get user action [action] for user with id [req.user._id]
const action: string = req.query.action as string;
// get user action [action] for user with id [req.user._id]
const {
query: { action }
} = await validateRequest(reqValidator.GetUserActionV1, req);
const userAction = await UserAction.findOne({
user: req.user._id,
action,
action
});
return res.status(200).send({
userAction,
});
return res.status(200).send({
userAction
});
};

View File

@ -1,16 +1,32 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { client, getRootEncryptionKey } from "../../config";
import { validateMembership } from "../../helpers";
import Webhook from "../../models/webhooks";
import { Webhook } from "../../models";
import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService";
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { ADMIN, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, MEMBER } from "../../variables";
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64 } from "../../variables";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/webhooks";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
export const createWebhook = async (req: Request, res: Response) => {
const { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath } = req.body;
const {
body: { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath }
} = await validateRequest(reqValidator.CreateWebhookV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Webhooks
);
const webhook = new Webhook({
workspace: workspaceId,
environment,
@ -29,7 +45,7 @@ export const createWebhook = async (req: Request, res: Response) => {
}
await webhook.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
@ -43,7 +59,7 @@ export const createWebhook = async (req: Request, res: Response) => {
}
},
{
workspaceId
workspaceId: new Types.ObjectId(workspaceId)
}
);
@ -54,19 +70,24 @@ export const createWebhook = async (req: Request, res: Response) => {
};
export const updateWebhook = async (req: Request, res: Response) => {
const { webhookId } = req.params;
const { isDisabled } = req.body;
const {
body: { isDisabled },
params: { webhookId }
} = await validateRequest(reqValidator.UpdateWebhookV1, req);
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId: webhook.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
const { permission } = await getUserProjectPermissions(
req.user._id,
webhook.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Webhooks
);
if (typeof isDisabled !== undefined) {
webhook.isDisabled = isDisabled;
@ -97,19 +118,24 @@ export const updateWebhook = async (req: Request, res: Response) => {
};
export const deleteWebhook = async (req: Request, res: Response) => {
const { webhookId } = req.params;
const {
params: { webhookId }
} = await validateRequest(reqValidator.DeleteWebhookV1, req);
let webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw ResourceNotFoundError({ message: "Webhook not found!!" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: webhook.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
const { permission } = await getUserProjectPermissions(
req.user._id,
webhook.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Webhooks
);
webhook = await Webhook.findByIdAndDelete(webhookId);
if (!webhook) {
@ -139,17 +165,23 @@ export const deleteWebhook = async (req: Request, res: Response) => {
};
export const testWebhook = async (req: Request, res: Response) => {
const { webhookId } = req.params;
const {
params: { webhookId }
} = await validateRequest(reqValidator.TestWebhookV1, req);
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: webhook.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
const { permission } = await getUserProjectPermissions(
req.user._id,
webhook.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks
);
try {
await triggerWebhookRequest(
@ -182,7 +214,15 @@ export const testWebhook = async (req: Request, res: Response) => {
};
export const listWebhooks = async (req: Request, res: Response) => {
const { environment, workspaceId, secretPath } = req.query;
const {
query: { environment, workspaceId, secretPath }
} = await validateRequest(reqValidator.ListWebhooksV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks
);
const optionalFilters: Record<string, string> = {};
if (environment) optionalFilters.environment = environment as string;

View File

@ -5,17 +5,28 @@ import {
Integration,
IntegrationAuth,
Membership,
MembershipOrg,
Organization,
ServiceToken,
Workspace,
Workspace
} from "../../models";
import {
createWorkspace as create,
deleteWorkspace as deleteWork,
} from "../../helpers/workspace";
import { createWorkspace as create, deleteWorkspace as deleteWork } from "../../helpers/workspace";
import { EELicenseService } from "../../ee/services";
import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables";
import { OrganizationNotFoundError } from "../../utils/errors";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
/**
* Return public keys of members of workspace with id [workspaceId]
@ -24,21 +35,29 @@ import { ADMIN } from "../../variables";
* @returns
*/
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspacePublicKeysV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
);
const publicKeys = (
await Membership.find({
workspace: workspaceId,
workspace: workspaceId
}).populate<{ user: IUser }>("user", "publicKey")
).map((member) => {
return {
publicKey: member.user.publicKey,
userId: member.user._id,
userId: member.user._id
};
});
return res.status(200).send({
publicKeys,
publicKeys
});
};
@ -49,14 +68,22 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
);
const users = await Membership.find({
workspace: workspaceId,
workspace: workspaceId
}).populate("user", "+publicKey");
return res.status(200).send({
users,
users
});
};
@ -69,12 +96,12 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
export const getWorkspaces = async (req: Request, res: Response) => {
const workspaces = (
await Membership.find({
user: req.user._id,
user: req.user._id
}).populate("workspace")
).map((m) => m.workspace);
return res.status(200).send({
workspaces,
workspaces
});
};
@ -85,14 +112,16 @@ export const getWorkspaces = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspace = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceV1, req);
const workspace = await Workspace.findOne({
_id: workspaceId,
_id: workspaceId
});
return res.status(200).send({
workspace,
workspace
});
};
@ -104,26 +133,32 @@ export const getWorkspace = async (req: Request, res: Response) => {
* @returns
*/
export const createWorkspace = async (req: Request, res: Response) => {
const { workspaceName, organizationId } = req.body;
const {
body: { organizationId, workspaceName }
} = await validateRequest(reqValidator.CreateWorkspaceV1, req);
// validate organization membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: new Types.ObjectId(organizationId),
});
if (!membershipOrg) {
throw new Error("Failed to validate organization membership");
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Workspace
);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (plan.workspaceLimit !== null) {
// case: limit imposed on number of workspaces allowed
if (plan.workspacesUsed >= plan.workspaceLimit) {
// case: number of workspaces used exceeds the number of workspaces allowed
return res.status(400).send({
message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces.",
message:
"Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
});
}
}
@ -135,17 +170,17 @@ export const createWorkspace = async (req: Request, res: Response) => {
// create workspace and add user as member
const workspace = await create({
name: workspaceName,
organizationId: new Types.ObjectId(organizationId),
organizationId: new Types.ObjectId(organizationId)
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN],
roles: [ADMIN]
});
return res.status(200).send({
workspace,
workspace
});
};
@ -156,15 +191,23 @@ export const createWorkspace = async (req: Request, res: Response) => {
* @returns
*/
export const deleteWorkspace = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.DeleteWorkspaceV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Workspace
);
// delete workspace
await deleteWork({
id: workspaceId,
id: workspaceId
});
return res.status(200).send({
message: "Successfully deleted workspace",
message: "Successfully deleted workspace"
});
};
@ -175,24 +218,32 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
* @returns
*/
export const changeWorkspaceName = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const { name } = req.body;
const {
params: { workspaceId },
body: { name }
} = await validateRequest(reqValidator.ChangeWorkspaceNameV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Workspace
);
const workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId,
_id: workspaceId
},
{
name,
name
},
{
new: true,
new: true
}
);
return res.status(200).send({
message: "Successfully changed workspace name",
workspace,
workspace
});
};
@ -203,14 +254,21 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceIntegrationsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
const integrations = await Integration.find({
workspace: workspaceId,
workspace: workspaceId
});
return res.status(200).send({
integrations,
integrations
});
};
@ -220,18 +278,23 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
* @param res
* @returns
*/
export const getWorkspaceIntegrationAuthorizations = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceIntegrationAuthorizationsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
const authorizations = await IntegrationAuth.find({
workspace: workspaceId,
workspace: workspaceId
});
return res.status(200).send({
authorizations,
authorizations
});
};
@ -241,18 +304,24 @@ export const getWorkspaceIntegrationAuthorizations = async (
* @param res
* @returns
*/
export const getWorkspaceServiceTokens = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
export const getWorkspaceServiceTokens = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceServiceTokensV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.ServiceTokens
);
// ?? FIX.
const serviceTokens = await ServiceToken.find({
user: req.user._id,
workspace: workspaceId,
workspace: workspaceId
});
return res.status(200).send({
serviceTokens,
serviceTokens
});
};

View File

@ -10,16 +10,11 @@ import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services";
import { EELogService } from "../../ee/services";
import { BadRequestError, InternalServerError } from "../../utils/errors";
import {
ACTION_LOGIN,
TOKEN_EMAIL_MFA,
} from "../../variables";
import { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables";
import { getUserAgentType } from "../../utils/posthog"; // TODO: move this
import {
getHttpsEnabled,
getJwtMfaLifetime,
getJwtMfaSecret,
} from "../../config";
import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module "jsonwebtoken" {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -34,13 +29,10 @@ declare module "jsonwebtoken" {
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
const {
email,
clientPublicKey,
}: { email: string; clientPublicKey: string } = req.body;
const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({
email,
email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
@ -49,25 +41,28 @@ export const login1 = async (req: Request, res: Response) => {
server.init(
{
salt: user.salt,
verifier: user.verifier,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: email }, {
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false });
await LoginSRPDetail.findOneAndReplace(
{ email: email },
{
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
},
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({
serverPublicKey,
salt: user.salt,
salt: user.salt
});
}
);
};
/**
@ -78,19 +73,22 @@ export const login1 = async (req: Request, res: Response) => {
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
if (!req.headers["user-agent"]) throw InternalServerError({ message: "User-Agent header is required" });
if (!req.headers["user-agent"])
throw InternalServerError({ message: "User-Agent header is required" });
const { email, clientProof } = req.body;
const user = await User.findOne({
email,
}).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices");
email
}).select(
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user");
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email });
if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"))
return BadRequestError(Error("Failed to find login details for SRP"));
}
const server = new jsrp.server();
@ -98,7 +96,7 @@ export const login2 = async (req: Request, res: Response) => {
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
@ -111,15 +109,15 @@ export const login2 = async (req: Request, res: Response) => {
// generate temporary MFA token
const token = createToken({
payload: {
userId: user._id.toString(),
userId: user._id.toString()
},
expiresIn: await getJwtMfaLifetime(),
secret: await getJwtMfaSecret(),
secret: await getJwtMfaSecret()
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email,
email
});
// send MFA code [code] to [email]
@ -128,27 +126,27 @@ export const login2 = async (req: Request, res: Response) => {
subjectLine: "Infisical MFA code",
recipients: [email],
substitutions: {
code,
},
code
}
});
return res.status(200).send({
mfaEnabled: true,
token,
token
});
}
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgent: req.headers["user-agent"] ?? ""
});
// issue tokens
const tokens = await issueAuthTokens({
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie
@ -156,7 +154,7 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled(),
secure: await getHttpsEnabled()
});
// case: user does not have MFA enabled
@ -182,36 +180,33 @@ export const login2 = async (req: Request, res: Response) => {
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
}
tag: user.tag
};
if (
user?.protectedKey &&
user?.protectedKeyIV &&
user?.protectedKeyTag
) {
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV
response.protectedKeyIV = user.protectedKeyIV;
response.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.ip,
});
loginAction &&
(await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.ip
}));
return res.status(200).send(response);
}
return res.status(400).send({
message: "Failed to authenticate. Try again?",
message: "Failed to authenticate. Try again?"
});
}
);
@ -219,15 +214,17 @@ export const login2 = async (req: Request, res: Response) => {
/**
* Send MFA token to email [email]
* @param req
* @param res
* @param req
* @param res
*/
export const sendMfaToken = async (req: Request, res: Response) => {
const { email } = req.body;
const {
body: { email }
} = await validateRequest(reqValidator.SendMfaTokenV2, req);
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email,
email
});
// send MFA code [code] to [email]
@ -236,49 +233,53 @@ export const sendMfaToken = async (req: Request, res: Response) => {
subjectLine: "Infisical MFA code",
recipients: [email],
substitutions: {
code,
},
code
}
});
return res.status(200).send({
message: "Successfully sent new MFA code",
message: "Successfully sent new MFA code"
});
}
};
/**
* Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
* MFA token [mfaToken] is valid
* @param req
* @param res
* @param req
* @param res
*/
export const verifyMfaToken = async (req: Request, res: Response) => {
const { email, mfaToken } = req.body;
const {
body: { email, mfaToken }
} = await validateRequest(reqValidator.VerifyMfaTokenV2, req);
await TokenService.validateToken({
type: TOKEN_EMAIL_MFA,
email,
token: mfaToken,
token: mfaToken
});
const user = await User.findOne({
email,
}).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices");
email
}).select(
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user");
await LoginSRPDetail.deleteOne({ userId: user.id })
await LoginSRPDetail.deleteOne({ userId: user.id });
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgent: req.headers["user-agent"] ?? ""
});
// issue tokens
const tokens = await issueAuthTokens({
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie
@ -286,7 +287,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled(),
secure: await getHttpsEnabled()
});
interface VerifyMfaTokenRes {
@ -319,8 +320,8 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
publicKey: user.publicKey as string,
encryptedPrivateKey: user.encryptedPrivateKey as string,
iv: user.iv as string,
tag: user.tag as string,
}
tag: user.tag as string
};
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
resObj.protectedKey = user.protectedKey;
@ -330,15 +331,16 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP,
});
loginAction &&
(await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP
}));
return res.status(200).send(resObj);
}
};

View File

@ -1,32 +1,135 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
Folder,
Integration,
Membership,
Secret,
ServiceToken,
ServiceTokenData,
Workspace,
Workspace
} from "../../models";
import { EventType, SecretVersion } from "../../ee/models";
import { EEAuditLogService, EELicenseService } from "../../ee/services";
import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors";
import _ from "lodash";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/environments";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { SecretImport } from "../../models";
import { ServiceAccountWorkspacePermission } from "../../models";
import { Webhook } from "../../models";
/**
* Create new workspace environment named [environmentName] under workspace with id
* Create new workspace environment named [environmentName]
* with slug [environmentSlug] under workspace with id
* @param req
* @param res
* @returns
*/
export const createWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
export const createWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Create environment'
#swagger.description = 'Create environment'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
/*
#swagger.summary = 'Create environment'
#swagger.description = 'Create environment'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environmentName": {
"type": "string",
"description": "Name of the environment",
"example": "development"
},
"environmentSlug": {
"type": "string",
"description": "Slug of the environment",
"example": "dev-environment"
}
},
"required": ["environmentName", "environmentSlug"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Successfully created new environment"
},
"workspace": {
"type": "string",
"example": "someWorkspaceId"
},
"environment": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "someEnvironmentName"
},
"slug": {
"type": "string",
"example": "someEnvironmentSlug"
}
}
}
},
"description": "Response after creating a new environment"
}
}
}
}
*/
const {
params: { workspaceId },
body: { environmentName, environmentSlug }
} = await validateRequest(reqValidator.CreateWorkspaceEnvironmentV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Environments
);
const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body;
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) throw WorkspaceNotFoundError();
@ -39,7 +142,8 @@ export const createWorkspaceEnvironment = async (
// case: number of environments used exceeds the number of environments allowed
return res.status(400).send({
message: "Failed to create environment due to environment limit reached. Upgrade plan to create more environments.",
message:
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
});
}
}
@ -55,7 +159,7 @@ export const createWorkspaceEnvironment = async (
workspace?.environments.push({
name: environmentName,
slug: environmentSlug.toLowerCase(),
slug: environmentSlug.toLowerCase()
});
await workspace.save();
@ -80,8 +184,8 @@ export const createWorkspaceEnvironment = async (
workspace: workspaceId,
environment: {
name: environmentName,
slug: environmentSlug,
},
slug: environmentSlug
}
});
};
@ -91,34 +195,46 @@ export const createWorkspaceEnvironment = async (
* @param res
* @returns
*/
export const reorderWorkspaceEnvironments = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const { environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName } = req.body;
export const reorderWorkspaceEnvironments = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { environmentName, environmentSlug, otherEnvironmentSlug, otherEnvironmentName }
} = await validateRequest(reqValidator.ReorderWorkspaceEnvironmentsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Environments
);
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw BadRequestError({message: "Couldn't load workspace"});
throw BadRequestError({ message: "Couldn't load workspace" });
}
const environmentIndex = workspace.environments.findIndex((env) => env.name === environmentName && env.slug === environmentSlug)
const otherEnvironmentIndex = workspace.environments.findIndex((env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug)
const environmentIndex = workspace.environments.findIndex(
(env) => env.name === environmentName && env.slug === environmentSlug
);
const otherEnvironmentIndex = workspace.environments.findIndex(
(env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug
);
if (environmentIndex === -1 || otherEnvironmentIndex === -1) {
throw BadRequestError({message: "environment or otherEnvironment couldn't be found"})
throw BadRequestError({ message: "environment or otherEnvironment couldn't be found" });
}
// swap the order of the environments
[workspace.environments[environmentIndex], workspace.environments[otherEnvironmentIndex]] = [workspace.environments[otherEnvironmentIndex], workspace.environments[environmentIndex]]
[workspace.environments[environmentIndex], workspace.environments[otherEnvironmentIndex]] = [
workspace.environments[otherEnvironmentIndex],
workspace.environments[environmentIndex]
];
await workspace.save()
await workspace.save();
return res.status(200).send({
message: "Successfully reordered environments",
workspace: workspaceId,
workspace: workspaceId
});
};
@ -129,12 +245,91 @@ export const reorderWorkspaceEnvironments = async (
* @param res
* @returns
*/
export const renameWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
export const renameWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Rename workspace environment'
#swagger.description = 'Rename a specific environment within a workspace'
#swagger.parameters['workspaceId'] = {
"description": "ID of the workspace",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environmentName": {
"type": "string",
"description": "New name for the environment",
"example": "Staging-Renamed"
},
"environmentSlug": {
"type": "string",
"description": "New slug for the environment",
"example": "staging-renamed"
},
"oldEnvironmentSlug": {
"type": "string",
"description": "Current slug of the environment to rename",
"example": "staging-old"
}
},
"required": ["environmentName", "environmentSlug", "oldEnvironmentSlug"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Successfully update environment"
},
"workspace": {
"type": "string",
"example": "someWorkspaceId"
},
"environment": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "Staging-Renamed"
},
"slug": {
"type": "string",
"example": "staging-renamed"
}
}
}
},
"description": "Details of the renamed environment"
}
}
}
}
*/
const {
params: { workspaceId },
body: { environmentName, environmentSlug, oldEnvironmentSlug }
} = await validateRequest(reqValidator.UpdateWorkspaceEnvironmentV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Environments
);
// user should pass both new slug and env name
if (!environmentSlug || !environmentName) {
throw new Error("Invalid environment given.");
@ -148,16 +343,13 @@ export const renameWorkspaceEnvironment = async (
const isEnvExist = workspace.environments.some(
({ name, slug }) =>
slug !== oldEnvironmentSlug &&
(name === environmentName || slug === environmentSlug)
slug !== oldEnvironmentSlug && (name === environmentName || slug === environmentSlug)
);
if (isEnvExist) {
throw new Error("Invalid environment given");
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === oldEnvironmentSlug
);
const envIndex = workspace?.environments.findIndex(({ slug }) => slug === oldEnvironmentSlug);
if (envIndex === -1) {
throw new Error("Invalid environment given");
}
@ -181,17 +373,42 @@ export const renameWorkspaceEnvironment = async (
{ environment: environmentSlug }
);
await ServiceTokenData.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
{
workspace: workspaceId,
"scopes.environment": oldEnvironmentSlug
},
{ $set: { "scopes.$[element].environment": environmentSlug } },
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] }
);
await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Folder.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretImport.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceAccountWorkspacePermission.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Webhook.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Membership.updateMany(
{
workspace: workspaceId,
"deniedPermissions.environmentSlug": oldEnvironmentSlug,
"deniedPermissions.environmentSlug": oldEnvironmentSlug
},
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
@ -218,8 +435,8 @@ export const renameWorkspaceEnvironment = async (
workspace: workspaceId,
environment: {
name: environmentName,
slug: environmentSlug,
},
slug: environmentSlug
}
});
};
@ -229,21 +446,83 @@ export const renameWorkspaceEnvironment = async (
* @param res
* @returns
*/
export const deleteWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const { environmentSlug } = req.body;
export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete workspace environment'
#swagger.description = 'Delete a specific environment from a workspace'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of the workspace",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environmentSlug": {
"type": "string",
"description": "Slug of the environment to delete",
"example": "dev-environment"
}
},
"required": ["environmentSlug"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Successfully deleted environment"
},
"workspace": {
"type": "string",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"example": "dev-environment"
}
},
"description": "Response after deleting an environment from a workspace"
}
}
}
}
*/
const {
params: { workspaceId },
body: { environmentSlug }
} = await validateRequest(reqValidator.DeleteWorkspaceEnvironmentV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Environments
);
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error("Failed to create workspace environment");
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === environmentSlug
);
const envIndex = workspace?.environments.findIndex(({ slug }) => slug === environmentSlug);
if (envIndex === -1) {
throw new Error("Invalid environment given");
}
@ -256,11 +535,11 @@ export const deleteWorkspaceEnvironment = async (
// clean up
await Secret.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
environment: environmentSlug
});
await SecretVersion.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
environment: environmentSlug
});
// await ServiceToken.deleteMany({
@ -279,7 +558,7 @@ export const deleteWorkspaceEnvironment = async (
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
environment: environmentSlug
});
await Membership.updateMany(
{ workspace: workspaceId },
@ -305,46 +584,100 @@ export const deleteWorkspaceEnvironment = async (
return res.status(200).send({
message: "Successfully deleted environment",
workspace: workspaceId,
environment: environmentSlug,
environment: environmentSlug
});
};
// TODO(akhilmhdh) after rbac this can be completely removed
export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Get all accessible environments of a workspace'
#swagger.description = 'Fetch all environments that the user has access to in a specified workspace'
#swagger.security = [{
"apiKeyAuth": []
}]
export const getAllAccessibleEnvironmentsOfWorkspace = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const workspacesUserIsMemberOf = await Membership.findOne({
workspace: workspaceId,
user: req.user,
})
#swagger.parameters['workspaceId'] = {
"description": "ID of the workspace",
"required": true,
"type": "string",
"in": "path"
}
if (!workspacesUserIsMemberOf) {
throw BadRequestError()
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"accessibleEnvironments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "Development"
},
"slug": {
"type": "string",
"example": "development"
},
"isWriteDenied": {
"type": "boolean",
"example": false
},
"isReadDenied": {
"type": "boolean",
"example": false
}
}
}
}
},
"description": "List of environments the user has access to in the specified workspace"
}
}
}
}
*/
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetAllAccessibileEnvironmentsOfWorkspaceV2, req);
const accessibleEnvironments: any = []
const deniedPermission = workspacesUserIsMemberOf.deniedPermissions
const { membership: workspacesUserIsMemberOf } = await getUserProjectPermissions(
req.user._id,
workspaceId
);
const relatedWorkspace = await Workspace.findById(workspaceId)
const accessibleEnvironments: any = [];
const deniedPermission = workspacesUserIsMemberOf.deniedPermissions;
const relatedWorkspace = await Workspace.findById(workspaceId);
if (!relatedWorkspace) {
throw BadRequestError()
throw BadRequestError();
}
relatedWorkspace.environments.forEach(environment => {
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS })
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS })
relatedWorkspace.environments.forEach((environment) => {
const isReadBlocked = _.some(deniedPermission, {
environmentSlug: environment.slug,
ability: PERMISSION_READ_SECRETS
});
const isWriteBlocked = _.some(deniedPermission, {
environmentSlug: environment.slug,
ability: PERMISSION_WRITE_SECRETS
});
if (isReadBlocked && isWriteBlocked) {
return
return;
} else {
accessibleEnvironments.push({
name: environment.name,
slug: environment.slug,
isWriteDenied: isWriteBlocked,
isReadDenied: isReadBlocked,
})
isReadDenied: isReadBlocked
});
}
})
});
res.json({ accessibleEnvironments })
res.json({ accessibleEnvironments });
};

View File

@ -1,21 +1,27 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
Membership,
MembershipOrg,
ServiceAccount,
Workspace,
} from "../../models";
import { Membership, MembershipOrg, ServiceAccount, Workspace } from "../../models";
import { deleteMembershipOrg } from "../../helpers/membershipOrg";
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
import Role from "../../ee/models/role";
import { BadRequestError } from "../../utils/errors";
import { CUSTOM } from "../../variables";
import * as reqValidator from "../../validation/organization";
import { validateRequest } from "../../helpers/validation";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Return memberships for organization with id [organizationId]
* @param req
* @param res
* @param req
* @param res
*/
export const getOrganizationMemberships = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Return organization memberships'
#swagger.description = 'Return organization memberships'
@ -48,24 +54,32 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
}
}
*/
const { organizationId } = req.params;
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv2, req);
const memberships = await MembershipOrg.find({
organization: organizationId,
}).populate("user", "+publicKey");
return res.status(200).send({
memberships,
});
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Member
);
const memberships = await MembershipOrg.find({
organization: organizationId
}).populate("user", "+publicKey");
return res.status(200).send({
memberships
});
};
/**
* Update role of membership with id [membershipId] to role [role]
* @param req
* @param res
* @param req
* @param res
*/
export const updateOrganizationMembership = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Update organization membership'
#swagger.description = 'Update organization membership'
@ -118,31 +132,58 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
}
}
*/
const { membershipId } = req.params;
const { role } = req.body;
const membership = await MembershipOrg.findByIdAndUpdate(
membershipId,
{
role,
}, {
new: true,
}
);
return res.status(200).send({
membership,
const {
params: { organizationId, membershipId },
body: { role }
} = await validateRequest(reqValidator.UpdateOrgMemberv2, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Member
);
const isCustomRole = !["admin", "member", "owner"].includes(role);
if (isCustomRole) {
const orgRole = await Role.findOne({ slug: role, isOrgRole: true });
if (!orgRole) throw BadRequestError({ message: "Role not found" });
const membership = await MembershipOrg.findByIdAndUpdate(membershipId, {
role: CUSTOM,
customRole: orgRole
});
}
return res.status(200).send({
membership
});
}
const membership = await MembershipOrg.findByIdAndUpdate(
membershipId,
{
$set: {
role
},
$unset: {
customRole: 1
}
},
{
new: true
}
);
return res.status(200).send({
membership
});
};
/**
* Delete organization membership with id [membershipId]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const deleteOrganizationMembership = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Delete organization membership'
#swagger.description = 'Delete organization membership'
@ -178,30 +219,37 @@ export const deleteOrganizationMembership = async (req: Request, res: Response)
}
}
*/
const { membershipId } = req.params;
// delete organization membership
const membership = await deleteMembershipOrg({
membershipOrgId: membershipId,
});
const {
params: { organizationId, membershipId }
} = await validateRequest(reqValidator.DeleteOrgMemberv2, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Member
);
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString(),
});
// delete organization membership
const membership = await deleteMembershipOrg({
membershipOrgId: membershipId
});
return res.status(200).send({
membership,
});
}
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString()
});
return res.status(200).send({
membership
});
};
/**
* Return workspaces for organization with id [organizationId] that user has
* access to
* @param req
* @param res
* @param req
* @param res
*/
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Return projects in organization that user is part of'
#swagger.description = 'Return projects in organization that user is part of'
@ -234,45 +282,53 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
}
}
*/
const { organizationId } = req.params;
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgWorkspacesv2, req);
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId,
},
"_id"
)
).map((w) => w._id.toString())
);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Workspace
);
const workspaces = (
await Membership.find({
user: req.user._id,
}).populate("workspace")
)
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
"_id"
)
).map((w) => w._id.toString())
);
const workspaces = (
await Membership.find({
user: req.user._id
}).populate("workspace")
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
return res.status(200).send({
workspaces,
});
}
return res.status(200).send({
workspaces
});
};
/**
* Return service accounts for organization with id [organizationId]
* @param req
* @param res
* @param req
* @param res
*/
export const getOrganizationServiceAccounts = async (req: Request, res: Response) => {
const { organizationId } = req.params;
const serviceAccounts = await ServiceAccount.find({
organization: new Types.ObjectId(organizationId),
});
return res.status(200).send({
serviceAccounts,
});
}
const { organizationId } = req.params;
const serviceAccounts = await ServiceAccount.find({
organization: new Types.ObjectId(organizationId)
});
return res.status(200).send({
serviceAccounts
});
};

View File

@ -1,6 +1,5 @@
import { Request, Response } from "express";
import mongoose, { Types } from "mongoose";
import Secret, { ISecret } from "../../models/secret";
import {
CreateSecretRequestBody,
ModifySecretRequestBody,
@ -20,7 +19,7 @@ import {
SECRET_SHARED
} from "../../variables";
import { TelemetryService } from "../../services";
import { User } from "../../models";
import { ISecret, Secret, User } from "../../models";
import { AccountNotFoundError } from "../../utils/errors";
/**

View File

@ -1,6 +1,6 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import { ISecret, Secret, ServiceTokenData } from "../../models";
import { Folder, ISecret, Secret, ServiceTokenData, Tag } from "../../models";
import { AuditLog, EventType, IAction, SecretVersion } from "../../ee/models";
import {
ACTION_ADD_SECRETS,
@ -24,10 +24,7 @@ import {
userHasWorkspaceAccess,
userHasWriteOnlyAbility
} from "../../ee/helpers/checkMembershipPermissions";
import Tag from "../../models/tag";
import _ from "lodash";
import { BatchSecret, BatchSecretRequest } from "../../types/secret";
import Folder from "../../models/folder";
import {
getFolderByPath,
getFolderIdFromServiceToken,
@ -37,6 +34,18 @@ import {
import { isValidScope } from "../../helpers/secrets";
import path from "path";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { validateRequest } from "../../helpers/validation";
import {
BatchSecretsV2,
GetSecretsV2,
validateServiceTokenDataClientForWorkspace
} from "../../validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
/**
* Peform a batch of any specified CUD secret operations
@ -48,22 +57,31 @@ export const batchSecrets = async (req: Request, res: Response) => {
const channel = getUserAgentType(req.headers["user-agent"]);
const postHogClient = await TelemetryService.getPostHogClient();
const validatedData = await validateRequest(BatchSecretsV2, req);
const {
workspaceId,
environment,
requests
}: {
workspaceId: string;
environment: string;
requests: BatchSecretRequest[];
} = req.body;
body: { workspaceId, environment, requests }
} = validatedData;
let {
body: { secretPath, folderId }
} = validatedData;
let secretPath = req.body.secretPath as string;
let folderId = req.body.folderId as string;
const secretIds = requests
.filter(({ method }) => method !== "POST")
// akhilmhdh: ts is dumb
.map((el) => new Types.ObjectId((el.secret as any)._id));
const createSecrets: BatchSecret[] = [];
const updateSecrets: BatchSecret[] = [];
const deleteSecrets: { _id: Types.ObjectId, secretName: string; }[] = [];
const oldSecrets = await Secret.find({
_id: {
$in: secretIds
}
});
if (oldSecrets.length != secretIds.length) {
throw BadRequestError({ message: "Failed to validate non-existent secrets" });
}
const createSecrets: any[] = [];
const updateSecrets: any[] = [];
const deleteSecrets: { _id: Types.ObjectId; secretName: string }[] = [];
const actions: IAction[] = [];
// get secret blind index salt
@ -71,31 +89,33 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId)
});
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
// in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used
if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
if (secretPath) {
if (secretPath !== "/") {
folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
}
if (folders && folderId !== "root") {
if (folderId !== "root") {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) throw BadRequestError({ message: "Folder not found" });
const folder = searchByFolderIdWithDir(folders.nodes, folderId as string);
if (!folder?.folder) throw BadRequestError({ message: "Folder not found" });
secretPath = path.join(
"/",
...folder.dir.map(({ name }) => name).filter((name) => name !== "root")
);
}
if (req.authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
for await (const request of requests) {
// do a validation
@ -112,7 +132,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
version: 1,
user: request.secret.type === SECRET_PERSONAL ? req.user : undefined,
environment,
workspace: new Types.ObjectId(workspaceId),
workspace: workspaceId,
folder: folderId,
secretBlindIndex,
algorithm: ALGORITHM_AES_256_GCM,
@ -127,7 +147,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
updateSecrets.push({
...request.secret,
_id: new Types.ObjectId(request.secret._id),
_id: request.secret._id,
secretBlindIndex,
folder: folderId,
algorithm: ALGORITHM_AES_256_GCM,
@ -135,15 +155,39 @@ export const batchSecrets = async (req: Request, res: Response) => {
});
break;
case "DELETE":
deleteSecrets.push({ _id: new Types.ObjectId(request.secret._id), secretName: request.secret.secretName });
deleteSecrets.push({
_id: new Types.ObjectId(request.secret._id),
secretName: request.secret.secretName
});
break;
}
}
// not using service token using auth
if (!(req.authData.authPayload instanceof ServiceTokenData)) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
if (createSecrets.length)
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
if (updateSecrets.length)
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
if (deleteSecrets.length)
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
// handle create secrets
let createdSecrets: ISecret[] = [];
if (createSecrets.length > 0) {
createdSecrets = await Secret.insertMany(createSecrets);
createdSecrets = (await Secret.insertMany(createSecrets)) as any;
// (EE) add secret versions for new secrets
await EESecretService.addSecretVersions({
secretVersions: createdSecrets.map((n: any) => {
@ -208,7 +252,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
// handle update secrets
let updatedSecrets: ISecret[] = [];
if (updateSecrets.length > 0 && req.secrets) {
if (updateSecrets.length > 0 && oldSecrets) {
// construct object containing all secrets
let listedSecretsObj: {
[key: string]: {
@ -217,7 +261,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
};
} = {};
listedSecretsObj = req.secrets.reduce(
listedSecretsObj = oldSecrets.reduce(
(obj: any, secret: ISecret) => ({
...obj,
[secret._id.toString()]: secret
@ -229,21 +273,21 @@ export const batchSecrets = async (req: Request, res: Response) => {
updateOne: {
filter: {
_id: new Types.ObjectId(u._id),
workspace: new Types.ObjectId(workspaceId)
workspace: new Types.ObjectId(workspaceId),
environment
},
update: {
$inc: {
version: 1
},
$unset: {
'metadata.source': true as true
"metadata.source": true as const
},
...u,
_id: new Types.ObjectId(u._id)
}
}
}));
await Secret.bulkWrite(updateOperations);
const secretVersions = updateSecrets.map(
@ -334,23 +378,26 @@ export const batchSecrets = async (req: Request, res: Response) => {
if (deleteSecrets.length > 0) {
const deleteSecretIds: Types.ObjectId[] = deleteSecrets.map((s) => s._id);
const deletedSecretsObj = (await Secret.find({
_id: {
$in: deleteSecretIds
}
}))
.reduce(
(obj: any, secret: ISecret) => ({
...obj,
[secret._id.toString()]: secret
}),
{}
);
const deletedSecretsObj = (
await Secret.find({
_id: {
$in: deleteSecretIds
}
})
).reduce(
(obj: any, secret: ISecret) => ({
...obj,
[secret._id.toString()]: secret
}),
{}
);
await Secret.deleteMany({
_id: {
$in: deleteSecretIds
}
},
workspace: new Types.ObjectId(workspaceId),
environment
});
await EESecretService.markDeletedSecretVersions({
@ -783,10 +830,13 @@ export const getSecrets = async (req: Request, res: Response) => {
}
*/
const { tagSlugs, secretPath, include_imports } = req.query;
let { folderId } = req.query;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const validatedData = await validateRequest(GetSecretsV2, req);
const {
query: { tagSlugs, secretPath, include_imports, workspaceId, environment }
} = validatedData;
let {
query: { folderId }
} = validatedData;
const folders = await Folder.findOne({ workspace: workspaceId, environment });
@ -928,8 +978,14 @@ export const getSecrets = async (req: Request, res: Response) => {
// TODO(akhilmhdh) - secret-imp change this to org type
let importedSecrets: any[] = [];
if (include_imports === "true") {
importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string);
if (include_imports) {
// depreciated
importedSecrets = await getAllImportedSecrets(
workspaceId,
environment,
folderId as string,
() => false
);
}
const channel = getUserAgentType(req.headers["user-agent"]);
@ -972,17 +1028,17 @@ export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
// reduce the number of events captured
let shouldRecordK8Event = false
let shouldRecordK8Event = false;
if (req.authData.userAgent == K8_USER_AGENT_NAME) {
const randomNumber = Math.random();
if (randomNumber > 0.9) {
shouldRecordK8Event = true
shouldRecordK8Event = true;
}
}
if (postHogClient) {
const shouldCapture = req.authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event;
const approximateForNoneCapturedEvents = secrets.length * 10
const approximateForNoneCapturedEvents = secrets.length * 10;
if (shouldCapture) {
postHogClient.capture({
@ -1106,10 +1162,10 @@ export const updateSecrets = async (req: Request, res: Response) => {
tags,
...(secretCommentCiphertext !== undefined && secretCommentIV && secretCommentTag
? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
}
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
}
: {})
}
}

View File

@ -6,6 +6,15 @@ import { getSaltRounds } from "../../config";
import { BadRequestError } from "../../utils/errors";
import { ActorType, EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/serviceTokenData";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { Types } from "mongoose";
/**
* Return service token data associated with service token on request
@ -63,7 +72,14 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData;
const { name, workspaceId, encryptedKey, iv, tag, expiresIn, permissions, scopes } = req.body;
const {
body: { workspaceId, permissions, tag, encryptedKey, scopes, name, expiresIn, iv }
} = await validateRequest(reqValidator.CreateServiceTokenV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.ServiceTokens
);
const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
@ -75,7 +91,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
}
let user;
if (req.authData.actor.type === ActorType.USER) {
user = req.authData.authPayload._id;
}
@ -100,7 +116,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
if (!serviceTokenData) throw new Error("Failed to find service token data");
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
await EEAuditLogService.createAuditLog(
req.authData,
{
@ -111,7 +127,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
}
},
{
workspaceId
workspaceId: new Types.ObjectId(workspaceId)
}
);
@ -128,14 +144,29 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
* @returns
*/
export const deleteServiceTokenData = async (req: Request, res: Response) => {
const { serviceTokenDataId } = req.params;
const {
params: { serviceTokenDataId }
} = await validateRequest(reqValidator.DeleteServiceTokenV2, req);
let serviceTokenData = await ServiceTokenData.findById(serviceTokenDataId);
if (!serviceTokenData) throw BadRequestError({ message: "Service token not found" });
const { permission } = await getUserProjectPermissions(
req.user._id,
serviceTokenData.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.ServiceTokens
);
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
if (!serviceTokenData)
return res.status(200).send({
message: "Failed to delete service token"
});
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
if (!serviceTokenData) return res.status(200).send({
message: "Failed to delete service token"
});
await EEAuditLogService.createAuditLog(
req.authData,
{

View File

@ -1,43 +1,58 @@
import { ForbiddenError } from "@casl/ability";
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Membership, Secret } from "../../models";
import Tag from "../../models/tag";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { Secret, Tag } from "../../models";
import { BadRequestError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import * as reqValidator from "../../validation/tags";
export const createWorkspaceTag = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const { name, slug, tagColor } = req.body;
const {
body: { name, slug },
params: { workspaceId }
} = await validateRequest(reqValidator.CreateWorkspaceTagsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Tags
);
const tagToCreate = {
name,
tagColor,
workspace: new Types.ObjectId(workspaceId),
slug,
user: new Types.ObjectId(req.user._id),
};
name,
workspace: new Types.ObjectId(workspaceId),
slug,
user: new Types.ObjectId(req.user._id)
};
const createdTag = await new Tag(tagToCreate).save();
res.json(createdTag);
};
export const deleteWorkspaceTag = async (req: Request, res: Response) => {
const { tagId } = req.params;
const {
params: { tagId }
} = await validateRequest(reqValidator.DeleteWorkspaceTagsV2, req);
const tagFromDB = await Tag.findById(tagId);
if (!tagFromDB) {
throw BadRequestError();
}
// can only delete if the request user is one that belongs to the same workspace as the tag
const membership = await Membership.findOne({
user: req.user,
workspace: tagFromDB.workspace
});
if (!membership) {
UnauthorizedRequestError({ message: "Failed to validate membership" });
}
const { permission } = await getUserProjectPermissions(
req.user._id,
tagFromDB.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Tags
);
const result = await Tag.findByIdAndDelete(tagId);
@ -48,12 +63,19 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => {
};
export const getWorkspaceTags = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const workspaceTags = await Tag.find({
workspace: new Types.ObjectId(workspaceId)
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceTagsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Tags
);
const workspaceTags = await Tag.find({
workspace: new Types.ObjectId(workspaceId)
});
return res.json({
workspaceTags
});

View File

@ -2,23 +2,19 @@ import { Request, Response } from "express";
import { Types } from "mongoose";
import crypto from "crypto";
import bcrypt from "bcrypt";
import {
APIKeyData,
AuthMethod,
MembershipOrg,
TokenVersion,
User
} from "../../models";
import { APIKeyData, AuthMethod, MembershipOrg, TokenVersion, User } from "../../models";
import { getSaltRounds } from "../../config";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation";
/**
* Return the current user.
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getMe = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = "Retrieve the current user on the request"
#swagger.description = "Retrieve the current user on the request"
@ -43,124 +39,117 @@ export const getMe = async (req: Request, res: Response) => {
}
}
*/
const user = await User
.findById(req.user._id)
.select("+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag");
return res.status(200).send({
user,
});
}
const user = await User.findById(req.user._id).select(
"+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"
);
return res.status(200).send({
user
});
};
/**
* Update the current user's MFA-enabled status [isMfaEnabled].
* Note: Infisical currently only supports email-based 2FA only; this will expand to
* include SMS and authenticator app modes of authentication in the future.
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
req.user.isMfaEnabled = isMfaEnabled;
if (isMfaEnabled) {
// TODO: adapt this route/controller
// to work for different forms of MFA
req.user.mfaMethods = ["email"];
} else {
req.user.mfaMethods = [];
}
const {
body: { isMfaEnabled }
} = await validateRequest(reqValidator.UpdateMyMfaEnabledV2, req);
await req.user.save();
const user = req.user;
return res.status(200).send({
user,
});
}
req.user.isMfaEnabled = isMfaEnabled;
if (isMfaEnabled) {
// TODO: adapt this route/controller
// to work for different forms of MFA
req.user.mfaMethods = ["email"];
} else {
req.user.mfaMethods = [];
}
await req.user.save();
const user = req.user;
return res.status(200).send({
user
});
};
/**
* Update name of the current user to [firstName, lastName].
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const updateName = async (req: Request, res: Response) => {
const {
firstName,
lastName
}: {
firstName: string;
lastName: string;
} = req.body;
const user = await User.findByIdAndUpdate(
req.user._id.toString(),
{
firstName,
lastName: lastName ?? ""
},
{
new: true
}
);
return res.status(200).send({
user,
});
}
const {
body: { lastName, firstName }
} = await validateRequest(reqValidator.UpdateNameV2, req);
const user = await User.findByIdAndUpdate(
req.user._id.toString(),
{
firstName,
lastName: lastName ?? ""
},
{
new: true
}
);
return res.status(200).send({
user
});
};
/**
* Update auth method of the current user to [authMethods]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const updateAuthMethods = async (req: Request, res: Response) => {
const {
authMethods
} = req.body;
const hasSamlEnabled = req.user.authMethods
.some(
(authMethod: AuthMethod) => [
AuthMethod.OKTA_SAML,
AuthMethod.AZURE_SAML,
AuthMethod.JUMPCLOUD_SAML
].includes(authMethod)
);
export const updateAuthMethods = async (req: Request, res: Response) => {
const {
body: { authMethods }
} = await validateRequest(reqValidator.UpdateAuthMethodsV2, req);
if (hasSamlEnabled) {
return res.status(400).send({
message: "Failed to update user authentication method because SAML SSO is enforced"
});
}
const hasSamlEnabled = req.user.authMethods.some((authMethod: AuthMethod) =>
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod)
);
const user = await User.findByIdAndUpdate(
req.user._id.toString(),
{
authMethods
},
{
new: true
}
);
return res.status(200).send({
user
if (hasSamlEnabled) {
return res.status(400).send({
message: "Failed to update user authentication method because SAML SSO is enforced"
});
}
}
const user = await User.findByIdAndUpdate(
req.user._id.toString(),
{
authMethods
},
{
new: true
}
);
return res.status(200).send({
user
});
};
/**
* Return organizations that the current user is part of.
* @param req
* @param res
* @param req
* @param res
*/
export const getMyOrganizations = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Return organizations that current user is part of'
#swagger.description = 'Return organizations that current user is part of'
@ -189,114 +178,121 @@ export const getMyOrganizations = async (req: Request, res: Response) => {
*/
const organizations = (
await MembershipOrg.find({
user: req.user._id,
user: req.user._id
}).populate("organization")
).map((m) => m.organization);
return res.status(200).send({
organizations,
});
}
return res.status(200).send({
organizations
});
};
/**
* Return API keys belonging to current user.
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getMyAPIKeys = async (req: Request, res: Response) => {
const apiKeyData = await APIKeyData.find({
user: req.user._id,
});
const apiKeyData = await APIKeyData.find({
user: req.user._id
});
return res.status(200).send(apiKeyData);
}
return res.status(200).send(apiKeyData);
};
/**
* Create new API key for current user.
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const createAPIKey = async (req: Request, res: Response) => {
const { name, expiresIn } = req.body;
const {
body: { name, expiresIn }
} = await validateRequest(reqValidator.CreateApiKeyV2, req);
const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
let apiKeyData = await new APIKeyData({
name,
lastUsed: new Date(),
expiresAt,
user: req.user._id,
secretHash,
}).save();
let apiKeyData = await new APIKeyData({
name,
lastUsed: new Date(),
expiresAt,
user: req.user._id,
secretHash
}).save();
// return api key data without sensitive data
apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any;
// return api key data without sensitive data
apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any;
if (!apiKeyData) throw new Error("Failed to find API key data");
if (!apiKeyData) throw new Error("Failed to find API key data");
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
return res.status(200).send({
apiKey,
apiKeyData,
});
}
return res.status(200).send({
apiKey,
apiKeyData
});
};
/**
* Delete API key with id [apiKeyDataId] belonging to current user
* @param req
* @param res
* @param req
* @param res
*/
export const deleteAPIKey = async (req: Request, res: Response) => {
const { apiKeyDataId } = req.params;
const {
params: { apiKeyDataId }
} = await validateRequest(reqValidator.DeleteApiKeyV2, req);
const apiKeyData = await APIKeyData.findOneAndDelete({
_id: new Types.ObjectId(apiKeyDataId),
user: req.user._id
});
const apiKeyData = await APIKeyData.findOneAndDelete({
_id: new Types.ObjectId(apiKeyDataId),
user: req.user._id
});
return res.status(200).send({
apiKeyData
});
}
return res.status(200).send({
apiKeyData
});
};
/**
* Return active sessions (TokenVersion) belonging to user
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getMySessions = async (req: Request, res: Response) => {
const tokenVersions = await TokenVersion.find({
user: req.user._id
});
return res.status(200).send(tokenVersions);
}
const tokenVersions = await TokenVersion.find({
user: req.user._id
});
return res.status(200).send(tokenVersions);
};
/**
* Revoke all active sessions belong to user
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const deleteMySessions = async (req: Request, res: Response) => {
await TokenVersion.updateMany({
user: req.user._id,
}, {
$inc: {
refreshVersion: 1,
accessVersion: 1,
},
});
await TokenVersion.updateMany(
{
user: req.user._id
},
{
$inc: {
refreshVersion: 1,
accessVersion: 1
}
}
);
return res.status(200).send({
message: "Successfully revoked all sessions"
});
}
return res.status(200).send({
message: "Successfully revoked all sessions"
});
};

View File

@ -11,6 +11,14 @@ import { EventService, TelemetryService } from "../../services";
import { eventPushSecrets } from "../../events";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
interface V2PushSecret {
type: string; // personal or shared
@ -181,15 +189,17 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
}
}
*/
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceKeyV2, req);
const key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
}).populate("sender", "+publicKey");
if (!key) throw new Error("Failed to find workspace key");
await EEAuditLogService.createAuditLog(
req.authData,
{
@ -258,7 +268,15 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
}
}
*/
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
);
const memberships = await Membership.find({
workspace: workspaceId
@ -329,8 +347,16 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
}
}
*/
const { membershipId } = req.params;
const { role } = req.body;
const {
params: { workspaceId, membershipId },
body: { role }
} = await validateRequest(reqValidator.UpdateWorkspaceMembershipsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
const membership = await Membership.findByIdAndUpdate(
membershipId,
@ -390,7 +416,15 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
}
}
*/
const { membershipId } = req.params;
const {
params: { workspaceId, membershipId }
} = await validateRequest(reqValidator.DeleteWorkspaceMembershipsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Member
);
const membership = await Membership.findByIdAndDelete(membershipId);
@ -413,8 +447,16 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
* @returns
*/
export const toggleAutoCapitalization = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const { autoCapitalization } = req.body;
const {
params: { workspaceId },
body: { autoCapitalization }
} = await validateRequest(reqValidator.ToggleAutoCapitalizationV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Settings
);
const workspace = await Workspace.findOneAndUpdate(
{

View File

@ -10,25 +10,20 @@ import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services";
import { EELogService } from "../../ee/services";
import { BadRequestError, InternalServerError } from "../../utils/errors";
import {
ACTION_LOGIN,
TOKEN_EMAIL_MFA,
} from "../../variables";
import { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables";
import { getUserAgentType } from "../../utils/posthog"; // TODO: move this
import {
getHttpsEnabled,
getJwtMfaLifetime,
getJwtMfaSecret,
} from "../../config";
import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config";
import { AuthMethod } from "../../models/user";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module "jsonwebtoken" {
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
userId: string;
email: string;
authProvider: AuthMethod;
isUserCompleted: boolean,
}
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
userId: string;
email: string;
authProvider: AuthMethod;
isUserCompleted: boolean;
}
}
/**
@ -38,53 +33,51 @@ declare module "jsonwebtoken" {
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
const {
email,
providerAuthToken,
clientPublicKey,
}: {
email: string;
clientPublicKey: string,
providerAuthToken?: string;
} = req.body;
const user = await User.findOne({
email,
}).select("+salt +verifier");
const {
body: { email, clientPublicKey, providerAuthToken }
} = await validateRequest(reqValidator.Login1V3, req);
if (!user) throw new Error("Failed to find user");
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await validateProviderAuthToken({
email,
providerAuthToken,
});
}
const user = await User.findOne({
email
}).select("+salt +verifier");
const server = new jsrp.server();
server.init(
if (!user) throw new Error("Failed to find user");
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await validateProviderAuthToken({
email,
providerAuthToken
});
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace(
{
salt: user.salt,
verifier: user.verifier,
email: email
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({
email: email,
}, {
email,
userId: user.id,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false });
{
email,
userId: user.id,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
},
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({
serverPublicKey,
salt: user.salt,
});
}
);
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
};
/**
@ -95,150 +88,151 @@ export const login1 = async (req: Request, res: Response) => {
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
if (!req.headers["user-agent"]) throw InternalServerError({ message: "User-Agent header is required" });
if (!req.headers["user-agent"])
throw InternalServerError({ message: "User-Agent header is required" });
const { email, clientProof, providerAuthToken } = req.body;
const {
body: { email, providerAuthToken, clientProof }
} = await validateRequest(reqValidator.Login2V3, req);
const user = await User.findOne({
email,
}).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices");
const user = await User.findOne({
email
}).select(
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user");
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await validateProviderAuthToken({
email,
providerAuthToken,
})
}
if (!user) throw new Error("Failed to find user");
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await validateProviderAuthToken({
email,
providerAuthToken
});
}
if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"))
}
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email });
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt,
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"));
}
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
if (user.isMfaEnabled) {
// case: user has MFA enabled
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
// generate temporary MFA token
const token = createToken({
payload: {
userId: user._id.toString(),
},
expiresIn: await getJwtMfaLifetime(),
secret: await getJwtMfaSecret(),
});
// generate temporary MFA token
const token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtMfaLifetime(),
secret: await getJwtMfaSecret()
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email,
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: "emailMfa.handlebars",
subjectLine: "Infisical MFA code",
recipients: [user.email],
substitutions: {
code,
},
});
return res.status(200).send({
mfaEnabled: true,
token,
});
}
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
});
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled(),
});
// case: user does not have MFA enablgged
// return (access) token in response
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
}
if (
user?.protectedKey &&
user?.protectedKeyIV &&
user?.protectedKeyTag
) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV
response.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id,
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP,
});
return res.status(200).send(response);
// send MFA code [code] to [email]
await sendMail({
template: "emailMfa.handlebars",
subjectLine: "Infisical MFA code",
recipients: [user.email],
substitutions: {
code
}
});
return res.status(400).send({
message: "Failed to authenticate. Try again?",
});
return res.status(200).send({
mfaEnabled: true,
token
});
}
);
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
// case: user does not have MFA enablgged
// return (access) token in response
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
};
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV;
response.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction &&
(await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP
}));
return res.status(200).send(response);
}
return res.status(400).send({
message: "Failed to authenticate. Try again?"
});
}
);
};

View File

@ -6,12 +6,19 @@ import { BotService } from "../../services";
import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
import { Folder, IServiceTokenData } from "../../models";
import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { IServiceTokenData } from "../../models";
import { requireWorkspaceAuth } from "../../middleware";
import { ADMIN, MEMBER, PERMISSION_READ_SECRETS } from "../../variables";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secrets";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
import { validateServiceTokenDataClientForWorkspace } from "../../validation";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
/**
* Return secrets for workspace with id [workspaceId] and environment
@ -20,33 +27,67 @@ import { ADMIN, MEMBER, PERMISSION_READ_SECRETS } from "../../variables";
* @param res
*/
export const getSecretsRaw = async (req: Request, res: Response) => {
let workspaceId = req.query.workspaceId as string;
let environment = req.query.environment as string;
let secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
const validatedData = await validateRequest(reqValidator.GetSecretsRawV3, req);
let {
query: { secretPath, environment, workspaceId }
} = validatedData;
const {
query: { folderId, include_imports: includeImports }
} = validatedData;
// if the service token has single scope, it will get all secrets for that scope by default
const serviceTokenDetails: IServiceTokenData = req?.serviceTokenData;
if (serviceTokenDetails && serviceTokenDetails.scopes.length == 1 && !containsGlobPatterns(serviceTokenDetails.scopes[0].secretPath)) {
if (
serviceTokenDetails &&
serviceTokenDetails.scopes.length == 1 &&
!containsGlobPatterns(serviceTokenDetails.scopes[0].secretPath)
) {
const scope = serviceTokenDetails.scopes[0];
secretPath = scope.secretPath;
environment = scope.environment;
workspaceId = serviceTokenDetails.workspace.toString();
} else {
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query",
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true
});
}
if (folderId && folderId !== "root") {
const folder = await Folder.findOne({ workspace: workspaceId, environment });
if (!folder) throw BadRequestError({ message: "Folder not found" });
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
}
if (!environment || !workspaceId)
throw BadRequestError({ message: "Missing environment or workspace id" });
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
permissionCheckFn = () => true;
}
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId,
secretPath,
authData: req.authData
});
@ -55,7 +96,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId)
});
if (includeImports === "true") {
if (includeImports) {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root";
// if folder exist get it and replace folderid with new one
@ -66,7 +107,12 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
}
folderId = folder.id;
}
const importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId);
const importedSecrets = await getAllImportedSecrets(
workspaceId,
environment,
folderId,
permissionCheckFn
);
return res.status(200).send({
secrets: secrets.map((secret) =>
repackageSecretToRaw({
@ -98,11 +144,26 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
* @param res
*/
export const getSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const type = req.query.type as "shared" | "personal" | undefined;
const {
query: { secretPath, environment, workspaceId, type, include_imports },
params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
}
const secret = await SecretService.getSecret({
secretName,
@ -110,7 +171,8 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
environment,
type,
secretPath,
authData: req.authData
authData: req.authData,
include_imports
});
const key = await BotService.getWorkspaceKeyWithBot({
@ -131,8 +193,34 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
* @param res
*/
export const createSecretRaw = async (req: Request, res: Response) => {
const { secretName } = req.params;
const { workspaceId, environment, type, secretValue, secretComment, secretPath = "/" } = req.body;
const {
params: { secretName },
body: {
secretPath,
environment,
workspaceId,
type,
secretValue,
secretComment,
skipMultilineEncoding
}
} = await validateRequest(reqValidator.CreateSecretRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
@ -168,7 +256,8 @@ export const createSecretRaw = async (req: Request, res: Response) => {
secretPath,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag
secretCommentTag: secretCommentEncrypted.tag,
skipMultilineEncoding
});
await EventService.handleEvent({
@ -196,8 +285,26 @@ export const createSecretRaw = async (req: Request, res: Response) => {
* @param res
*/
export const updateSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params;
const { workspaceId, environment, type, secretValue, secretPath = "/" } = req.body;
const {
params: { secretName },
body: { secretValue, environment, secretPath, type, workspaceId, skipMultilineEncoding }
} = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
@ -210,14 +317,15 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
const secret = await SecretService.updateSecret({
secretName,
workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretPath
secretPath,
skipMultilineEncoding
});
await EventService.handleEvent({
@ -242,12 +350,30 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
* @param res
*/
export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params;
const { workspaceId, environment, type, secretPath = "/" } = req.body;
const {
params: { secretName },
body: { environment, secretPath, type, workspaceId }
} = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const { secret } = await SecretService.deleteSecret({
secretName,
workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
@ -281,19 +407,57 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
* @param res
*/
export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
const validatedData = await validateRequest(reqValidator.GetSecretsV3, req);
const {
query: { environment, workspaceId, include_imports: includeImports, folderId }
} = validatedData;
let {
query: { secretPath }
} = validatedData;
if (folderId && folderId !== "root") {
const folder = await Folder.findOne({ workspace: workspaceId, environment });
if (!folder) throw BadRequestError({ message: "Folder not found" });
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
}
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
permissionCheckFn = () => true;
}
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId,
secretPath,
authData: req.authData
});
if (includeImports === "true") {
if (includeImports) {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root";
// if folder exist get it and replace folderid with new one
@ -304,7 +468,12 @@ export const getSecrets = async (req: Request, res: Response) => {
}
folderId = folder.id;
}
const importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId);
const importedSecrets = await getAllImportedSecrets(
workspaceId,
environment,
folderId,
permissionCheckFn
);
return res.status(200).send({
secrets,
imports: importedSecrets
@ -322,11 +491,26 @@ export const getSecrets = async (req: Request, res: Response) => {
* @param res
*/
export const getSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const type = req.query.type as "shared" | "personal" | undefined;
const {
query: { secretPath, environment, workspaceId, type, include_imports },
params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
}
const secret = await SecretService.getSecret({
secretName,
@ -334,7 +518,8 @@ export const getSecretByName = async (req: Request, res: Response) => {
environment,
type,
secretPath,
authData: req.authData
authData: req.authData,
include_imports
});
return res.status(200).send({
@ -348,23 +533,42 @@ export const getSecretByName = async (req: Request, res: Response) => {
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretPath = "/",
metadata
} = req.body;
body: {
workspaceId,
secretPath,
environment,
metadata,
type,
secretKeyIV,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding
},
params: { secretName }
} = await validateRequest(reqValidator.CreateSecretV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const secret = await SecretService.createSecret({
secretName,
@ -382,7 +586,8 @@ export const createSecret = async (req: Request, res: Response) => {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
metadata
metadata,
skipMultilineEncoding
});
await EventService.handleEvent({
@ -407,27 +612,66 @@ export const createSecret = async (req: Request, res: Response) => {
* @param res
*/
export const updateSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath = "/"
} = req.body;
body: {
secretValueCiphertext,
secretValueTag,
secretValueIV,
type,
environment,
secretPath,
workspaceId,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
secretName: newSecretName,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
skipMultilineEncoding
},
params: { secretName }
} = await validateRequest(reqValidator.UpdateSecretByNameV3, req);
if (newSecretName && (!secretKeyIV || !secretKeyTag || !secretKeyCiphertext))
throw BadRequestError({ message: "Missing encrypted key" });
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const secret = await SecretService.updateSecret({
secretName,
workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
newSecretName,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath
secretPath,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
});
await EventService.handleEvent({
@ -449,12 +693,30 @@ export const updateSecretByName = async (req: Request, res: Response) => {
* @param res
*/
export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const { workspaceId, environment, type, secretPath = "/" } = req.body;
const {
body: { type, environment, secretPath, workspaceId },
params: { secretName }
} = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const { secret } = await SecretService.deleteSecret({
secretName,
workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
@ -473,3 +735,105 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
secret
});
};
export const createSecretByNameBatch = async (req: Request, res: Response) => {
const {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const createdSecrets = await SecretService.createSecretBatch({
secretPath,
environment,
workspaceId: new Types.ObjectId(workspaceId),
secrets,
authData: req.authData
});
return res.status(200).send({
secrets: createdSecrets
});
};
export const updateSecretByNameBatch = async (req: Request, res: Response) => {
const {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.UpdateSecretByNameBatchV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const updatedSecrets = await SecretService.updateSecretBatch({
secretPath,
environment,
workspaceId: new Types.ObjectId(workspaceId),
secrets,
authData: req.authData
});
return res.status(200).send({
secrets: updatedSecrets
});
};
export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
const {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.DeleteSecretByNameBatchV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const deletedSecrets = await SecretService.deleteSecretBatch({
secretPath,
environment,
workspaceId: new Types.ObjectId(workspaceId),
secrets,
authData: req.authData
});
return res.status(200).send({
secrets: deletedSecrets
});
};

View File

@ -3,9 +3,7 @@ import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import { MembershipOrg, User } from "../../models";
import { completeAccount } from "../../helpers/user";
import {
initializeDefaultOrg,
} from "../../helpers/signup";
import { initializeDefaultOrg } from "../../helpers/signup";
import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth";
import { ACCEPTED, INVITED } from "../../variables";
import { standardRequest } from "../../config/request";
@ -13,6 +11,8 @@ import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../confi
import { BadRequestError } from "../../utils/errors";
import { TelemetryService } from "../../services";
import { AuthMethod } from "../../models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
/**
* Complete setting up user by adding their personal and auth information as part of the
@ -22,177 +22,173 @@ import { AuthMethod } from "../../models";
* @returns
*/
export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token, refreshToken;
try {
const {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
organizationName,
providerAuthToken,
attributionSource,
}: {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
organizationName: string;
providerAuthToken?: string;
attributionSource?: string;
} = req.body;
let user, token;
try {
const {
body: {
email,
publicKey,
salt,
lastName,
verifier,
firstName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
organizationName,
providerAuthToken,
attributionSource,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag
}
} = await validateRequest(reqValidator.CompletedAccountSignupV3, req);
user = await User.findOne({ email });
user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: "Failed to complete account for complete user",
});
}
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: "Failed to complete account for complete user"
});
}
if (providerAuthToken) {
await validateProviderAuthToken({
email,
providerAuthToken
});
} else {
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers["authorization"]?.split(" ", 2) ?? [null, null]
if (AUTH_TOKEN_TYPE === null) {
throw BadRequestError({ message: "Missing Authorization Header in the request header." });
}
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
throw BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` })
}
if (AUTH_TOKEN_VALUE === null) {
throw BadRequestError({
message: "Missing Authorization Body in the request header",
})
}
if (providerAuthToken) {
await validateProviderAuthToken({
email,
providerAuthToken
});
} else {
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>(
req.headers["authorization"]?.split(" ", 2)
) ?? [null, null];
if (AUTH_TOKEN_TYPE === null) {
throw BadRequestError({ message: "Missing Authorization Header in the request header." });
}
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
throw BadRequestError({
message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
});
}
if (AUTH_TOKEN_VALUE === null) {
throw BadRequestError({
message: "Missing Authorization Body in the request header"
});
}
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret())
);
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret())
);
if (decodedToken.userId !== user.id) {
throw BadRequestError();
}
}
if (decodedToken.userId !== user.id) {
throw BadRequestError();
}
}
// complete setting up user's account
user = await completeAccount({
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
});
// complete setting up user's account
user = await completeAccount({
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
if (!user)
throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
if (!user) throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) => [AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod));
if (!hasSamlEnabled) { // TODO: modify this part
// initialize default organization and workspace
await initializeDefaultOrg({
organizationName,
user,
});
}
const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) =>
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod)
);
// update organization membership statuses that are
// invited to completed with user attached
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED,
},
{
user,
status: ACCEPTED,
}
);
if (!hasSamlEnabled) {
// TODO: modify this part
// initialize default organization and workspace
await initializeDefaultOrg({
organizationName,
user
});
}
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
});
// update organization membership statuses that are
// invited to completed with user attached
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED
},
{
user,
status: ACCEPTED
}
);
token = tokens.token;
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// sending a welcome email to new users
if (await getLoopsApiKey()) {
await standardRequest.post("https://app.loops.so/api/v1/events/send", {
"email": email,
"eventName": "Sign Up",
"firstName": firstName,
"lastName": lastName,
}, {
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + (await getLoopsApiKey()),
},
});
}
token = tokens.token;
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled(),
});
// sending a welcome email to new users
if (await getLoopsApiKey()) {
await standardRequest.post(
"https://app.loops.so/api/v1/events/send",
{
email: email,
eventName: "Sign Up",
firstName: firstName,
lastName: lastName
},
{
headers: {
Accept: "application/json",
Authorization: "Bearer " + (await getLoopsApiKey())
}
}
);
}
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "User Signed Up",
distinctId: email,
properties: {
email,
...(attributionSource ? { attributionSource } : {})
},
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to complete account setup",
});
}
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
return res.status(200).send({
message: "Successfully set up account",
user,
token,
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "User Signed Up",
distinctId: email,
properties: {
email,
...(attributionSource ? { attributionSource } : {})
}
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to complete account setup"
});
}
return res.status(200).send({
message: "Successfully set up account",
user,
token
});
};

View File

@ -1,90 +1,103 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { validateRequest } from "../../helpers/validation";
import { Secret } from "../../models";
import { SecretService } from"../../services";
import { SecretService } from "../../services";
import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService";
import { UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/workspace";
/**
* Return whether or not all secrets in workspace with id [workspaceId]
* are blind-indexed
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceBlinkIndexStatusV3, req);
const secretsWithoutBlindIndex = await Secret.countDocuments({
workspace: new Types.ObjectId(workspaceId),
secretBlindIndex: {
$exists: false,
},
});
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
return res.status(200).send(secretsWithoutBlindIndex === 0);
}
const secretsWithoutBlindIndex = await Secret.countDocuments({
workspace: new Types.ObjectId(workspaceId),
secretBlindIndex: {
$exists: false
}
});
return res.status(200).send(secretsWithoutBlindIndex === 0);
};
/**
* Get all secrets for workspace with id [workspaceId]
*/
export const getWorkspaceSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceSecretsV3, req);
const secrets = await Secret.find({
workspace: new Types.ObjectId (workspaceId),
});
return res.status(200).send({
secrets,
});
}
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
const secrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
secrets
});
};
/**
* Update blind indices for secrets in workspace with id [workspaceId]
* @param req
* @param res
* @param req
* @param res
*/
export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
interface SecretToUpdate {
secretName: string;
_id: string;
}
const {
params: { workspaceId },
body: { secretsToUpdate }
} = await validateRequest(reqValidator.NameWorkspaceSecretsV3, req);
const { workspaceId } = req.params;
const {
secretsToUpdate,
}: {
secretsToUpdate: SecretToUpdate[];
} = req.body;
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
// get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId),
});
// get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId)
});
// update secret blind indices
const operations = await Promise.all(
secretsToUpdate.map(async (secretToUpdate: SecretToUpdate) => {
const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName: secretToUpdate.secretName,
salt,
});
return ({
updateOne: {
filter: {
_id: new Types.ObjectId(secretToUpdate._id),
},
update: {
secretBlindIndex,
},
},
});
})
);
// update secret blind indices
const operations = await Promise.all(
secretsToUpdate.map(async (secretToUpdate) => {
const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName: secretToUpdate.secretName,
salt
});
await Secret.bulkWrite(operations);
return {
updateOne: {
filter: {
_id: new Types.ObjectId(secretToUpdate._id)
},
update: {
secretBlindIndex
}
}
};
})
);
return res.status(200).send({
message: "Successfully named workspace secrets",
});
}
await Secret.bulkWrite(operations);
return res.status(200).send({
message: "Successfully named workspace secrets"
});
};

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,32 @@
import { Request, Response } from "express";
import { Action } from "../../models";
import { ActionNotFoundError } from "../../../utils/errors";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/action";
export const getAction = async (req: Request, res: Response) => {
let action;
try {
const { actionId } = req.params;
action = await Action
.findById(actionId)
.populate([
"payload.secretVersions.oldSecretVersion",
"payload.secretVersions.newSecretVersion",
]);
if (!action) throw ActionNotFoundError({
message: "Failed to find action",
});
let action;
try {
const {
params: { actionId }
} = await validateRequest(reqValidator.GetActionV1, req);
} catch (err) {
throw ActionNotFoundError({
message: "Failed to find action",
});
}
return res.status(200).send({
action,
action = await Action.findById(actionId).populate([
"payload.secretVersions.oldSecretVersion",
"payload.secretVersions.newSecretVersion"
]);
if (!action)
throw ActionNotFoundError({
message: "Failed to find action"
});
} catch (err) {
throw ActionNotFoundError({
message: "Failed to find action"
});
}
}
return res.status(200).send({
action
});
};

View File

@ -2,27 +2,31 @@ import { Request, Response } from "express";
import { EELicenseService } from "../../services";
import { getLicenseServerUrl } from "../../../config";
import { licenseServerKeyRequest } from "../../../config/request";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/cloudProducts";
/**
* Return available cloud product information.
* Note: Nicely formatted to easily construct a table from
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getCloudProducts = async (req: Request, res: Response) => {
const billingCycle = req.query["billing-cycle"] as string;
const {
query: { "billing-cycle": billingCycle }
} = await validateRequest(reqValidator.GetCloudProductsV1, req);
if (EELicenseService.instanceType === "cloud") {
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
);
if (EELicenseService.instanceType === "cloud") {
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
);
return res.status(200).send(data);
}
return res.status(200).send({
head: [],
rows: [],
});
}
return res.status(200).send(data);
}
return res.status(200).send({
head: [],
rows: []
});
};

View File

@ -7,15 +7,17 @@ import * as workspaceController from "./workspaceController";
import * as actionController from "./actionController";
import * as membershipController from "./membershipController";
import * as cloudProductsController from "./cloudProductsController";
import * as roleController from "./roleController";
export {
secretController,
secretSnapshotController,
organizationsController,
ssoController,
usersController,
workspaceController,
actionController,
membershipController,
cloudProductsController,
}
secretController,
secretSnapshotController,
organizationsController,
ssoController,
usersController,
workspaceController,
actionController,
membershipController,
cloudProductsController,
roleController
};

View File

@ -3,228 +3,503 @@ import { Request, Response } from "express";
import { getLicenseServerUrl } from "../../../config";
import { licenseServerKeyRequest } from "../../../config/request";
import { EELicenseService } from "../../services";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/organization";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../services/RoleService";
import { ForbiddenError } from "@casl/ability";
import { Organization } from "../../../models";
import { OrganizationNotFoundError } from "../../../utils/errors";
export const getOrganizationPlansTable = async (req: Request, res: Response) => {
const billingCycle = req.query.billingCycle as string;
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
);
const {
query: { billingCycle },
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlansTablev1, req);
return res.status(200).send(data);
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
);
return res.status(200).send(data);
};
/**
* Return the organization current plan's feature set
*/
export const getOrganizationPlan = async (req: Request, res: Response) => {
const { organizationId } = req.params;
const workspaceId = req.query.workspaceId as string;
const {
query: { workspaceId },
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanv1, req);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId), new Types.ObjectId(workspaceId));
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
return res.status(200).send({
plan,
});
}
const plan = await EELicenseService.getPlan(
new Types.ObjectId(organizationId),
new Types.ObjectId(workspaceId)
);
return res.status(200).send({
plan
});
};
/**
* Return checkout url for pro trial
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const startOrganizationTrial = async (req: Request, res: Response) => {
const { organizationId } = req.params;
const { success_url } = req.body;
const {
params: { organizationId },
body: { success_url }
} = await validateRequest(reqValidator.StartOrgTrailv1, req);
const { data: { url } } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/session/trial`,
{
success_url
}
);
EELicenseService.delPlan(new Types.ObjectId(organizationId));
return res.status(200).send({
url
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Billing
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
}
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/session/trial`,
{
success_url
}
);
EELicenseService.delPlan(new Types.ObjectId(organizationId));
return res.status(200).send({
url
});
};
/**
* Return the organization's current plan's billing info
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => {
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/billing`
);
return res.status(200).send(data);
}
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/cloud-plan/billing`
);
return res.status(200).send(data);
};
/**
* Return the organization's current plan's feature table
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getOrganizationPlanTable = async (req: Request, res: Response) => {
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/table`
);
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanTablev1, req);
return res.status(200).send(data);
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/cloud-plan/table`
);
return res.status(200).send(data);
};
export const getOrganizationBillingDetails = async (req: Request, res: Response) => {
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details`
);
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgBillingDetailsv1, req);
return res.status(200).send(data);
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details`
);
return res.status(200).send(data);
};
export const updateOrganizationBillingDetails = async (req: Request, res: Response) => {
const {
name,
email
} = req.body;
const {
params: { organizationId },
body: { name, email }
} = await validateRequest(reqValidator.UpdateOrgBillingDetailsv1, req);
const { data } = await licenseServerKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details`,
{
...(name ? { name } : {}),
...(email ? { email } : {})
}
);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Billing
);
return res.status(200).send(data);
}
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details`,
{
...(name ? { name } : {}),
...(email ? { email } : {})
}
);
return res.status(200).send(data);
};
/**
* Return the organization's payment methods on file
*/
export const getOrganizationPmtMethods = async (req: Request, res: Response) => {
const { data: { pmtMethods } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`
);
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPmtMethodsv1, req);
return res.status(200).send(pmtMethods);
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { pmtMethods }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`
);
return res.status(200).send(pmtMethods);
};
/**
* Return URL to add payment method for organization
*/
export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
const {
success_url,
cancel_url,
} = req.body;
const { data: { url } } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`,
{
success_url,
cancel_url,
}
);
return res.status(200).send({
url,
});
}
const {
params: { organizationId },
body: { success_url, cancel_url }
} = await validateRequest(reqValidator.CreateOrgPmtMethodv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`,
{
success_url,
cancel_url
}
);
return res.status(200).send({
url
});
};
/**
* Delete payment method with id [pmtMethodId] for organization
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => {
const { pmtMethodId } = req.params;
const {
params: { organizationId, pmtMethodId }
} = await validateRequest(reqValidator.DelOrgPmtMethodv1, req);
const { data } = await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods/${pmtMethodId}`,
);
return res.status(200).send(data);
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods/${pmtMethodId}`
);
return res.status(200).send(data);
};
/**
* Return the organization's tax ids on file
*/
export const getOrganizationTaxIds = async (req: Request, res: Response) => {
const { data: { tax_ids } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids`
);
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgTaxIdsv1, req);
return res.status(200).send(tax_ids);
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { tax_ids }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/tax-ids`
);
return res.status(200).send(tax_ids);
};
/**
* Add tax id to organization
*/
export const addOrganizationTaxId = async (req: Request, res: Response) => {
const {
type,
value
} = req.body;
const {
params: { organizationId },
body: { type, value }
} = await validateRequest(reqValidator.CreateOrgTaxId, req);
const { data } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids`,
{
type,
value
}
);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Billing
);
return res.status(200).send(data);
}
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/tax-ids`,
{
type,
value
}
);
return res.status(200).send(data);
};
/**
* Delete tax id with id [taxId] from organization tax ids on file
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
const { taxId } = req.params;
const {
params: { organizationId, taxId }
} = await validateRequest(reqValidator.DelOrgTaxIdv1, req);
const { data } = await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids/${taxId}`,
);
return res.status(200).send(data);
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/tax-ids/${taxId}`
);
return res.status(200).send(data);
};
/**
* Return organization's invoices on file
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getOrganizationInvoices = async (req: Request, res: Response) => {
const { data: { invoices } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices`
);
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgInvoicesv1, req);
return res.status(200).send(invoices);
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { invoices }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/invoices`
);
return res.status(200).send(invoices);
};
/**
* Return organization's licenses on file
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getOrganizationLicenses = async (req: Request, res: Response) => {
const { data: { licenses } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/licenses`
);
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgLicencesv1, req);
return res.status(200).send(licenses);
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { licenses }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/licenses`
);
return res.status(200).send(licenses);
};

View File

@ -0,0 +1,235 @@
import { Request, Response } from "express";
import {
CreateRoleSchema,
DeleteRoleSchema,
GetRoleSchema,
GetUserPermission,
GetUserProjectPermission,
UpdateRoleSchema
} from "../../validation/role";
import {
ProjectPermissionActions,
ProjectPermissionSub,
adminProjectPermissions,
getUserProjectPermissions,
memberProjectPermissions,
viewerProjectPermission
} from "../../services/ProjectRoleService";
import {
OrgPermissionActions,
OrgPermissionSubjects,
adminPermissions,
getUserOrgPermissions,
memberPermissions
} from "../../services/RoleService";
import { BadRequestError } from "../../../utils/errors";
import Role from "../../models/role";
import { validateRequest } from "../../../helpers/validation";
import { packRules } from "@casl/ability/extra";
export const createRole = async (req: Request, res: Response) => {
const {
body: { workspaceId, name, description, slug, permissions, orgId }
} = await validateRequest(CreateRoleSchema, req);
const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule
if (isOrgRole) {
const { permission } = await getUserOrgPermissions(req.user.id, orgId);
if (permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "user doesn't have the permission." });
}
} else {
const { permission } = await getUserProjectPermissions(req.user.id, workspaceId);
if (permission.cannot(ProjectPermissionActions.Create, ProjectPermissionSub.Role)) {
throw BadRequestError({ message: "User doesn't have the permission." });
}
}
const existingRole = await Role.findOne({ organization: orgId, workspace: workspaceId, slug });
if (existingRole) {
throw BadRequestError({ message: "Role already exist" });
}
const role = new Role({
organization: orgId,
workspace: workspaceId,
isOrgRole,
name,
slug,
permissions,
description
});
await role.save();
res.status(200).json({
message: "Successfully created role",
data: {
role
}
});
};
export const updateRole = async (req: Request, res: Response) => {
const {
params: { id },
body: { name, description, slug, permissions, workspaceId, orgId }
} = await validateRequest(UpdateRoleSchema, req);
const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule
if (isOrgRole) {
const { permission } = await getUserOrgPermissions(req.user.id, orgId);
if (permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "User doesn't have the org permission." });
}
} else {
const { permission } = await getUserProjectPermissions(req.user.id, workspaceId);
if (permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Role)) {
throw BadRequestError({ message: "User doesn't have the workspace permission." });
}
}
if (slug) {
const existingRole = await Role.findOne({
organization: orgId,
slug,
isOrgRole,
workspace: workspaceId
});
if (existingRole && existingRole.id !== id) {
throw BadRequestError({ message: "Role already exist" });
}
}
const role = await Role.findByIdAndUpdate(
id,
{ name, description, slug, permissions },
{ returnDocument: "after" }
);
if (!role) {
throw BadRequestError({ message: "Role not found" });
}
res.status(200).json({
message: "Successfully updated role",
data: {
role
}
});
};
export const deleteRole = async (req: Request, res: Response) => {
const {
params: { id }
} = await validateRequest(DeleteRoleSchema, req);
const role = await Role.findById(id);
if (!role) {
throw BadRequestError({ message: "Role not found" });
}
const isOrgRole = !role.workspace;
if (isOrgRole) {
const { permission } = await getUserOrgPermissions(req.user.id, role.organization.toString());
if (permission.cannot(OrgPermissionActions.Delete, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "User doesn't have the org permission." });
}
} else {
const { permission } = await getUserProjectPermissions(req.user.id, role.workspace.toString());
if (permission.cannot(ProjectPermissionActions.Delete, ProjectPermissionSub.Role)) {
throw BadRequestError({ message: "User doesn't have the workspace permission." });
}
}
await Role.findByIdAndDelete(role.id);
res.status(200).json({
message: "Successfully deleted role",
data: {
role
}
});
};
export const getRoles = async (req: Request, res: Response) => {
const {
query: { workspaceId, orgId }
} = await validateRequest(GetRoleSchema, req);
const isOrgRole = !workspaceId;
if (isOrgRole) {
const { permission } = await getUserOrgPermissions(req.user.id, orgId);
if (permission.cannot(OrgPermissionActions.Read, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "User doesn't have the org permission." });
}
} else {
const { permission } = await getUserProjectPermissions(req.user.id, workspaceId);
if (permission.cannot(ProjectPermissionActions.Read, ProjectPermissionSub.Role)) {
throw BadRequestError({ message: "User doesn't have the workspace permission." });
}
}
const customRoles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId });
// as this is shared between org and workspace switch the rule set based on it
const roles = [
{
_id: "admin",
name: "Admin",
slug: "admin",
description: "Complete administration access over the organization",
permissions: isOrgRole ? adminPermissions.rules : adminProjectPermissions.rules
},
{
_id: "member",
name: isOrgRole ? "Member" : "Developer",
slug: "member",
description: "Non-administrative role in an organization",
permissions: isOrgRole ? memberPermissions.rules : memberProjectPermissions.rules
},
// viewer role only for project level
...(isOrgRole
? []
: [
{
_id: "viewer",
name: "Viewer",
slug: "viewer",
description: "Non-administrative role in an organization",
permissions: viewerProjectPermission.rules
}
]),
...customRoles
];
res.status(200).json({
message: "Successfully fetched role list",
data: {
roles
}
});
};
export const getUserPermissions = async (req: Request, res: Response) => {
const {
params: { orgId }
} = await validateRequest(GetUserPermission, req);
const { permission } = await getUserOrgPermissions(req.user._id, orgId);
res.status(200).json({
data: {
permissions: packRules(permission.rules)
}
});
};
export const getUserWorkspacePermissions = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(GetUserProjectPermission, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
res.status(200).json({
data: {
permissions: packRules(permission.rules)
}
});
};

View File

@ -1,7 +1,17 @@
import { ForbiddenError, subject } from "@casl/ability";
import { Request, Response } from "express";
import { Secret } from "../../../models";
import { validateRequest } from "../../../helpers/validation";
import { Folder, Secret } from "../../../models";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../services/ProjectRoleService";
import { BadRequestError } from "../../../utils/errors";
import * as reqValidator from "../../../validation";
import { SecretVersion } from "../../models";
import { EESecretService } from "../../services";
import { getFolderWithPathFromId } from "../../../services/FolderService";
/**
* Return secret versions for secret with id [secretId]
@ -54,10 +64,21 @@ export const getSecretVersions = async (req: Request, res: Response) => {
}
}
*/
const { secretId } = req.params;
const {
params: { secretId },
query: { offset, limit }
} = await validateRequest(reqValidator.GetSecretVersionsV1, req);
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
const secret = await Secret.findById(secretId);
if (!secret) {
throw BadRequestError({ message: "Failed to find secret" });
}
const { permission } = await getUserProjectPermissions(req.user._id, secret.workspace.toString());
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback
);
const secretVersions = await SecretVersion.find({
secret: secretId
@ -126,8 +147,24 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
}
}
*/
const { secretId } = req.params;
const { version } = req.body;
const {
params: { secretId },
body: { version }
} = await validateRequest(reqValidator.RollbackSecretVersionV1, req);
const toBeUpdatedSec = await Secret.findById(secretId);
if (!toBeUpdatedSec) {
throw BadRequestError({ message: "Failed to find secret" });
}
const { permission } = await getUserProjectPermissions(
req.user._id,
toBeUpdatedSec.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRollback
);
// validate secret version
const oldSecretVersion = await SecretVersion.findOne({
@ -154,6 +191,15 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
keyEncoding
} = oldSecretVersion;
let secretPath = "/";
const folders = await Folder.findOne({ workspace, environment });
if (folders)
secretPath = getFolderWithPathFromId(folders.nodes, folder || "root")?.folderPath || "/";
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment, secretPath })
);
// update secret
const secret = await Secret.findByIdAndUpdate(
secretId,

View File

@ -1,9 +1,13 @@
import { ForbiddenError } from "@casl/ability";
import { Request, Response } from "express";
import { validateRequest } from "../../../helpers/validation";
import {
ISecretVersion,
SecretSnapshot,
TFolderRootVersionSchema,
} from "../../models";
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../services/ProjectRoleService";
import * as reqValidator from "../../../validation/secretSnapshot";
import { ISecretVersion, SecretSnapshot, TFolderRootVersionSchema } from "../../models";
/**
* Return secret snapshot with id [secretSnapshotId]
@ -12,7 +16,9 @@ import {
* @returns
*/
export const getSecretSnapshot = async (req: Request, res: Response) => {
const { secretSnapshotId } = req.params;
const {
params: { secretSnapshotId }
} = await validateRequest(reqValidator.GetSecretSnapshotV1, req);
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
.lean()
@ -20,26 +26,36 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
path: "secretVersions",
populate: {
path: "tags",
model: "Tag",
},
model: "Tag"
}
})
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
const { permission } = await getUserProjectPermissions(
req.user._id,
secretSnapshot.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback
);
const folderId = secretSnapshot.folderId;
// to show only the folder required secrets
secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter(
({ folder }) => folder === folderId
);
secretSnapshot.folderVersion =
secretSnapshot?.folderVersion?.nodes?.children?.map(({ id, name }) => ({
secretSnapshot.folderVersion = secretSnapshot?.folderVersion?.nodes?.children?.map(
({ id, name }) => ({
id,
name,
})) as any;
name
})
) as any;
return res.status(200).send({
secretSnapshot,
secretSnapshot
});
};

View File

@ -2,239 +2,258 @@ import { Request, Response } from "express";
import { Types } from "mongoose";
import { BotOrgService } from "../../../services";
import { SSOConfig } from "../../models";
import {
AuthMethod,
MembershipOrg,
User
} from "../../../models";
import { AuthMethod, MembershipOrg, User } from "../../../models";
import { getSSOConfigHelper } from "../../helpers/organizations";
import { client } from "../../../config";
import { ResourceNotFoundError } from "../../../utils/errors";
import { getSiteURL } from "../../../config";
import { EELicenseService } from "../../services";
import * as reqValidator from "../../../validation/sso";
import { validateRequest } from "../../../helpers/validation";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../services/RoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Redirect user to appropriate SSO endpoint after successful authentication
* to finish inputting their master key for logging in or signing up
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const redirectSSO = async (req: Request, res: Response) => {
if (req.isUserCompleted) {
return res.redirect(`${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`);
}
return res.redirect(`${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`);
}
if (req.isUserCompleted) {
return res.redirect(
`${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`
);
}
return res.redirect(
`${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`
);
};
/**
* Return organization SAML SSO configuration
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getSSOConfig = async (req: Request, res: Response) => {
const organizationId = req.query.organizationId as string;
const data = await getSSOConfigHelper({
organizationId: new Types.ObjectId(organizationId)
});
const {
query: { organizationId }
} = await validateRequest(reqValidator.GetSsoConfigv1, req);
return res.status(200).send(data);
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Sso
);
const data = await getSSOConfigHelper({
organizationId: new Types.ObjectId(organizationId)
});
return res.status(200).send(data);
};
/**
* Update organization SAML SSO configuration
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const updateSSOConfig = async (req: Request, res: Response) => {
const {
body: { organizationId, authProvider, isActive, entryPoint, issuer, cert }
} = await validateRequest(reqValidator.UpdateSsoConfigv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Sso
);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (!plan.samlSSO)
return res.status(400).send({
message:
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
});
interface PatchUpdate {
authProvider?: string;
isActive?: boolean;
encryptedEntryPoint?: string;
entryPointIV?: string;
entryPointTag?: string;
encryptedIssuer?: string;
issuerIV?: string;
issuerTag?: string;
encryptedCert?: string;
certIV?: string;
certTag?: string;
}
const update: PatchUpdate = {};
if (authProvider) {
update.authProvider = authProvider;
}
if (isActive !== undefined) {
update.isActive = isActive;
}
const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
if (entryPoint) {
const {
organizationId,
authProvider,
isActive,
entryPoint,
issuer,
cert,
} = req.body;
ciphertext: encryptedEntryPoint,
iv: entryPointIV,
tag: entryPointTag
} = client.encryptSymmetric(entryPoint, key);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (!plan.samlSSO) return res.status(400).send({
message: "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
update.encryptedEntryPoint = encryptedEntryPoint;
update.entryPointIV = entryPointIV;
update.entryPointTag = entryPointTag;
}
if (issuer) {
const {
ciphertext: encryptedIssuer,
iv: issuerIV,
tag: issuerTag
} = client.encryptSymmetric(issuer, key);
update.encryptedIssuer = encryptedIssuer;
update.issuerIV = issuerIV;
update.issuerTag = issuerTag;
}
if (cert) {
const {
ciphertext: encryptedCert,
iv: certIV,
tag: certTag
} = client.encryptSymmetric(cert, key);
update.encryptedCert = encryptedCert;
update.certIV = certIV;
update.certTag = certTag;
}
const ssoConfig = await SSOConfig.findOneAndUpdate(
{
organization: new Types.ObjectId(organizationId)
},
update,
{
new: true
}
);
if (!ssoConfig)
throw ResourceNotFoundError({
message: "Failed to find SSO config to update"
});
interface PatchUpdate {
authProvider?: string;
isActive?: boolean;
encryptedEntryPoint?: string;
entryPointIV?: string;
entryPointTag?: string;
encryptedIssuer?: string;
issuerIV?: string;
issuerTag?: string;
encryptedCert?: string;
certIV?: string;
certTag?: string;
}
const update: PatchUpdate = {};
if (authProvider) {
update.authProvider = authProvider;
}
if (isActive !== undefined) {
update.isActive = isActive;
}
const key = await BotOrgService.getSymmetricKey(
new Types.ObjectId(organizationId)
);
if (entryPoint) {
const {
ciphertext: encryptedEntryPoint,
iv: entryPointIV,
tag: entryPointTag
} = client.encryptSymmetric(entryPoint, key);
update.encryptedEntryPoint = encryptedEntryPoint;
update.entryPointIV = entryPointIV;
update.entryPointTag = entryPointTag;
}
if (issuer) {
const {
ciphertext: encryptedIssuer,
iv: issuerIV,
tag: issuerTag
} = client.encryptSymmetric(issuer, key);
update.encryptedIssuer = encryptedIssuer;
update.issuerIV = issuerIV;
update.issuerTag = issuerTag;
}
if (update.isActive !== undefined) {
const membershipOrgs = await MembershipOrg.find({
organization: new Types.ObjectId(organizationId)
}).select("user");
if (cert) {
const {
ciphertext: encryptedCert,
iv: certIV,
tag: certTag
} = client.encryptSymmetric(cert, key);
update.encryptedCert = encryptedCert;
update.certIV = certIV;
update.certTag = certTag;
}
const ssoConfig = await SSOConfig.findOneAndUpdate(
if (update.isActive) {
await User.updateMany(
{
organization: new Types.ObjectId(organizationId)
_id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
}
},
update,
{
new: true
authMethods: [ssoConfig.authProvider]
}
);
if (!ssoConfig) throw ResourceNotFoundError({
message: "Failed to find SSO config to update"
});
if (update.isActive !== undefined) {
const membershipOrgs = await MembershipOrg.find({
organization: new Types.ObjectId(organizationId)
}).select("user");
if (update.isActive) {
await User.updateMany(
{
_id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
}
},
{
authMethods: [ssoConfig.authProvider],
}
);
} else {
await User.updateMany(
{
_id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
}
},
{
authMethods: [AuthMethod.EMAIL],
}
);
);
} else {
await User.updateMany(
{
_id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
}
},
{
authMethods: [AuthMethod.EMAIL]
}
);
}
return res.status(200).send(ssoConfig);
}
}
return res.status(200).send(ssoConfig);
};
/**
* Create organization SAML SSO configuration
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const createSSOConfig = async (req: Request, res: Response) => {
const {
organizationId,
authProvider,
isActive,
entryPoint,
issuer,
cert
} = req.body;
const {
body: { organizationId, authProvider, isActive, entryPoint, issuer, cert }
} = await validateRequest(reqValidator.CreateSsoConfigv1, req);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (!plan.samlSSO) return res.status(400).send({
message: "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration."
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Sso
);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (!plan.samlSSO)
return res.status(400).send({
message:
"Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration."
});
const key = await BotOrgService.getSymmetricKey(
new Types.ObjectId(organizationId)
);
const {
ciphertext: encryptedEntryPoint,
iv: entryPointIV,
tag: entryPointTag
} = client.encryptSymmetric(entryPoint, key);
const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
const {
ciphertext: encryptedIssuer,
iv: issuerIV,
tag: issuerTag
} = client.encryptSymmetric(issuer, key);
const {
ciphertext: encryptedEntryPoint,
iv: entryPointIV,
tag: entryPointTag
} = client.encryptSymmetric(entryPoint, key);
const {
ciphertext: encryptedCert,
iv: certIV,
tag: certTag
} = client.encryptSymmetric(cert, key);
const ssoConfig = await new SSOConfig({
organization: new Types.ObjectId(organizationId),
authProvider,
isActive,
encryptedEntryPoint,
entryPointIV,
entryPointTag,
encryptedIssuer,
issuerIV,
issuerTag,
encryptedCert,
certIV,
certTag
}).save();
const {
ciphertext: encryptedIssuer,
iv: issuerIV,
tag: issuerTag
} = client.encryptSymmetric(issuer, key);
return res.status(200).send(ssoConfig);
}
const {
ciphertext: encryptedCert,
iv: certIV,
tag: certTag
} = client.encryptSymmetric(cert, key);
const ssoConfig = await new SSOConfig({
organization: new Types.ObjectId(organizationId),
authProvider,
isActive,
encryptedEntryPoint,
entryPointIV,
entryPointTag,
encryptedIssuer,
issuerIV,
issuerTag,
encryptedCert,
certIV,
certTag
}).save();
return res.status(200).send(ssoConfig);
};

View File

@ -1,6 +1,14 @@
import { Request, Response } from "express";
import { PipelineStage, Types } from "mongoose";
import { Membership, Secret, ServiceTokenData, User } from "../../../models";
import {
Folder,
Membership,
Secret,
ServiceTokenData,
TFolderSchema,
User,
Workspace
} from "../../../models";
import {
ActorType,
AuditLog,
@ -18,20 +26,37 @@ import {
} from "../../models";
import { EESecretService } from "../../services";
import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
import Folder, { TFolderSchema } from "../../../models/folder";
import { searchByFolderId } from "../../../services/FolderService";
// import Folder, { TFolderSchema } from "../../../models/folder";
import { getFolderByPath, searchByFolderId } from "../../../services/FolderService";
import { EEAuditLogService, EELicenseService } from "../../services";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
import { validateRequest } from "../../../helpers/validation";
import {
AddWorkspaceTrustedIpV1,
DeleteWorkspaceTrustedIpV1,
GetWorkspaceAuditLogActorFilterOptsV1,
GetWorkspaceAuditLogsV1,
GetWorkspaceLogsV1,
GetWorkspaceSecretSnapshotsCountV1,
GetWorkspaceSecretSnapshotsV1,
GetWorkspaceTrustedIpsV1,
RollbackWorkspaceSecretSnapshotV1,
UpdateWorkspaceTrustedIpV1
} from "../../../validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError } from "../../../utils/errors";
/**
* Return secret snapshots for workspace with id [workspaceId]
* @param req
* @param res
*/
export const getWorkspaceSecretSnapshots = async (
req: Request,
res: Response
) => {
export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return project secret snapshot ids'
#swagger.description = 'Return project secret snapshots ids'
@ -77,23 +102,38 @@ export const getWorkspaceSecretSnapshots = async (
}
}
*/
const { workspaceId } = req.params;
const { environment, folderId } = req.query;
const {
params: { workspaceId },
query: { environment, directory, offset, limit }
} = await validateRequest(GetWorkspaceSecretSnapshotsV1, req);
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback
);
let folderId = "root";
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
if (folders) {
const folder = getFolderByPath(folders?.nodes, directory);
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
folderId = folder.id;
}
const secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId,
environment,
folderId: folderId || "root",
folderId
})
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
return res.status(200).send({
secretSnapshots,
secretSnapshots
});
};
@ -102,21 +142,36 @@ export const getWorkspaceSecretSnapshots = async (
* @param req
* @param res
*/
export const getWorkspaceSecretSnapshotsCount = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const { environment, folderId } = req.query;
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
const {
params: { workspaceId },
query: { environment, directory }
} = await validateRequest(GetWorkspaceSecretSnapshotsCountV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback
);
let folderId = "root";
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
if (folders) {
const folder = getFolderByPath(folders?.nodes, directory);
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
folderId = folder.id;
}
const count = await SecretSnapshot.countDocuments({
workspace: workspaceId,
environment,
folderId: folderId || "root",
folderId
});
return res.status(200).send({
count,
count
});
};
@ -126,10 +181,7 @@ export const getWorkspaceSecretSnapshotsCount = async (
* @param res
* @returns
*/
export const rollbackWorkspaceSecretSnapshot = async (
req: Request,
res: Response
) => {
export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Roll back project secrets to those captured in a secret snapshot version.'
#swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.'
@ -181,19 +233,37 @@ export const rollbackWorkspaceSecretSnapshot = async (
}
*/
const { workspaceId } = req.params;
const { version, environment, folderId = "root" } = req.body;
const {
params: { workspaceId },
body: { directory, environment, version }
} = await validateRequest(RollbackWorkspaceSecretSnapshotV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRollback
);
let folderId = "root";
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
if (folders) {
const folder = getFolderByPath(folders?.nodes, directory);
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
folderId = folder.id;
}
// validate secret snapshot
const secretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId,
version,
environment,
folderId: folderId,
folderId: folderId
})
.populate<{ secretVersions: ISecretVersion[] }>({
path: "secretVersions",
select: "+secretBlindIndex",
select: "+secretBlindIndex"
})
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
@ -202,13 +272,13 @@ export const rollbackWorkspaceSecretSnapshot = async (
const snapshotFolderTree = secretSnapshot.folderVersion;
const latestFolderTree = await Folder.findOne({
workspace: workspaceId,
environment,
environment
});
const latestFolderVersion = await FolderVersion.findOne({
environment,
workspace: workspaceId,
"nodes.id": folderId,
"nodes.id": folderId
}).sort({ "nodes.version": -1 });
const oldSecretVersionsObj: Record<string, ISecretVersion> = {};
@ -222,8 +292,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
// the parent node from current latest one
// this will be modified according to the snapshot and latest snapshots
const newFolderTree =
latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId);
const newFolderTree = latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId);
if (newFolderTree) {
newFolderTree.children = snapshotFolderTree?.nodes?.children || [];
@ -252,43 +321,43 @@ export const rollbackWorkspaceSecretSnapshot = async (
workspace: new Types.ObjectId(workspaceId),
environment,
folderId: {
$in: Object.keys(groupByFolderId),
},
},
$in: Object.keys(groupByFolderId)
}
}
};
const sortByFolderIdAndVersion: PipelineStage = {
$sort: { folderId: 1, version: -1 },
$sort: { folderId: 1, version: -1 }
};
const pickLatestVersionOfEachFolder = {
$group: {
_id: "$folderId",
latestVersion: { $first: "$version" },
doc: {
$first: "$$ROOT",
},
},
$first: "$$ROOT"
}
}
};
const populateSecVersion = {
$lookup: {
from: SecretVersion.collection.name,
localField: "doc.secretVersions",
foreignField: "_id",
as: "doc.secretVersions",
},
as: "doc.secretVersions"
}
};
const populateFolderVersion = {
$lookup: {
from: FolderVersion.collection.name,
localField: "doc.folderVersion",
foreignField: "_id",
as: "doc.folderVersion",
},
as: "doc.folderVersion"
}
};
const unwindFolderVerField = {
$unwind: {
path: "$doc.folderVersion",
preserveNullAndEmptyArrays: true,
},
preserveNullAndEmptyArrays: true
}
};
const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> =
await SecretSnapshot.aggregate([
@ -297,7 +366,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
pickLatestVersionOfEachFolder,
populateSecVersion,
populateFolderVersion,
unwindFolderVerField,
unwindFolderVerField
]);
// recursive snapshotting each level
@ -327,7 +396,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
// TODO: fix any
const latestSecretVersionIds = await getLatestSecretVersionIds({
secretIds,
secretIds
});
// TODO: fix any
@ -335,32 +404,31 @@ export const rollbackWorkspaceSecretSnapshot = async (
await SecretVersion.find(
{
_id: {
$in: latestSecretVersionIds.map((s) => s.versionId),
},
$in: latestSecretVersionIds.map((s) => s.versionId)
}
},
"secret version"
)
).reduce(
(accumulator, s) => ({
...accumulator,
[`${s.secret.toString()}`]: s,
[`${s.secret.toString()}`]: s
}),
{}
);
const secDelQuery: Record<string, unknown> = {
workspace: workspaceId,
environment,
environment
// undefined means root thus collect all secrets
};
if (folderId !== "root" && folderIds.length)
secDelQuery.folder = { $in: folderIds };
if (folderId !== "root" && folderIds.length) secDelQuery.folder = { $in: folderIds };
// delete existing secrets
await Secret.deleteMany(secDelQuery);
await Folder.deleteOne({
workspace: workspaceId,
environment,
environment
});
// add secrets
@ -382,7 +450,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
createdAt,
algorithm,
keyEncoding,
folder: secFolderId,
folder: secFolderId
} = oldSecretVersionsObj[sv];
return {
@ -405,7 +473,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
createdAt,
algorithm,
keyEncoding,
folder: secFolderId,
folder: secFolderId
};
})
);
@ -429,7 +497,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
secretValueTag,
algorithm,
keyEncoding,
folder: secFolderId,
folder: secFolderId
}) => ({
_id: new Types.ObjectId(),
secret: _id,
@ -448,7 +516,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
secretValueTag,
algorithm,
keyEncoding,
folder: secFolderId,
folder: secFolderId
})
)
);
@ -464,7 +532,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
const newFolderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: newFolderTree,
nodes: newFolderTree
});
await newFolderVersion.save();
}
@ -473,13 +541,11 @@ export const rollbackWorkspaceSecretSnapshot = async (
await SecretVersion.updateMany(
{
secret: {
$in: Object.keys(oldSecretVersionsObj).map(
(sv) => oldSecretVersionsObj[sv].secret
),
},
$in: Object.keys(oldSecretVersionsObj).map((sv) => oldSecretVersionsObj[sv].secret)
}
},
{
isDeleted: false,
isDeleted: false
}
);
@ -487,11 +553,11 @@ export const rollbackWorkspaceSecretSnapshot = async (
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId,
folderId
});
return res.status(200).send({
secrets,
secrets
});
};
@ -568,13 +634,16 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
}
}
*/
const { workspaceId } = req.params;
const {
query: { limit, offset, userId, sortBy, actionNames },
params: { workspaceId }
} = await validateRequest(GetWorkspaceLogsV1, req);
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
const sortBy: string = req.query.sortBy as string;
const userId: string = req.query.userId as string;
const actionNames: string = req.query.actionNames as string;
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.AuditLogs
);
const logs = await Log.find({
workspace: workspaceId,
@ -582,10 +651,10 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
...(actionNames
? {
actionNames: {
$in: actionNames.split(","),
},
$in: actionNames.split(",")
}
}
: {}),
: {})
})
.sort({ createdAt: sortBy === "recent" ? -1 : 1 })
.skip(offset)
@ -594,93 +663,109 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
.populate("user serviceAccount serviceTokenData");
return res.status(200).send({
logs,
logs
});
};
/**
* Return audit logs for workspace with id [workspaceId]
* @param req
* @param res
* @param res
*/
export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const eventType = req.query.eventType;
const userAgentType = req.query.userAgentType;
const actor = req.query.actor as string | undefined;
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
const startDate = req.query.startDate as string;
const endDate = req.query.endDate as string;
const {
query: { limit, offset, endDate, eventType, startDate, userAgentType, actor },
params: { workspaceId }
} = await validateRequest(GetWorkspaceAuditLogsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.AuditLogs
);
const query = {
workspace: new Types.ObjectId(workspaceId),
...(eventType ? {
"event.type": eventType
} : {}),
...(userAgentType ? {
userAgentType
} : {}),
...(actor ? {
"actor.type": actor.split("-", 2)[0],
...(actor.split("-", 2)[0] === ActorType.USER ? {
"actor.metadata.userId": actor.split("-", 2)[1]
} : {
"actor.metadata.serviceId": actor.split("-", 2)[1]
})
} : {}),
...(startDate || endDate ? {
createdAt: {
...(startDate && { $gte: new Date(startDate) }),
...(endDate && { $lte: new Date(endDate) })
}
} : {})
}
const auditLogs = await AuditLog.find(query)
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
...(eventType
? {
"event.type": eventType
}
: {}),
...(userAgentType
? {
userAgentType
}
: {}),
...(actor
? {
"actor.type": actor.split("-", 2)[0],
...(actor.split("-", 2)[0] === ActorType.USER
? {
"actor.metadata.userId": actor.split("-", 2)[1]
}
: {
"actor.metadata.serviceId": actor.split("-", 2)[1]
})
}
: {}),
...(startDate || endDate
? {
createdAt: {
...(startDate && { $gte: new Date(startDate) }),
...(endDate && { $lte: new Date(endDate) })
}
}
: {})
};
const auditLogs = await AuditLog.find(query).sort({ createdAt: -1 }).skip(offset).limit(limit);
const totalCount = await AuditLog.countDocuments(query);
return res.status(200).send({
auditLogs,
totalCount
});
}
};
/**
* Return audit log actor filter options for workspace with id [workspaceId]
* @param req
* @param res
* @param res
*/
export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(GetWorkspaceAuditLogActorFilterOptsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.AuditLogs
);
const userIds = await Membership.distinct("user", {
workspace: new Types.ObjectId(workspaceId)
});
const userActors: UserActor[] = (await User.find({
_id: {
$in: userIds
}
})
.select("email"))
.map((user) => ({
const userActors: UserActor[] = (
await User.find({
_id: {
$in: userIds
}
}).select("email")
).map((user) => ({
type: ActorType.USER,
metadata: {
userId: user._id.toString(),
email: user.email
}
}));
const serviceActors: ServiceActor[] = (await ServiceTokenData.find({
workspace: new Types.ObjectId(workspaceId)
})
.select("name"))
.map((serviceTokenData) => ({
const serviceActors: ServiceActor[] = (
await ServiceTokenData.find({
workspace: new Types.ObjectId(workspaceId)
}).select("name")
).map((serviceTokenData) => ({
type: ActorType.SERVICE,
metadata: {
serviceId: serviceTokenData._id.toString(),
@ -691,50 +776,68 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res
return res.status(200).send({
actors: [...userActors, ...serviceActors]
});
}
};
/**
* Return trusted ips for workspace with id [workspaceId]
* @param req
* @param res
* @param res
*/
export const getWorkspaceTrustedIps = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(GetWorkspaceTrustedIpsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.IpAllowList
);
const trustedIps = await TrustedIP.find({
workspace: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
trustedIps
});
}
};
/**
* Add a trusted ip to workspace with id [workspaceId]
* @param req
* @param res
* @param req
* @param res
*/
export const addWorkspaceTrustedIp = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
ipAddress: ip,
comment,
isActive
} = req.body;
const plan = await EELicenseService.getPlan(req.workspace.organization);
if (!plan.ipAllowlisting) return res.status(400).send({
message: "Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range."
});
params: { workspaceId },
body: { comment, isActive, ipAddress: ip }
} = await validateRequest(AddWorkspaceTrustedIpV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.IpAllowList
);
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
const plan = await EELicenseService.getPlan(workspace.organization);
if (!plan.ipAllowlisting)
return res.status(400).send({
message:
"Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range."
});
const isValidIPOrCidr = isValidIpOrCidr(ip);
if (!isValidIPOrCidr) return res.status(400).send({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
if (!isValidIPOrCidr)
return res.status(400).send({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
const { ipAddress, type, prefix } = extractIPDetails(ip);
const trustedIp = await new TrustedIP({
@ -743,9 +846,9 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => {
type,
prefix,
isActive,
comment,
comment
}).save();
await EEAuditLogService.createAuditLog(
req.authData,
{
@ -764,32 +867,43 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => {
return res.status(200).send({
trustedIp
});
}
};
/**
* Update trusted ip with id [trustedIpId] workspace with id [workspaceId]
* @param req
* @param res
* @param req
* @param res
*/
export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => {
const { workspaceId, trustedIpId } = req.params;
const {
ipAddress: ip,
comment
} = req.body;
params: { workspaceId, trustedIpId },
body: { ipAddress: ip, comment }
} = await validateRequest(UpdateWorkspaceTrustedIpV1, req);
const plan = await EELicenseService.getPlan(req.workspace.organization);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.IpAllowList
);
if (!plan.ipAllowlisting) return res.status(400).send({
message: "Failed to update IP access range due to plan restriction. Upgrade plan to update IP access range."
});
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
const plan = await EELicenseService.getPlan(workspace.organization);
if (!plan.ipAllowlisting)
return res.status(400).send({
message:
"Failed to update IP access range due to plan restriction. Upgrade plan to update IP access range."
});
const isValidIPOrCidr = isValidIpOrCidr(ip);
if (!isValidIPOrCidr) return res.status(400).send({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
if (!isValidIPOrCidr)
return res.status(400).send({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
const { ipAddress, type, prefix } = extractIPDetails(ip);
const updateObject: {
@ -799,33 +913,34 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => {
prefix?: number;
$unset?: {
prefix: number;
}
};
} = {
ipAddress,
type,
comment
};
if (prefix !== undefined) {
updateObject.prefix = prefix;
} else {
updateObject.$unset = { prefix: 1 };
}
const trustedIp = await TrustedIP.findOneAndUpdate(
{
_id: new Types.ObjectId(trustedIpId),
workspace: new Types.ObjectId(workspaceId),
workspace: new Types.ObjectId(workspaceId)
},
updateObject,
{
new: true
}
);
if (!trustedIp) return res.status(400).send({
message: "Failed to update trusted IP"
});
if (!trustedIp)
return res.status(400).send({
message: "Failed to update trusted IP"
});
await EEAuditLogService.createAuditLog(
req.authData,
@ -841,34 +956,48 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => {
workspaceId: trustedIp.workspace
}
);
return res.status(200).send({
trustedIp
});
}
};
/**
* Delete IP access range from workspace with id [workspaceId]
* @param req
* @param res
* @param req
* @param res
*/
export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => {
const { workspaceId, trustedIpId } = req.params;
const {
params: { workspaceId, trustedIpId }
} = await validateRequest(DeleteWorkspaceTrustedIpV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.IpAllowList
);
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
const plan = await EELicenseService.getPlan(workspace.organization);
if (!plan.ipAllowlisting)
return res.status(400).send({
message:
"Failed to delete IP access range due to plan restriction. Upgrade plan to delete IP access range."
});
const plan = await EELicenseService.getPlan(req.workspace.organization);
if (!plan.ipAllowlisting) return res.status(400).send({
message: "Failed to delete IP access range due to plan restriction. Upgrade plan to delete IP access range."
});
const trustedIp = await TrustedIP.findOneAndDelete({
_id: new Types.ObjectId(trustedIpId),
workspace: new Types.ObjectId(workspaceId)
});
if (!trustedIp) return res.status(400).send({
message: "Failed to delete trusted IP"
});
if (!trustedIp)
return res.status(400).send({
message: "Failed to delete trusted IP"
});
await EEAuditLogService.createAuditLog(
req.authData,
@ -888,4 +1017,4 @@ export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => {
return res.status(200).send({
trustedIp
});
}
};

View File

@ -1,47 +1,50 @@
export enum ActorType {
USER = "user",
SERVICE = "service"
USER = "user",
SERVICE = "service"
}
export enum UserAgentType {
WEB = "web",
CLI = "cli",
K8_OPERATOR = "k8-operator",
OTHER = "other"
WEB = "web",
CLI = "cli",
K8_OPERATOR = "k8-operator",
OTHER = "other"
}
export enum EventType {
GET_SECRETS = "get-secrets",
GET_SECRET = "get-secret",
REVEAL_SECRET = "reveal-secret",
CREATE_SECRET = "create-secret",
UPDATE_SECRET = "update-secret",
DELETE_SECRET = "delete-secret",
GET_WORKSPACE_KEY = "get-workspace-key",
AUTHORIZE_INTEGRATION = "authorize-integration",
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
CREATE_INTEGRATION = "create-integration",
DELETE_INTEGRATION = "delete-integration",
ADD_TRUSTED_IP = "add-trusted-ip",
UPDATE_TRUSTED_IP = "update-trusted-ip",
DELETE_TRUSTED_IP = "delete-trusted-ip",
CREATE_SERVICE_TOKEN = "create-service-token",
DELETE_SERVICE_TOKEN = "delete-service-token",
CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment",
ADD_WORKSPACE_MEMBER = "add-workspace-member",
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
CREATE_FOLDER = "create-folder",
UPDATE_FOLDER = "update-folder",
DELETE_FOLDER = "delete-folder",
CREATE_WEBHOOK = "create-webhook",
UPDATE_WEBHOOK_STATUS = "update-webhook-status",
DELETE_WEBHOOK = "delete-webhook",
GET_SECRET_IMPORTS = "get-secret-imports",
CREATE_SECRET_IMPORT = "create-secret-import",
UPDATE_SECRET_IMPORT = "update-secret-import",
DELETE_SECRET_IMPORT = "delete-secret-import",
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions"
}
GET_SECRETS = "get-secrets",
GET_SECRET = "get-secret",
REVEAL_SECRET = "reveal-secret",
CREATE_SECRET = "create-secret",
CREATE_SECRETS = "create-secrets",
UPDATE_SECRET = "update-secret",
UPDATE_SECRETS = "update-secrets",
DELETE_SECRET = "delete-secret",
DELETE_SECRETS = "delete-secrets",
GET_WORKSPACE_KEY = "get-workspace-key",
AUTHORIZE_INTEGRATION = "authorize-integration",
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
CREATE_INTEGRATION = "create-integration",
DELETE_INTEGRATION = "delete-integration",
ADD_TRUSTED_IP = "add-trusted-ip",
UPDATE_TRUSTED_IP = "update-trusted-ip",
DELETE_TRUSTED_IP = "delete-trusted-ip",
CREATE_SERVICE_TOKEN = "create-service-token",
DELETE_SERVICE_TOKEN = "delete-service-token",
CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment",
ADD_WORKSPACE_MEMBER = "add-workspace-member",
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
CREATE_FOLDER = "create-folder",
UPDATE_FOLDER = "update-folder",
DELETE_FOLDER = "delete-folder",
CREATE_WEBHOOK = "create-webhook",
UPDATE_WEBHOOK_STATUS = "update-webhook-status",
DELETE_WEBHOOK = "delete-webhook",
GET_SECRET_IMPORTS = "get-secret-imports",
CREATE_SECRET_IMPORT = "create-secret-import",
UPDATE_SECRET_IMPORT = "update-secret-import",
DELETE_SECRET_IMPORT = "delete-secret-import",
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions"
}

View File

@ -1,403 +1,428 @@
import {
ActorType,
EventType
} from "./enums";
import { ActorType, EventType } from "./enums";
interface UserActorMetadata {
userId: string;
email: string;
userId: string;
email: string;
}
interface ServiceActorMetadata {
serviceId: string;
name: string;
serviceId: string;
name: string;
}
export interface UserActor {
type: ActorType.USER;
metadata: UserActorMetadata;
type: ActorType.USER;
metadata: UserActorMetadata;
}
export interface ServiceActor {
type: ActorType.SERVICE;
metadata: ServiceActorMetadata;
type: ActorType.SERVICE;
metadata: ServiceActorMetadata;
}
export type Actor =
| UserActor
| ServiceActor;
export type Actor = UserActor | ServiceActor;
interface GetSecretsEvent {
type: EventType.GET_SECRETS;
metadata: {
environment: string;
secretPath: string;
numberOfSecrets: number;
};
type: EventType.GET_SECRETS;
metadata: {
environment: string;
secretPath: string;
numberOfSecrets: number;
};
}
interface GetSecretEvent {
type: EventType.GET_SECRET;
metadata: {
environment: string;
secretPath: string;
secretId: string;
secretKey: string;
secretVersion: number;
};
type: EventType.GET_SECRET;
metadata: {
environment: string;
secretPath: string;
secretId: string;
secretKey: string;
secretVersion: number;
};
}
interface CreateSecretEvent {
type: EventType.CREATE_SECRET;
metadata: {
environment: string;
secretPath: string;
secretId: string;
secretKey: string;
secretVersion: number;
}
type: EventType.CREATE_SECRET;
metadata: {
environment: string;
secretPath: string;
secretId: string;
secretKey: string;
secretVersion: number;
};
}
interface CreateSecretBatchEvent {
type: EventType.CREATE_SECRETS;
metadata: {
environment: string;
secretPath: string;
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
};
}
interface UpdateSecretEvent {
type: EventType.UPDATE_SECRET;
metadata: {
environment: string;
secretPath: string;
secretId: string;
secretKey: string;
secretVersion: number;
}
type: EventType.UPDATE_SECRET;
metadata: {
environment: string;
secretPath: string;
secretId: string;
secretKey: string;
secretVersion: number;
};
}
interface UpdateSecretBatchEvent {
type: EventType.UPDATE_SECRETS;
metadata: {
environment: string;
secretPath: string;
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
};
}
interface DeleteSecretEvent {
type: EventType.DELETE_SECRET;
metadata: {
environment: string;
secretPath: string;
secretId: string;
secretKey: string;
secretVersion: number;
}
type: EventType.DELETE_SECRET;
metadata: {
environment: string;
secretPath: string;
secretId: string;
secretKey: string;
secretVersion: number;
};
}
interface DeleteSecretBatchEvent {
type: EventType.DELETE_SECRETS;
metadata: {
environment: string;
secretPath: string;
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
};
}
interface GetWorkspaceKeyEvent {
type: EventType.GET_WORKSPACE_KEY,
metadata: {
keyId: string;
}
type: EventType.GET_WORKSPACE_KEY;
metadata: {
keyId: string;
};
}
interface AuthorizeIntegrationEvent {
type: EventType.AUTHORIZE_INTEGRATION;
metadata: {
integration: string;
}
type: EventType.AUTHORIZE_INTEGRATION;
metadata: {
integration: string;
};
}
interface UnauthorizeIntegrationEvent {
type: EventType.UNAUTHORIZE_INTEGRATION;
metadata: {
integration: string;
}
type: EventType.UNAUTHORIZE_INTEGRATION;
metadata: {
integration: string;
};
}
interface CreateIntegrationEvent {
type: EventType.CREATE_INTEGRATION;
metadata: {
integrationId: string;
integration: string; // TODO: fix type
environment: string;
secretPath: string;
url?: string;
app?: string;
appId?: string;
targetEnvironment?: string;
targetEnvironmentId?: string;
targetService?: string;
targetServiceId?: string;
path?: string;
region?: string;
}
type: EventType.CREATE_INTEGRATION;
metadata: {
integrationId: string;
integration: string; // TODO: fix type
environment: string;
secretPath: string;
url?: string;
app?: string;
appId?: string;
targetEnvironment?: string;
targetEnvironmentId?: string;
targetService?: string;
targetServiceId?: string;
path?: string;
region?: string;
};
}
interface DeleteIntegrationEvent {
type: EventType.DELETE_INTEGRATION;
metadata: {
integrationId: string;
integration: string; // TODO: fix type
environment: string;
secretPath: string;
url?: string;
app?: string;
appId?: string;
targetEnvironment?: string;
targetEnvironmentId?: string;
targetService?: string;
targetServiceId?: string;
path?: string;
region?: string;
}
type: EventType.DELETE_INTEGRATION;
metadata: {
integrationId: string;
integration: string; // TODO: fix type
environment: string;
secretPath: string;
url?: string;
app?: string;
appId?: string;
targetEnvironment?: string;
targetEnvironmentId?: string;
targetService?: string;
targetServiceId?: string;
path?: string;
region?: string;
};
}
interface AddTrustedIPEvent {
type: EventType.ADD_TRUSTED_IP;
metadata: {
trustedIpId: string;
ipAddress: string;
prefix?: number;
}
type: EventType.ADD_TRUSTED_IP;
metadata: {
trustedIpId: string;
ipAddress: string;
prefix?: number;
};
}
interface UpdateTrustedIPEvent {
type: EventType.UPDATE_TRUSTED_IP;
metadata: {
trustedIpId: string;
ipAddress: string;
prefix?: number;
}
type: EventType.UPDATE_TRUSTED_IP;
metadata: {
trustedIpId: string;
ipAddress: string;
prefix?: number;
};
}
interface DeleteTrustedIPEvent {
type: EventType.DELETE_TRUSTED_IP;
metadata: {
trustedIpId: string;
ipAddress: string;
prefix?: number;
}
type: EventType.DELETE_TRUSTED_IP;
metadata: {
trustedIpId: string;
ipAddress: string;
prefix?: number;
};
}
interface CreateServiceTokenEvent {
type: EventType.CREATE_SERVICE_TOKEN;
metadata: {
name: string;
scopes: Array<{
environment: string;
secretPath: string;
}>;
}
type: EventType.CREATE_SERVICE_TOKEN;
metadata: {
name: string;
scopes: Array<{
environment: string;
secretPath: string;
}>;
};
}
interface DeleteServiceTokenEvent {
type: EventType.DELETE_SERVICE_TOKEN;
metadata: {
name: string;
scopes: Array<{
environment: string;
secretPath: string;
}>;
}
type: EventType.DELETE_SERVICE_TOKEN;
metadata: {
name: string;
scopes: Array<{
environment: string;
secretPath: string;
}>;
};
}
interface CreateEnvironmentEvent {
type: EventType.CREATE_ENVIRONMENT;
metadata: {
name: string;
slug: string;
}
type: EventType.CREATE_ENVIRONMENT;
metadata: {
name: string;
slug: string;
};
}
interface UpdateEnvironmentEvent {
type: EventType.UPDATE_ENVIRONMENT;
metadata: {
oldName: string;
newName: string;
oldSlug: string;
newSlug: string;
}
type: EventType.UPDATE_ENVIRONMENT;
metadata: {
oldName: string;
newName: string;
oldSlug: string;
newSlug: string;
};
}
interface DeleteEnvironmentEvent {
type: EventType.DELETE_ENVIRONMENT;
metadata: {
name: string;
slug: string;
}
type: EventType.DELETE_ENVIRONMENT;
metadata: {
name: string;
slug: string;
};
}
interface AddWorkspaceMemberEvent {
type: EventType.ADD_WORKSPACE_MEMBER;
metadata: {
userId: string;
email: string;
}
type: EventType.ADD_WORKSPACE_MEMBER;
metadata: {
userId: string;
email: string;
};
}
interface RemoveWorkspaceMemberEvent {
type: EventType.REMOVE_WORKSPACE_MEMBER;
metadata: {
userId: string;
email: string;
}
type: EventType.REMOVE_WORKSPACE_MEMBER;
metadata: {
userId: string;
email: string;
};
}
interface CreateFolderEvent {
type: EventType.CREATE_FOLDER;
metadata: {
environment: string;
folderId: string;
folderName: string;
folderPath: string;
}
type: EventType.CREATE_FOLDER;
metadata: {
environment: string;
folderId: string;
folderName: string;
folderPath: string;
};
}
interface UpdateFolderEvent {
type: EventType.UPDATE_FOLDER;
metadata: {
environment: string;
folderId: string;
oldFolderName: string;
newFolderName: string;
folderPath: string;
}
type: EventType.UPDATE_FOLDER;
metadata: {
environment: string;
folderId: string;
oldFolderName: string;
newFolderName: string;
folderPath: string;
};
}
interface DeleteFolderEvent {
type: EventType.DELETE_FOLDER;
metadata: {
environment: string;
folderId: string;
folderName: string;
folderPath: string;
}
type: EventType.DELETE_FOLDER;
metadata: {
environment: string;
folderId: string;
folderName: string;
folderPath: string;
};
}
interface CreateWebhookEvent {
type: EventType.CREATE_WEBHOOK,
metadata: {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
}
type: EventType.CREATE_WEBHOOK;
metadata: {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
};
}
interface UpdateWebhookStatusEvent {
type: EventType.UPDATE_WEBHOOK_STATUS,
metadata: {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
}
type: EventType.UPDATE_WEBHOOK_STATUS;
metadata: {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
};
}
interface DeleteWebhookEvent {
type: EventType.DELETE_WEBHOOK,
metadata: {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
}
type: EventType.DELETE_WEBHOOK;
metadata: {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
};
}
interface GetSecretImportsEvent {
type: EventType.GET_SECRET_IMPORTS,
metadata: {
environment: string;
secretImportId: string;
folderId: string;
numberOfImports: number;
}
type: EventType.GET_SECRET_IMPORTS;
metadata: {
environment: string;
secretImportId: string;
folderId: string;
numberOfImports: number;
};
}
interface CreateSecretImportEvent {
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
secretImportId: string;
folderId: string;
importFromEnvironment: string;
importFromSecretPath: string;
importToEnvironment: string;
importToSecretPath: string;
}
type: EventType.CREATE_SECRET_IMPORT;
metadata: {
secretImportId: string;
folderId: string;
importFromEnvironment: string;
importFromSecretPath: string;
importToEnvironment: string;
importToSecretPath: string;
};
}
interface UpdateSecretImportEvent {
type: EventType.UPDATE_SECRET_IMPORT,
metadata: {
secretImportId: string;
folderId: string;
importToEnvironment: string;
importToSecretPath: string;
orderBefore: {
environment: string;
secretPath: string;
}[],
orderAfter: {
environment: string;
secretPath: string;
}[]
}
type: EventType.UPDATE_SECRET_IMPORT;
metadata: {
secretImportId: string;
folderId: string;
importToEnvironment: string;
importToSecretPath: string;
orderBefore: {
environment: string;
secretPath: string;
}[];
orderAfter: {
environment: string;
secretPath: string;
}[];
};
}
interface DeleteSecretImportEvent {
type: EventType.DELETE_SECRET_IMPORT,
metadata: {
secretImportId: string;
folderId: string;
importFromEnvironment: string;
importFromSecretPath: string;
importToEnvironment: string;
importToSecretPath: string;
}
type: EventType.DELETE_SECRET_IMPORT;
metadata: {
secretImportId: string;
folderId: string;
importFromEnvironment: string;
importFromSecretPath: string;
importToEnvironment: string;
importToSecretPath: string;
};
}
interface UpdateUserRole {
type: EventType.UPDATE_USER_WORKSPACE_ROLE,
metadata: {
userId: string;
email: string;
oldRole: string;
newRole: string;
}
type: EventType.UPDATE_USER_WORKSPACE_ROLE;
metadata: {
userId: string;
email: string;
oldRole: string;
newRole: string;
};
}
interface UpdateUserDeniedPermissions {
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS,
metadata: {
userId: string;
email: string;
deniedPermissions: {
environmentSlug: string;
ability: string;
}[]
}
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS;
metadata: {
userId: string;
email: string;
deniedPermissions: {
environmentSlug: string;
ability: string;
}[];
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
| CreateSecretEvent
| UpdateSecretEvent
| DeleteSecretEvent
| GetWorkspaceKeyEvent
| AuthorizeIntegrationEvent
| UnauthorizeIntegrationEvent
| CreateIntegrationEvent
| DeleteIntegrationEvent
| AddTrustedIPEvent
| UpdateTrustedIPEvent
| DeleteTrustedIPEvent
| CreateServiceTokenEvent
| DeleteServiceTokenEvent
| CreateEnvironmentEvent
| UpdateEnvironmentEvent
| DeleteEnvironmentEvent
| AddWorkspaceMemberEvent
| RemoveWorkspaceMemberEvent
| CreateFolderEvent
| UpdateFolderEvent
| DeleteFolderEvent
| CreateWebhookEvent
| UpdateWebhookStatusEvent
| DeleteWebhookEvent
| GetSecretImportsEvent
| CreateSecretImportEvent
| UpdateSecretImportEvent
| DeleteSecretImportEvent
| UpdateUserRole
| UpdateUserDeniedPermissions;
export type Event =
| GetSecretsEvent
| GetSecretEvent
| CreateSecretEvent
| CreateSecretBatchEvent
| UpdateSecretEvent
| UpdateSecretBatchEvent
| DeleteSecretEvent
| DeleteSecretBatchEvent
| GetWorkspaceKeyEvent
| AuthorizeIntegrationEvent
| UnauthorizeIntegrationEvent
| CreateIntegrationEvent
| DeleteIntegrationEvent
| AddTrustedIPEvent
| UpdateTrustedIPEvent
| DeleteTrustedIPEvent
| CreateServiceTokenEvent
| DeleteServiceTokenEvent
| CreateEnvironmentEvent
| UpdateEnvironmentEvent
| DeleteEnvironmentEvent
| AddWorkspaceMemberEvent
| RemoveWorkspaceMemberEvent
| CreateFolderEvent
| UpdateFolderEvent
| DeleteFolderEvent
| CreateWebhookEvent
| UpdateWebhookStatusEvent
| DeleteWebhookEvent
| GetSecretImportsEvent
| CreateSecretImportEvent
| UpdateSecretImportEvent
| DeleteSecretImportEvent
| UpdateUserRole
| UpdateUserDeniedPermissions;

View File

@ -0,0 +1,55 @@
import { Schema, Types, model } from "mongoose";
export interface IRole {
_id: Types.ObjectId;
name: string;
description: string;
slug: string;
permissions: Array<unknown>;
workspace: Types.ObjectId;
organization: Types.ObjectId;
isOrgRole: boolean;
}
const roleSchema = new Schema<IRole>(
{
name: {
type: String,
required: true
},
organization: {
type: Schema.Types.ObjectId,
ref: "Organization",
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace"
},
isOrgRole: {
type: Boolean,
required: true,
select: false
},
description: {
type: String
},
slug: {
type: String,
required: true
},
permissions: {
type: Array,
required: true
}
},
{
timestamps: true
}
);
roleSchema.index({ organization: 1, workspace: 1 });
const Role = model<IRole>("Role", roleSchema);
export default Role;

View File

@ -4,7 +4,7 @@ import {
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8,
SECRET_PERSONAL,
SECRET_SHARED,
SECRET_SHARED
} from "../../variables";
export interface ISecretVersion {
@ -23,6 +23,7 @@ export interface ISecretVersion {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
skipMultilineEncoding?: boolean;
algorithm: "aes-256-gcm";
keyEncoding: "utf8" | "base64";
createdAt: string;
@ -36,95 +37,96 @@ const secretVersionSchema = new Schema<ISecretVersion>(
// could be deleted
type: Schema.Types.ObjectId,
ref: "Secret",
required: true,
required: true
},
version: {
type: Number,
default: 1,
required: true,
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
required: true
},
type: {
type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true,
required: true
},
user: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: "User",
ref: "User"
},
environment: {
type: String,
required: true,
required: true
},
isDeleted: {
// consider removing field
type: Boolean,
default: false,
required: true,
required: true
},
secretBlindIndex: {
type: String,
select: false,
select: false
},
secretKeyCiphertext: {
type: String,
required: true,
required: true
},
secretKeyIV: {
type: String, // symmetric
required: true,
required: true
},
secretKeyTag: {
type: String, // symmetric
required: true,
required: true
},
secretValueCiphertext: {
type: String,
required: true,
required: true
},
secretValueIV: {
type: String, // symmetric
required: true,
required: true
},
secretValueTag: {
type: String, // symmetric
required: true,
required: true
},
skipMultilineEncoding: {
type: Boolean,
required: false
},
algorithm: {
// the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
default: ALGORITHM_AES_256_GCM,
default: ALGORITHM_AES_256_GCM
},
keyEncoding: {
type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true,
default: ENCODING_SCHEME_UTF8,
default: ENCODING_SCHEME_UTF8
},
folder: {
type: String,
required: true,
required: true
},
tags: {
ref: "Tag",
type: [Schema.Types.ObjectId],
default: [],
default: []
}
},
{
timestamps: true,
timestamps: true
}
);
export const SecretVersion = model<ISecretVersion>(
"SecretVersion",
secretVersionSchema
);
export const SecretVersion = model<ISecretVersion>("SecretVersion", secretVersionSchema);

View File

@ -1,17 +1,8 @@
import express from "express";
const router = express.Router();
import {
validateRequest,
} from "../../../middleware";
import { param } from "express-validator";
import { actionController } from "../../controllers/v1";
// TODO: put into action controller
router.get(
"/:actionId",
param("actionId").exists().trim(),
validateRequest,
actionController.getAction
);
router.get("/:actionId", actionController.getAction);
export default router;
export default router;

View File

@ -1,21 +1,16 @@
import express from "express";
const router = express.Router();
import {
requireAuth,
validateRequest,
} from "../../../middleware";
import { query } from "express-validator";
import { requireAuth, validateRequest } from "../../../middleware";
import { cloudProductsController } from "../../controllers/v1";
import { AuthMode } from "../../../variables";
router.get(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
query("billing-cycle").exists().isIn(["monthly", "yearly"]),
validateRequest,
cloudProductsController.getCloudProducts
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
validateRequest,
cloudProductsController.getCloudProducts
);
export default router;
export default router;

View File

@ -7,15 +7,17 @@ import workspace from "./workspace";
import action from "./action";
import cloudProducts from "./cloudProducts";
import secretScanning from "./secretScanning";
import roles from "./role";
export {
secret,
secretSnapshot,
organizations,
sso,
users,
workspace,
action,
cloudProducts,
secretScanning
}
secret,
secretSnapshot,
organizations,
sso,
users,
workspace,
action,
cloudProducts,
secretScanning,
roles
};

View File

@ -1,237 +1,127 @@
import express from "express";
const router = express.Router();
import {
requireAuth,
requireOrganizationAuth,
validateRequest,
} from "../../../middleware";
import { body, param, query } from "express-validator";
import { requireAuth } from "../../../middleware";
import { organizationsController } from "../../controllers/v1";
import {
ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER
} from "../../../variables";
import { AuthMode } from "../../../variables";
router.get(
"/:organizationId/plans/table",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("billingCycle").exists().isString().isIn(["monthly", "yearly"]),
validateRequest,
organizationsController.getOrganizationPlansTable
"/:organizationId/plans/table",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.getOrganizationPlansTable
);
router.get(
"/:organizationId/plan",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("workspaceId").optional().isString(),
validateRequest,
organizationsController.getOrganizationPlan
"/:organizationId/plan",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.getOrganizationPlan
);
router.post(
"/:organizationId/session/trial",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("success_url").exists().trim(),
validateRequest,
organizationsController.startOrganizationTrial
"/:organizationId/session/trial",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.startOrganizationTrial
);
router.get(
"/:organizationId/plan/billing",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("workspaceId").optional().isString(),
validateRequest,
organizationsController.getOrganizationPlanBillingInfo
"/:organizationId/plan/billing",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.getOrganizationPlanBillingInfo
);
router.get(
"/:organizationId/plan/table",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("workspaceId").optional().isString(),
validateRequest,
organizationsController.getOrganizationPlanTable
"/:organizationId/plan/table",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.getOrganizationPlanTable
);
router.get(
"/:organizationId/billing-details",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationBillingDetails
"/:organizationId/billing-details",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.getOrganizationBillingDetails
);
router.patch(
"/:organizationId/billing-details",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("email").optional().isString().trim(),
body("name").optional().isString().trim(),
validateRequest,
organizationsController.updateOrganizationBillingDetails
"/:organizationId/billing-details",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.updateOrganizationBillingDetails
);
router.get(
"/:organizationId/billing-details/payment-methods",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationPmtMethods
"/:organizationId/billing-details/payment-methods",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.getOrganizationPmtMethods
);
router.post(
"/:organizationId/billing-details/payment-methods",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("success_url").exists().isString(),
body("cancel_url").exists().isString(),
validateRequest,
organizationsController.addOrganizationPmtMethod
"/:organizationId/billing-details/payment-methods",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.addOrganizationPmtMethod
);
router.delete(
"/:organizationId/billing-details/payment-methods/:pmtMethodId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
param("pmtMethodId").exists().trim(),
validateRequest,
organizationsController.deleteOrganizationPmtMethod
"/:organizationId/billing-details/payment-methods/:pmtMethodId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.deleteOrganizationPmtMethod
);
router.get(
"/:organizationId/billing-details/tax-ids",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationTaxIds
"/:organizationId/billing-details/tax-ids",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.getOrganizationTaxIds
);
router.post(
"/:organizationId/billing-details/tax-ids",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("type").exists().isString(),
body("value").exists().isString(),
validateRequest,
organizationsController.addOrganizationTaxId
"/:organizationId/billing-details/tax-ids",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.addOrganizationTaxId
);
router.delete(
"/:organizationId/billing-details/tax-ids/:taxId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
param("taxId").exists().trim(),
validateRequest,
organizationsController.deleteOrganizationTaxId
"/:organizationId/billing-details/tax-ids/:taxId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.deleteOrganizationTaxId
);
router.get(
"/:organizationId/invoices",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationInvoices
"/:organizationId/invoices",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.getOrganizationInvoices
);
router.get(
"/:organizationId/licenses",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationLicenses
"/:organizationId/licenses",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.getOrganizationLicenses
);
export default router;
export default router;

View File

@ -0,0 +1,33 @@
import express from "express";
import { roleController } from "../../controllers/v1";
import { requireAuth } from "../../../middleware";
import { AuthMode } from "../../../variables";
const router = express.Router();
router.post("/", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), roleController.createRole);
router.patch("/:id", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), roleController.updateRole);
router.delete(
"/:id",
requireAuth({ acceptedAuthModes: [AuthMode.JWT] }),
roleController.deleteRole
);
router.get("/", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), roleController.getRoles);
// get a user permissions in an org
router.get(
"/organization/:orgId/permissions",
requireAuth({ acceptedAuthModes: [AuthMode.JWT] }),
roleController.getUserPermissions
);
router.get(
"/workspace/:workspaceId/permissions",
requireAuth({ acceptedAuthModes: [AuthMode.JWT] }),
roleController.getUserWorkspacePermissions
);
export default router;

View File

@ -1,48 +1,25 @@
import express from "express";
const router = express.Router();
import {
requireAuth,
requireSecretAuth,
validateRequest,
} from "../../../middleware";
import { body, param, query } from "express-validator";
import { requireAuth } from "../../../middleware";
import { secretController } from "../../controllers/v1";
import {
ADMIN,
AuthMode,
MEMBER,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
AuthMode
} from "../../../variables";
router.get(
"/:secretId/secret-versions",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS],
}),
param("secretId").exists().trim(),
query("offset").exists().isInt(),
query("limit").exists().isInt(),
validateRequest,
secretController.getSecretVersions
"/:secretId/secret-versions",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
secretController.getSecretVersions
);
router.post(
"/:secretId/secret-versions/rollback",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS],
}),
param("secretId").exists().trim(),
body("version").exists().isInt(),
secretController.rollbackSecretVersion
"/:secretId/secret-versions/rollback",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
secretController.rollbackSecretVersion
);
export default router;
export default router;

View File

@ -1,81 +1,53 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../../middleware";
import {
requireAuth,
requireOrganizationAuth,
validateRequest,
} from "../../../middleware";
import { body, param } from "express-validator";
import { createInstallationSession, getCurrentOrganizationInstallationStatus, getRisksForOrganization, linkInstallationToOrganization, updateRisksStatus } from "../../../controllers/v1/secretScanningController";
import { ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER } from "../../../variables";
createInstallationSession,
getCurrentOrganizationInstallationStatus,
getRisksForOrganization,
linkInstallationToOrganization,
updateRisksStatus
} from "../../../controllers/v1/secretScanningController";
import { AuthMode } from "../../../variables";
router.post(
"/create-installation-session/organization/:organizationId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
acceptedAuthModes: [AuthMode.JWT]
}),
param("organizationId").exists().trim(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
createInstallationSession
);
router.post(
"/link-installation",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
acceptedAuthModes: [AuthMode.JWT]
}),
body("installationId").exists().trim(),
body("sessionId").exists().trim(),
validateRequest,
linkInstallationToOrganization
);
router.get(
"/installation-status/organization/:organizationId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
acceptedAuthModes: [AuthMode.JWT]
}),
param("organizationId").exists().trim(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
getCurrentOrganizationInstallationStatus
);
router.get(
"/organization/:organizationId/risks",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
acceptedAuthModes: [AuthMode.JWT]
}),
param("organizationId").exists().trim(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
getRisksForOrganization
);
router.post(
"/organization/:organizationId/risks/:riskId/status",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
acceptedAuthModes: [AuthMode.JWT]
}),
param("organizationId").exists().trim(),
param("riskId").exists().trim(),
body("status").exists(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
updateRisksStatus
);
export default router;
export default router;

View File

@ -1,27 +1,15 @@
import express from "express";
const router = express.Router();
import {
requireSecretSnapshotAuth,
} from "../../middleware";
import {
requireAuth,
validateRequest,
} from "../../../middleware";
import { param } from "express-validator";
import { ADMIN, AuthMode, MEMBER } from "../../../variables";
import { requireAuth } from "../../../middleware";
import { AuthMode } from "../../../variables";
import { secretSnapshotController } from "../../controllers/v1";
router.get(
"/:secretSnapshotId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireSecretSnapshotAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param("secretSnapshotId").exists().trim(),
validateRequest,
secretSnapshotController.getSecretSnapshot
"/:secretSnapshotId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretSnapshotController.getSecretSnapshot
);
export default router;
export default router;

View File

@ -1,146 +1,95 @@
import express from "express";
const router = express.Router();
import passport from "passport";
import {
AuthProvider
} from "../../models";
import {
requireAuth,
requireOrganizationAuth,
validateRequest,
} from "../../../middleware";
import { body, query } from "express-validator";
import { requireAuth } from "../../../middleware";
import { ssoController } from "../../controllers/v1";
import { authLimiter } from "../../../helpers/rateLimiter";
import {
ACCEPTED,
ADMIN,
AuthMode,
OWNER
} from "../../../variables";
import { AuthMode } from "../../../variables";
router.get(
"/redirect/google",
authLimiter,
(req, res, next) => {
passport.authenticate("google", {
scope: ["profile", "email"],
session: false,
...(req.query.callback_port ? {
state: req.query.callback_port as string
} : {})
})(req, res, next);
}
);
router.get("/redirect/google", authLimiter, (req, res, next) => {
passport.authenticate("google", {
scope: ["profile", "email"],
session: false,
...(req.query.callback_port
? {
state: req.query.callback_port as string
}
: {})
})(req, res, next);
});
router.get(
"/google",
passport.authenticate("google", {
failureRedirect: "/login/provider/error",
session: false
passport.authenticate("google", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get(
"/redirect/github",
authLimiter,
(req, res, next) => {
passport.authenticate("github", {
session: false,
...(req.query.callback_port ? {
state: req.query.callback_port as string
} : {})
})(req, res, next);
}
);
router.get("/redirect/github", authLimiter, (req, res, next) => {
passport.authenticate("github", {
session: false,
...(req.query.callback_port
? {
state: req.query.callback_port as string
}
: {})
})(req, res, next);
});
router.get(
"/github",
authLimiter,
passport.authenticate("github", {
failureRedirect: "/login/provider/error",
session: false
passport.authenticate("github", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get(
"/redirect/saml2/:ssoIdentifier",
authLimiter,
(req, res, next) => {
const options = {
failureRedirect: "/",
additionalParams: {
RelayState: req.query.callback_port ?? ""
},
};
passport.authenticate("saml", options)(req, res, next);
}
);
router.get("/redirect/saml2/:ssoIdentifier", authLimiter, (req, res, next) => {
const options = {
failureRedirect: "/",
additionalParams: {
RelayState: req.query.callback_port ?? ""
}
};
passport.authenticate("saml", options)(req, res, next);
});
router.post("/saml2/:ssoIdentifier",
passport.authenticate("saml", {
failureRedirect: "/login/provider/error",
failureFlash: true,
router.post(
"/saml2/:ssoIdentifier",
passport.authenticate("saml", {
failureRedirect: "/login/provider/error",
failureFlash: true,
session: false
}),
ssoController.redirectSSO
);
router.get(
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "query"
}),
query("organizationId").exists().trim(),
validateRequest,
ssoController.getSSOConfig
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
ssoController.getSSOConfig
);
router.post(
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "body"
}),
body("organizationId").exists().trim(),
body("authProvider").exists().isString().isIn([AuthProvider.OKTA_SAML]),
body("isActive").exists().isBoolean(),
body("entryPoint").exists().isString(),
body("issuer").exists().isString(),
body("cert").exists().isString(),
validateRequest,
ssoController.createSSOConfig
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
ssoController.createSSOConfig
);
router.patch(
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "body"
}),
body("organizationId").exists().trim(),
body("authProvider").optional().isString(),
body("isActive").optional().isBoolean(),
body("entryPoint").optional().isString(),
body("issuer").optional().isString(),
body("cert").optional().isString(),
validateRequest,
ssoController.updateSSOConfig
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
ssoController.updateSSOConfig
);
export default router;
export default router;

View File

@ -1,182 +1,85 @@
import express from "express";
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from "../../../middleware";
import { body, param, query } from "express-validator";
import {
ADMIN,
AuthMode,
MEMBER
} from "../../../variables";
import { requireAuth } from "../../../middleware";
import { AuthMode } from "../../../variables";
import { workspaceController } from "../../controllers/v1";
import { EventType, UserAgentType } from "../../models";
router.get(
"/:workspaceId/secret-snapshots",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
param("workspaceId").exists().trim(),
query("environment").isString().exists().trim(),
query("folderId").default("root").isString().trim(),
query("offset").exists().isInt(),
query("limit").exists().isInt(),
validateRequest,
workspaceController.getWorkspaceSecretSnapshots
);
router.get(
"/:workspaceId/secret-snapshots/count",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
acceptedAuthModes: [AuthMode.JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
param("workspaceId").exists().trim(),
query("environment").isString().exists().trim(),
query("folderId").default("root").isString().trim(),
validateRequest,
workspaceController.getWorkspaceSecretSnapshotsCount
);
router.post(
"/:workspaceId/secret-snapshots/rollback",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
param("workspaceId").exists().trim(),
body("environment").isString().exists().trim(),
query("folderId").default("root").isString().exists().trim(),
body("version").exists().isInt(),
validateRequest,
workspaceController.rollbackWorkspaceSecretSnapshot
);
router.get(
"/:workspaceId/logs",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
param("workspaceId").exists().trim(),
query("offset").exists().isInt(),
query("limit").exists().isInt(),
query("sortBy"),
query("userId"),
query("actionNames"),
validateRequest,
workspaceController.getWorkspaceLogs
);
router.get(
"/:workspaceId/audit-logs",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
param("workspaceId").exists().trim(),
query("eventType").isString().isIn(Object.values(EventType)).optional({ nullable: true }),
query("userAgentType").isString().isIn(Object.values(UserAgentType)).optional({ nullable: true }),
query("actor").optional({ nullable: true }),
query("startDate").isISO8601().withMessage("Invalid start date format").optional({ nullable: true }),
query("endDate").isISO8601().withMessage("Invalid end date format").optional({ nullable: true }),
query("offset"),
query("limit"),
validateRequest,
workspaceController.getWorkspaceAuditLogs
);
router.get(
"/:workspaceId/audit-logs/filters/actors",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
param("workspaceId").exists().trim(),
validateRequest,
workspaceController.getWorkspaceAuditLogActorFilterOpts
);
router.get(
"/:workspaceId/trusted-ips",
param("workspaceId").exists().isString().trim(),
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
acceptedAuthModes: [AuthMode.JWT]
}),
workspaceController.getWorkspaceTrustedIps
);
router.post(
"/:workspaceId/trusted-ips",
param("workspaceId").exists().isString().trim(),
body("ipAddress").exists().isString().trim(),
body("comment").default("").isString().trim(),
body("isActive").exists().isBoolean(),
validateRequest,
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
acceptedAuthModes: [AuthMode.JWT]
}),
workspaceController.addWorkspaceTrustedIp
);
router.patch(
"/:workspaceId/trusted-ips/:trustedIpId",
param("workspaceId").exists().isString().trim(),
param("trustedIpId").exists().isString().trim(),
body("ipAddress").isString().trim().default(""),
body("comment").default("").isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
acceptedAuthModes: [AuthMode.JWT]
}),
workspaceController.updateWorkspaceTrustedIp
);
router.delete(
"/:workspaceId/trusted-ips/:trustedIpId",
param("workspaceId").exists().isString().trim(),
param("trustedIpId").exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
acceptedAuthModes: [AuthMode.JWT]
}),
workspaceController.deleteWorkspaceTrustedIp
);

View File

@ -64,7 +64,7 @@ class EELicenseService {
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,
rbac: true,
rbac: false,
customRateLimits: false,
customAlerts: false,
auditLogs: false,

View File

@ -3,7 +3,21 @@ import { mkdir, readFile, rm, writeFile } from "fs";
import { tmpdir } from "os";
import { join } from "path"
import { SecretMatch } from "./types";
import { Octokit } from "@octokit/rest";
export async function scanFullRepoContentAndGetFindings(octokit: any, installationId: number, repositoryFullName: string): Promise<SecretMatch[]> {
const tempFolder = await createTempFolder();
const findingsPath = join(tempFolder, "findings.json");
const repoPath = join(tempFolder, "repo.git")
try {
const { data: { token }} = await octokit.apps.createInstallationAccessToken({installation_id: installationId})
await cloneRepo(token, repositoryFullName, repoPath)
await runInfisicalScanOnRepo(repoPath, findingsPath);
const findingsData = await readFindingsFile(findingsPath);
return JSON.parse(findingsData);
} finally {
await deleteTempFolder(tempFolder);
}
}
export async function scanContentAndGetFindings(textContent: string): Promise<SecretMatch[]> {
const tempFolder = await createTempFolder();
@ -36,6 +50,8 @@ export function createTempFolder(): Promise<string> {
});
}
export function writeTextToFile(filePath: string, content: string): Promise<void> {
return new Promise((resolve, reject) => {
writeFile(filePath, content, (err) => {
@ -48,6 +64,33 @@ export function writeTextToFile(filePath: string, content: string): Promise<void
});
}
export async function cloneRepo(installationAcccessToken: string, repositoryFullName: string, repoPath: string): Promise<void> {
const cloneUrl = `https://x-access-token:${installationAcccessToken}@github.com/${repositoryFullName}.git`;
const command = `git clone ${cloneUrl} ${repoPath} --bare`
return new Promise((resolve, reject) => {
exec(command, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
})
}
export function runInfisicalScanOnRepo(repoPath: string, outputPath: string): Promise<void> {
return new Promise((resolve, reject) => {
const command = `cd ${repoPath} && infisical scan --exit-code=77 -r "${outputPath}"`;
exec(command, (error) => {
if (error && error.code != 77) {
reject(error);
} else {
resolve();
}
});
});
}
export function runInfisicalScan(inputPath: string, outputPath: string): Promise<void> {
return new Promise((resolve, reject) => {
const command = `cat "${inputPath}" | infisical scan --exit-code=77 --pipe -r "${outputPath}"`;
@ -96,30 +139,4 @@ export function convertKeysToLowercase<T>(obj: T): T {
}
return convertedObj;
}
export async function getCommits(octokit: Octokit, owner: string, repo: string) {
let commits: { sha: string }[] = [];
let page = 1;
while (true) {
const response = await octokit.repos.listCommits({
owner,
repo,
per_page: 100,
page,
});
commits = commits.concat(response.data);
if (response.data.length == 0) break;
page++;
}
return commits;
}
export async function getFilesFromCommit(octokit: any, owner: string, repo: string, sha: string) {
const response = await octokit.repos.getCommit({
owner,
repo,
ref: sha,
});
}

View File

@ -0,0 +1,260 @@
import {
AbilityBuilder,
ForcedSubject,
MongoAbility,
RawRuleOf,
buildMongoQueryMatcher,
createMongoAbility
} from "@casl/ability";
import { Membership } from "../../models";
import { IRole } from "../models/role";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
import picomatch from "picomatch";
const $glob: FieldInstruction<string> = {
type: "field",
validate(instruction, value) {
if (typeof value !== "string") {
throw new Error(`"${instruction.name}" expects value to be a string`);
}
}
};
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
const secretPath = context.get(object, node.field);
const permissionSecretGlobPath = node.value;
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
};
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
export enum ProjectPermissionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum ProjectPermissionSub {
Role = "role",
Member = "member",
Settings = "settings",
Integrations = "integrations",
Webhooks = "webhooks",
ServiceTokens = "service-tokens",
Environments = "environments",
Tags = "tags",
AuditLogs = "audit-logs",
IpAllowList = "ip-allowlist",
Workspace = "workspace",
Secrets = "secrets",
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval"
}
type SubjectFields = {
environment: string;
secretPath: string;
};
export type ProjectPermissionSet =
| [
ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
]
| [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member]
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace);
return build({ conditionsMatcher });
};
export const adminProjectPermissions = buildAdminPermission();
const buildMemberPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
return build({ conditionsMatcher });
};
export const memberProjectPermissions = buildMemberPermission();
const buildViewerPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
return build({ conditionsMatcher });
};
export const viewerProjectPermission = buildViewerPermission();
export const getUserProjectPermissions = async (userId: string, workspaceId: string) => {
// TODO(akhilmhdh): speed this up by pulling from cache later
const membership = await Membership.findOne({
user: userId,
workspace: workspaceId
})
.populate<{
customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] };
}>("customRole")
.exec();
if (!membership || (membership.role === "custom" && !membership.customRole)) {
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
}
if (membership.role === "admin") return { permission: adminProjectPermissions, membership };
if (membership.role === "member") return { permission: memberProjectPermissions, membership };
if (membership.role === "viewer") return { permission: viewerProjectPermission, membership };
if (membership.role === "custom") {
const permission = createMongoAbility<ProjectPermissionSet>(membership.customRole.permissions, {
conditionsMatcher
});
return { permission, membership };
}
throw BadRequestError({ message: "User role not found" });
};

View File

@ -0,0 +1,134 @@
import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability";
import { MembershipOrg } from "../../models";
import { IRole } from "../models/role";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { ACCEPTED } from "../../variables";
import { conditionsMatcher } from "./ProjectRoleService";
export enum OrgPermissionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum OrgPermissionSubjects {
Workspace = "workspace",
Role = "role",
Member = "member",
Settings = "settings",
IncidentAccount = "incident-contact",
Sso = "sso",
Billing = "billing",
SecretScanning = "secret-scanning"
}
export type OrgPermissionSet =
| [OrgPermissionActions.Read, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions, OrgPermissionSubjects.Role]
| [OrgPermissionActions, OrgPermissionSubjects.Member]
| [OrgPermissionActions, OrgPermissionSubjects.Settings]
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing];
const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
// ws permissions
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
// role permission
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
return build({ conditionsMatcher });
};
export const adminPermissions = buildAdminPermission();
const buildMemberPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
return build({ conditionsMatcher });
};
export const memberPermissions = buildMemberPermission();
export const getUserOrgPermissions = async (userId: string, orgId: string) => {
// TODO(akhilmhdh): speed this up by pulling from cache later
const membership = await MembershipOrg.findOne({
user: userId,
organization: orgId,
status: ACCEPTED
})
.populate<{ customRole: IRole & { permissions: RawRuleOf<MongoAbility<OrgPermissionSet>>[] } }>(
"customRole"
)
.exec();
if (!membership || (membership.role === "custom" && !membership.customRole)) {
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
}
if (membership.role === "admin") return { permission: adminPermissions, membership };
if (membership.role === "member") return { permission: memberPermissions, membership };
if (membership.role === "custom") {
const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions, {
conditionsMatcher
});
return { permission, membership };
}
throw BadRequestError({ message: "User role not found" });
};

View File

@ -0,0 +1,68 @@
import { z } from "zod";
export const CreateRoleSchema = z.object({
body: z.object({
slug: z.string().trim(),
name: z.string().trim(),
description: z.string().trim().optional(),
workspaceId: z.string().trim().optional(),
orgId: z.string().trim(),
permissions: z
.object({
subject: z.string().trim(),
action: z.string().trim(),
conditions: z
.record(z.union([z.string().trim(), z.number(), z.object({ $glob: z.string() })]))
.optional()
})
.array()
})
});
export const UpdateRoleSchema = z.object({
params: z.object({
id: z.string().trim()
}),
body: z.object({
slug: z.string().trim().optional(),
name: z.string().trim().optional(),
description: z.string().trim().optional(),
workspaceId: z.string().trim().optional(),
orgId: z.string().trim(),
permissions: z
.object({
subject: z.string().trim(),
action: z.string().trim(),
conditions: z
.record(z.union([z.string().trim(), z.number(), z.object({ $glob: z.string() })]))
.optional()
})
.array()
.optional()
})
});
export const DeleteRoleSchema = z.object({
params: z.object({
id: z.string().trim()
})
});
export const GetRoleSchema = z.object({
query: z.object({
workspaceId: z.string().trim().optional(),
orgId: z.string().trim()
})
});
export const GetUserPermission = z.object({
params: z.object({
orgId: z.string().trim()
})
});
export const GetUserProjectPermission = z.object({
params: z.object({
workspaceId: z.string().trim()
})
});

View File

@ -14,7 +14,7 @@ import {
} from "../variables";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { InternalServerError } from "../utils/errors";
import Folder from "../models/folder";
import { Folder } from "../models";
import { getFolderByPath } from "../services/FolderService";
import { getAllImportedSecrets } from "../services/SecretImportService";
import { expandSecrets } from "./secrets";
@ -103,7 +103,10 @@ export const getSecretsBotHelper = async ({
environment: string;
secretPath: string;
}) => {
const content: Record<string, { value: string; comment?: string }> = {};
const content: Record<
string,
{ value: string; comment?: string; skipMultilineEncoding?: boolean }
> = {};
const key = await getKey({ workspaceId: workspaceId });
let folderId = "root";
@ -134,7 +137,8 @@ export const getSecretsBotHelper = async ({
const importedSecrets = await getAllImportedSecrets(
workspaceId.toString(),
environment,
folderId
folderId,
() => true // integrations are setup to read all the ones
);
importedSecrets.forEach(({ secrets }) => {
@ -164,6 +168,8 @@ export const getSecretsBotHelper = async ({
});
content[secretKey].comment = commentValue;
}
content[secretKey].skipMultilineEncoding = secret.skipMultilineEncoding;
});
});
@ -193,6 +199,8 @@ export const getSecretsBotHelper = async ({
});
content[secretKey].comment = commentValue;
}
content[secretKey].skipMultilineEncoding = secret.skipMultilineEncoding;
});
await expandSecrets(workspaceId.toString(), key, content);

View File

@ -1,21 +1,24 @@
import { Types } from "mongoose";
import { Bot, IntegrationAuth } from "../models";
import { Bot, IIntegrationAuth, IntegrationAuth } from "../models";
import { exchangeCode, exchangeRefresh } from "../integrations";
import { BotService } from "../services";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_NETLIFY,
INTEGRATION_VERCEL
INTEGRATION_VERCEL,
} from "../variables";
import { UnauthorizedRequestError } from "../utils/errors";
import { syncSecretsToActiveIntegrationsQueue } from "../queues/integrations/syncSecretsToThirdPartyServices"
import { InternalServerError, UnauthorizedRequestError } from "../utils/errors";
import { IntegrationAuthMetadata } from "../models/integrationAuth/types";
interface Update {
workspace: string;
integration: string;
url?: string;
teamId?: string;
accountId?: string;
metadata?: IntegrationAuthMetadata
}
/**
@ -34,12 +37,14 @@ export const handleOAuthExchangeHelper = async ({
workspaceId,
integration,
code,
environment
environment,
url
}: {
workspaceId: string;
integration: string;
code: string;
environment: string;
url?: string;
}) => {
const bot = await Bot.findOne({
workspace: workspaceId,
@ -51,13 +56,18 @@ export const handleOAuthExchangeHelper = async ({
// exchange code for access and refresh tokens
const res = await exchangeCode({
integration,
code
code,
url
});
const update: Update = {
workspace: workspaceId,
integration
};
if (res.url) {
update.url = res.url;
}
switch (integration) {
case INTEGRATION_VERCEL:
@ -66,6 +76,11 @@ export const handleOAuthExchangeHelper = async ({
case INTEGRATION_NETLIFY:
update.accountId = res.accountId;
break;
case INTEGRATION_GCP_SECRET_MANAGER:
update.metadata = {
authMethod: "oauth2"
}
break;
}
const integrationAuth = await IntegrationAuth.findOneAndUpdate(
@ -94,7 +109,6 @@ export const handleOAuthExchangeHelper = async ({
// set integration auth access token
await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt
});
@ -151,7 +165,7 @@ export const getIntegrationAuthAccessHelper = async ({
let accessId;
let accessToken;
const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select(
"workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag"
"workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt +refreshCiphertext +refreshIV +refreshTag +accessIdCiphertext +accessIdIV +accessIdTag metadata teamId url"
);
if (!integrationAuth)
@ -159,22 +173,24 @@ export const getIntegrationAuthAccessHelper = async ({
message: "Failed to locate Integration Authentication credentials"
});
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string
});
if (integrationAuth.accessCiphertext && integrationAuth.accessIV && integrationAuth.accessTag) {
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string
});
}
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
if (integrationAuth?.refreshCiphertext) {
// there is a access token expiration date
// and refresh token to exchange with the OAuth2 server
const refreshToken = await getIntegrationAuthRefreshHelper({
integrationAuthId
});
if (integrationAuth.accessExpiresAt < new Date()) {
if (integrationAuth?.accessExpiresAt && integrationAuth.accessExpiresAt < new Date()) {
// access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({
integrationAuthId
});
accessToken = await exchangeRefresh({
integrationAuth,
refreshToken
@ -195,7 +211,10 @@ export const getIntegrationAuthAccessHelper = async ({
});
}
if (!accessToken) throw InternalServerError();
return {
integrationAuth,
accessId,
accessToken
};
@ -215,7 +234,7 @@ export const setIntegrationAuthRefreshHelper = async ({
}: {
integrationAuthId: string;
refreshToken: string;
}) => {
}): Promise<IIntegrationAuth> => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error("Failed to find integration auth");
@ -240,6 +259,8 @@ export const setIntegrationAuthRefreshHelper = async ({
new: true
}
);
if (!integrationAuth) throw InternalServerError();
return integrationAuth;
};
@ -260,20 +281,24 @@ export const setIntegrationAuthAccessHelper = async ({
accessExpiresAt
}: {
integrationAuthId: string;
accessId: string | null;
accessToken: string;
accessId?: string;
accessToken?: string;
accessExpiresAt: Date | undefined;
}) => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error("Failed to find integration auth");
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken
});
let encryptedAccessTokenObj;
let encryptedAccessIdObj;
if (accessToken) {
encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken
});
}
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
@ -287,11 +312,11 @@ export const setIntegrationAuthAccessHelper = async ({
},
{
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessIdIV: encryptedAccessIdObj?.iv,
accessIdTag: encryptedAccessIdObj?.tag,
accessCiphertext: encryptedAccessTokenObj?.ciphertext,
accessIV: encryptedAccessTokenObj?.iv,
accessTag: encryptedAccessTokenObj?.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8

View File

@ -11,29 +11,29 @@ import { BadRequestError, MembershipNotFoundError } from "../utils/errors";
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
*/
export const validateMembership = async ({
userId,
workspaceId,
acceptedRoles,
userId,
workspaceId,
acceptedRoles
}: {
userId: Types.ObjectId | string;
workspaceId: Types.ObjectId | string;
acceptedRoles?: Array<"admin" | "member">;
acceptedRoles?: Array<"admin" | "member" | "custom" | "viewer">;
}) => {
const membership = await Membership.findOne({
user: userId,
workspace: workspaceId,
workspace: workspaceId
}).populate("workspace");
if (!membership) {
throw MembershipNotFoundError({
message: "Failed to find workspace membership",
message: "Failed to find workspace membership"
});
}
if (acceptedRoles) {
if (!acceptedRoles.includes(membership.role)) {
throw BadRequestError({
message: "Failed authorization for membership role",
message: "Failed authorization for membership role"
});
}
}
@ -47,7 +47,7 @@ export const validateMembership = async ({
* @return {Object} membership - membership
*/
export const findMembership = async (queryObj: any) => {
const membership = await Membership.findOne(queryObj);
const membership = await Membership.findOne(queryObj);
return membership;
};
@ -60,9 +60,9 @@ export const findMembership = async (queryObj: any) => {
* @param {String[]} obj.roles - roles of users.
*/
export const addMemberships = async ({
userIds,
workspaceId,
roles,
userIds,
workspaceId,
roles
}: {
userIds: string[];
workspaceId: string;
@ -74,15 +74,15 @@ export const addMemberships = async ({
filter: {
user: userId,
workspace: workspaceId,
role: roles[idx],
role: roles[idx]
},
update: {
user: userId,
workspace: workspaceId,
role: roles[idx],
role: roles[idx]
},
upsert: true,
},
upsert: true
}
};
});
await Membership.bulkWrite(operations as any);
@ -94,8 +94,8 @@ export const addMemberships = async ({
* @param {String} obj.membershipId - id of membership to delete
*/
export const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
const deletedMembership = await Membership.findOneAndDelete({
_id: membershipId,
const deletedMembership = await Membership.findOneAndDelete({
_id: membershipId
});
// delete keys associated with the membership
@ -103,9 +103,9 @@ export const deleteMembership = async ({ membershipId }: { membershipId: string
// case: membership had a registered user
await Key.deleteMany({
receiver: deletedMembership.user,
workspace: deletedMembership.workspace,
workspace: deletedMembership.workspace
});
}
return deletedMembership;
return deletedMembership;
};

View File

@ -1,14 +1,6 @@
import { Types } from "mongoose";
import {
Key,
Membership,
MembershipOrg,
Workspace,
} from "../models";
import {
MembershipOrgNotFoundError,
UnauthorizedRequestError,
} from "../utils/errors";
import { Key, Membership, MembershipOrg, Workspace } from "../models";
import { MembershipOrgNotFoundError, UnauthorizedRequestError } from "../utils/errors";
/**
* Validate that user with id [userId] is a member of organization with id [organizationId]
@ -19,39 +11,43 @@ import {
* @param {String[]} obj.acceptedRoles
*/
export const validateMembershipOrg = async ({
userId,
organizationId,
acceptedRoles,
acceptedStatuses,
userId,
organizationId,
acceptedRoles,
acceptedStatuses
}: {
userId: Types.ObjectId;
organizationId: Types.ObjectId;
acceptedRoles?: Array<"owner" | "admin" | "member">;
acceptedStatuses?: Array<"invited" | "accepted">;
userId: Types.ObjectId;
organizationId: Types.ObjectId;
acceptedRoles?: Array<"owner" | "admin" | "member" | "custom">;
acceptedStatuses?: Array<"invited" | "accepted">;
}) => {
const membershipOrg = await MembershipOrg.findOne({
user: userId,
organization: organizationId,
});
if (!membershipOrg) {
throw MembershipOrgNotFoundError({ message: "Failed to find organization membership" });
}
if (acceptedRoles) {
if (!acceptedRoles.includes(membershipOrg.role)) {
throw UnauthorizedRequestError({ message: "Failed to validate organization membership role" });
}
}
if (acceptedStatuses) {
if (!acceptedStatuses.includes(membershipOrg.status)) {
throw UnauthorizedRequestError({ message: "Failed to validate organization membership status" });
}
}
return membershipOrg;
}
const membershipOrg = await MembershipOrg.findOne({
user: userId,
organization: organizationId
});
if (!membershipOrg) {
throw MembershipOrgNotFoundError({ message: "Failed to find organization membership" });
}
if (acceptedRoles) {
if (!acceptedRoles.includes(membershipOrg.role)) {
throw UnauthorizedRequestError({
message: "Failed to validate organization membership role"
});
}
}
if (acceptedStatuses) {
if (!acceptedStatuses.includes(membershipOrg.status)) {
throw UnauthorizedRequestError({
message: "Failed to validate organization membership status"
});
}
}
return membershipOrg;
};
/**
* Return organization membership matching criteria specified in
@ -60,8 +56,8 @@ export const validateMembershipOrg = async ({
* @return {Object} membershipOrg - membership
*/
export const findMembershipOrg = (queryObj: any) => {
const membershipOrg = MembershipOrg.findOne(queryObj);
return membershipOrg;
const membershipOrg = MembershipOrg.findOne(queryObj);
return membershipOrg;
};
/**
@ -73,15 +69,15 @@ export const findMembershipOrg = (queryObj: any) => {
* @param {String[]} obj.roles - roles of users.
*/
export const addMembershipsOrg = async ({
userIds,
organizationId,
roles,
statuses,
userIds,
organizationId,
roles,
statuses
}: {
userIds: string[];
organizationId: string;
roles: string[];
statuses: string[];
userIds: string[];
organizationId: string;
roles: string[];
statuses: string[];
}) => {
const operations = userIds.map((userId, idx) => {
return {
@ -90,16 +86,16 @@ export const addMembershipsOrg = async ({
user: userId,
organization: organizationId,
role: roles[idx],
status: statuses[idx],
status: statuses[idx]
},
update: {
user: userId,
organization: organizationId,
role: roles[idx],
status: statuses[idx],
status: statuses[idx]
},
upsert: true,
},
upsert: true
}
};
});
@ -111,13 +107,9 @@ export const addMembershipsOrg = async ({
* @param {Object} obj
* @param {String} obj.membershipOrgId - id of organization membership to delete
*/
export const deleteMembershipOrg = async ({
membershipOrgId,
}: {
membershipOrgId: string;
}) => {
export const deleteMembershipOrg = async ({ membershipOrgId }: { membershipOrgId: string }) => {
const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
_id: membershipOrgId,
_id: membershipOrgId
});
if (!deletedMembershipOrg) throw new Error("Failed to delete organization membership");
@ -128,24 +120,24 @@ export const deleteMembershipOrg = async ({
const workspaces = (
await Workspace.find({
organization: deletedMembershipOrg.organization,
organization: deletedMembershipOrg.organization
})
).map((w) => w._id.toString());
await Membership.deleteMany({
user: deletedMembershipOrg.user,
workspace: {
$in: workspaces,
},
$in: workspaces
}
});
await Key.deleteMany({
receiver: deletedMembershipOrg.user,
workspace: {
$in: workspaces,
},
$in: workspaces
}
});
}
return deletedMembershipOrg;
};
return deletedMembershipOrg;
};

View File

@ -1,17 +1,22 @@
import { Types } from "mongoose";
import {
CreateSecretBatchParams,
CreateSecretParams,
DeleteSecretBatchParams,
DeleteSecretParams,
GetSecretParams,
GetSecretsParams,
UpdateSecretBatchParams,
UpdateSecretParams
} from "../interfaces/services/SecretService";
import {
Folder,
ISecret,
IServiceTokenData,
Secret,
SecretBlindIndexData,
ServiceTokenData
ServiceTokenData,
TFolderRootSchema
} from "../models";
import { EventType, SecretVersion } from "../ee/models";
import {
@ -46,7 +51,7 @@ import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/aut
import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService";
import picomatch from "picomatch";
import path from "path";
import Folder, { TFolderRootSchema } from "../models/folder";
import { getAnImportedSecret } from "../services/SecretImportService";
export const isValidScope = (
authPayload: IServiceTokenData,
@ -69,6 +74,8 @@ export function containsGlobPatterns(secretPath: string) {
return globChars.some((char) => normalizedPath.includes(char));
}
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "Folder not found" });
/**
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
*
@ -328,7 +335,8 @@ export const createSecretHelper = async ({
secretCommentIV,
secretCommentTag,
secretPath = "/",
metadata
metadata,
skipMultilineEncoding
}: CreateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
@ -392,6 +400,7 @@ export const createSecretHelper = async ({
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
skipMultilineEncoding,
folder: folderId,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
@ -414,6 +423,7 @@ export const createSecretHelper = async ({
secretValueCiphertext,
secretValueIV,
secretValueTag,
skipMultilineEncoding,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
});
@ -498,16 +508,15 @@ export const getSecretsHelper = async ({
workspaceId,
environment,
authData,
folderId,
secretPath = "/"
}: GetSecretsParams) => {
let secrets: ISecret[] = [];
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
if (!isValidScope(authData.authPayload, environment, secretPath)) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
if (!folderId) {
folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
}
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
// get personal secrets first
secrets = await Secret.find({
@ -570,17 +579,22 @@ export const getSecretsHelper = async ({
const postHogClient = await TelemetryService.getPostHogClient();
// reduce the number of events captured
let shouldRecordK8Event = false
let shouldRecordK8Event = false;
if (authData.userAgent == K8_USER_AGENT_NAME) {
const randomNumber = Math.random();
if (randomNumber > 0.9) {
shouldRecordK8Event = true
shouldRecordK8Event = true;
}
}
if (postHogClient) {
const numberOfSignupSecrets = secrets.filter(
(secret) => secret?.metadata?.source === "signup"
).length;
const atLeastOneNonSignUpSecret = secrets.length - numberOfSignupSecrets > 0;
if (postHogClient && atLeastOneNonSignUpSecret) {
const shouldCapture = authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event;
const approximateForNoneCapturedEvents = secrets.length * 10
const approximateForNoneCapturedEvents = secrets.length * 10;
if (shouldCapture) {
postHogClient.capture({
@ -617,19 +631,16 @@ export const getSecretHelper = async ({
environment,
type,
authData,
secretPath = "/"
secretPath = "/",
include_imports = true
}: GetSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
});
let secret: ISecret | null = null;
let secret: ISecret | null | undefined = null;
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
if (!isValidScope(authData.authPayload, environment, secretPath)) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
// try getting personal secret first (if exists)
@ -654,6 +665,11 @@ export const getSecretHelper = async ({
}).lean();
}
if (!secret && include_imports) {
// if still no secret found search in imported secret and retreive
secret = await getAnImportedSecret(secretName, workspaceId.toString(), environment, folderId);
}
if (!secret) throw SecretNotFoundError();
// (EE) create (audit) log
@ -732,30 +748,57 @@ export const updateSecretHelper = async ({
environment,
type,
authData,
newSecretName,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath
secretPath,
tags,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
skipMultilineEncoding
}: UpdateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
// get secret blind index salt
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const oldSecretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
secretName,
salt
});
let secret: ISecret | null = null;
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
if (!isValidScope(authData.authPayload, environment, secretPath)) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
let newSecretNameBlindIndex = undefined;
if (newSecretName) {
newSecretNameBlindIndex = await generateSecretBlindIndexWithSaltHelper({
secretName: newSecretName,
salt
});
const doesSecretAlreadyExist = await Secret.exists({
secretBlindIndex: newSecretNameBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
type
});
if (doesSecretAlreadyExist) {
throw BadRequestError({ message: "Secret with the provided name already exist" });
}
}
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
if (type === SECRET_SHARED) {
// case: update shared secret
secret = await Secret.findOneAndUpdate(
{
secretBlindIndex,
secretBlindIndex: oldSecretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
@ -765,6 +808,15 @@ export const updateSecretHelper = async ({
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex: newSecretNameBlindIndex,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
tags,
$inc: { version: 1 }
},
{
@ -776,7 +828,7 @@ export const updateSecretHelper = async ({
secret = await Secret.findOneAndUpdate(
{
secretBlindIndex,
secretBlindIndex: oldSecretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
type,
@ -787,10 +839,13 @@ export const updateSecretHelper = async ({
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
tags,
skipMultilineEncoding,
secretBlindIndex: newSecretNameBlindIndex,
$inc: { version: 1 }
},
{
new: true
}
);
}
@ -803,16 +858,18 @@ export const updateSecretHelper = async ({
workspace: secret.workspace,
folder: folderId,
type,
tags,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment,
isDeleted: false,
secretBlindIndex,
secretBlindIndex: newSecretName ? newSecretNameBlindIndex : oldSecretBlindIndex,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
skipMultilineEncoding,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
});
@ -908,12 +965,6 @@ export const deleteSecretHelper = async ({
workspaceId: new Types.ObjectId(workspaceId)
});
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
if (!isValidScope(authData.authPayload, environment, secretPath)) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
let secrets: ISecret[] = [];
@ -1102,6 +1153,7 @@ const recursivelyExpandSecret = async (
let interpolatedValue = interpolatedSec[key];
if (!interpolatedValue) {
// eslint-disable-next-line no-console
console.error(`Couldn't find referenced value - ${key}`);
return "";
}
@ -1151,7 +1203,7 @@ const formatMultiValueEnv = (val?: string) => {
export const expandSecrets = async (
workspaceId: string,
rootEncKey: string,
secrets: Record<string, { value: string; comment?: string }>
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>
) => {
const expandedSec: Record<string, string> = {};
const interpolatedSec: Record<string, string> = {};
@ -1169,7 +1221,10 @@ export const expandSecrets = async (
for (const key of Object.keys(secrets)) {
if (expandedSec?.[key]) {
secrets[key].value = formatMultiValueEnv(expandedSec[key]);
// should not do multi line encoding if user has set it to skip
secrets[key].value = secrets[key].skipMultilineEncoding
? expandedSec[key]
: formatMultiValueEnv(expandedSec[key]);
continue;
}
@ -1184,8 +1239,506 @@ export const expandSecrets = async (
key
);
secrets[key].value = formatMultiValueEnv(expandedVal);
secrets[key].value = secrets[key].skipMultilineEncoding
? expandedVal
: formatMultiValueEnv(expandedVal);
}
return secrets;
};
export const createSecretBatchHelper = async ({
secrets,
workspaceId,
authData,
secretPath,
environment
}: CreateSecretBatchParams) => {
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment
});
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
folderId = folder.id;
}
// get secret blind index salt
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
const secretBlindIndexes = await Promise.all(
secrets.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[secrets[i].secretName] = curr;
secretBlindIndexToKey[curr] = secrets[i].secretName;
return prev;
}, {})
);
const exists = await Secret.exists({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
secrets.map(({ secretName, type }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}))
)
.exec();
if (exists)
throw BadRequestError({
message: "Failed to create secret that already exists"
});
// create secret
const newlyCreatedSecrets: ISecret[] = await Secret.insertMany(
secrets.map(
({
type,
secretName,
secretKeyIV,
metadata,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding
}) => ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
environment,
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
folder: folderId,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
metadata,
skipMultilineEncoding,
secretBlindIndex: secretBlindIndexes[secretName],
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
})
)
);
await EESecretService.addSecretVersions({
secretVersions: newlyCreatedSecrets.map(
(secret) =>
new SecretVersion({
secret: secret._id,
version: secret.version,
workspace: secret.workspace,
type: secret.type,
folder: folderId,
skipMultilineEncoding: secret?.skipMultilineEncoding,
...(secret.type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment,
isDeleted: false,
secretBlindIndex: secret.secretBlindIndex,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
})
)
});
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.CREATE_SECRETS,
metadata: {
environment,
secretPath,
secrets: newlyCreatedSecrets.map(({ secretBlindIndex, version, _id }) => ({
secretId: _id.toString(),
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
secretVersion: version
}))
}
},
{
workspaceId
}
);
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "secrets added",
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: 1,
environment,
workspaceId,
folderId,
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
return newlyCreatedSecrets;
};
export const updateSecretBatchHelper = async ({
workspaceId,
environment,
authData,
secretPath,
secrets
}: UpdateSecretBatchParams) => {
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment
});
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
folderId = folder.id;
}
// get secret blind index salt
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
const secretBlindIndexes = await Promise.all(
secrets.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[secrets[i].secretName] = curr;
secretBlindIndexToKey[curr] = secrets[i].secretName;
return prev;
}, {})
);
const secretsToBeUpdated = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.select("+secretBlindIndex")
.or(
secrets.map(({ secretName, type }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}))
)
.lean();
if (secretsToBeUpdated.length !== secrets.length)
throw BadRequestError({ message: "Some secrets not found" });
await Secret.bulkWrite(
secrets.map(
({
type,
secretName,
tags,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding
}) => ({
updateOne: {
filter: {
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
secretBlindIndex: secretBlindIndexes[secretName],
type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
},
update: {
$inc: {
version: 1
},
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags,
skipMultilineEncoding
}
}
})
)
);
const secretsGroupedByBlindIndex = secretsToBeUpdated.reduce<Record<string, ISecret>>(
(prev, curr) => {
if (curr.secretBlindIndex) prev[curr.secretBlindIndex] = curr;
return prev;
},
{}
);
await EESecretService.addSecretVersions({
secretVersions: secrets.map((secret) => {
const {
_id,
version,
workspace,
type,
secretBlindIndex,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
skipMultilineEncoding
} = secretsGroupedByBlindIndex[secretBlindIndexes[secret.secretName]];
return new SecretVersion({
secret: _id,
version: version + 1,
workspace: workspace,
type,
folder: folderId,
...(secret.type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment,
isDeleted: false,
secretBlindIndex: secretBlindIndex,
secretKeyCiphertext: secretKeyCiphertext,
secretKeyIV: secretKeyIV,
secretKeyTag: secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
skipMultilineEncoding
});
})
});
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.UPDATE_SECRETS,
metadata: {
environment,
secretPath,
secrets: secretsToBeUpdated.map(({ _id, version, secretBlindIndex }) => ({
secretId: _id.toString(),
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
secretVersion: version + 1
}))
}
},
{
workspaceId
}
);
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "secrets modified",
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: 1,
environment,
workspaceId,
folderId,
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
return;
};
export const deleteSecretBatchHelper = async ({
workspaceId,
environment,
authData,
secretPath = "/",
secrets
}: DeleteSecretBatchParams) => {
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment
});
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
folderId = folder.id;
}
// get secret blind index salt
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
const secretBlindIndexes = await Promise.all(
secrets.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[secrets[i].secretName] = curr;
secretBlindIndexToKey[curr] = secrets[i].secretName;
return prev;
}, {})
);
const deletedSecrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
secrets.map(({ secretName, type }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: type === "shared" ? { $in: ["shared", "personal"] } : type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}))
)
.select({ secretBlindIndexes: 1 })
.lean()
.exec();
await Secret.deleteMany({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
secrets.map(({ secretName, type }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: type === "shared" ? { $in: ["shared", "personal"] } : type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}))
)
.exec();
await EESecretService.markDeletedSecretVersions({
secretIds: deletedSecrets.map((secret) => secret._id)
});
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.DELETE_SECRETS,
metadata: {
environment,
secretPath,
secrets: deletedSecrets.map(({ _id, version, secretBlindIndex }) => ({
secretId: _id.toString(),
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
secretVersion: version
}))
}
},
{
workspaceId
}
);
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "secrets deleted",
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
folderId,
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
return {
secrets: deletedSecrets
};
};

View File

@ -1,7 +1,7 @@
import { IUser } from "../models";
import { createOrganization } from "./organization";
import { addMembershipsOrg } from "./membershipOrg";
import { ACCEPTED, OWNER } from "../variables";
import { ACCEPTED, ADMIN } from "../variables";
import { sendMail } from "../helpers/nodemailer";
import { TokenService } from "../services";
import { TOKEN_EMAIL_CONFIRMATION } from "../variables";
@ -14,10 +14,10 @@ import { TOKEN_EMAIL_CONFIRMATION } from "../variables";
* @returns {Boolean} success - whether or not operation was successful
*/
export const sendEmailVerification = async ({ email }: { email: string }) => {
const token = await TokenService.createToken({
type: TOKEN_EMAIL_CONFIRMATION,
email,
});
const token = await TokenService.createToken({
type: TOKEN_EMAIL_CONFIRMATION,
email
});
// send mail
await sendMail({
@ -25,8 +25,8 @@ export const sendEmailVerification = async ({ email }: { email: string }) => {
subjectLine: "Infisical confirmation code",
recipients: [email],
substitutions: {
code: token,
},
code: token
}
});
};
@ -36,17 +36,11 @@ export const sendEmailVerification = async ({ email }: { email: string }) => {
* @param {String} obj.email - emai
* @param {String} obj.code - code that was sent to [email]
*/
export const checkEmailVerification = async ({
email,
code,
}: {
email: string;
code: string;
}) => {
export const checkEmailVerification = async ({ email, code }: { email: string; code: string }) => {
await TokenService.validateToken({
type: TOKEN_EMAIL_CONFIRMATION,
email,
token: code,
token: code
});
};
@ -58,27 +52,27 @@ export const checkEmailVerification = async ({
* @param {IUser} obj.user - user who we are initializing for
*/
export const initializeDefaultOrg = async ({
organizationName,
user,
organizationName,
user
}: {
organizationName: string;
user: IUser;
organizationName: string;
user: IUser;
}) => {
try {
// create organization with user as owner and initialize a free
// subscription
const organization = await createOrganization({
email: user.email,
name: organizationName,
});
try {
// create organization with user as owner and initialize a free
// subscription
const organization = await createOrganization({
email: user.email,
name: organizationName
});
await addMembershipsOrg({
userIds: [user._id.toString()],
organizationId: organization._id.toString(),
roles: [OWNER],
statuses: [ACCEPTED],
});
} catch (err) {
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
}
};
await addMembershipsOrg({
userIds: [user._id.toString()],
organizationId: organization._id.toString(),
roles: [ADMIN],
statuses: [ACCEPTED]
});
} catch (err) {
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
}
};

View File

@ -1,7 +1,4 @@
import {
IUser,
User,
} from "../models";
import { IUser, User } from "../models";
import { sendMail } from "./nodemailer";
/**
@ -12,10 +9,10 @@ import { sendMail } from "./nodemailer";
*/
export const setupAccount = async ({ email }: { email: string }) => {
const user = await new User({
email,
email
}).save();
return user;
return user;
};
/**
@ -37,36 +34,36 @@ export const setupAccount = async ({ email }: { email: string }) => {
* @returns {Object} user - the completed user
*/
export const completeAccount = async ({
userId,
firstName,
lastName,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
userId,
firstName,
lastName,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
}: {
userId: string;
firstName: string;
lastName: string;
encryptionVersion: number;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
userId: string;
firstName: string;
lastName?: string;
encryptionVersion: number;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
}) => {
const options = {
new: true,
new: true
};
const user = await User.findByIdAndUpdate(
userId,
@ -82,12 +79,12 @@ export const completeAccount = async ({
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
verifier
},
options
);
return user;
return user;
};
/**
@ -98,38 +95,42 @@ export const completeAccount = async ({
* @param {String} obj.userAgent - login user-agent
*/
export const checkUserDevice = async ({
user,
ip,
userAgent,
user,
ip,
userAgent
}: {
user: IUser;
ip: string;
userAgent: string;
user: IUser;
ip: string;
userAgent: string;
}) => {
const isDeviceSeen = user.devices.some((device) => device.ip === ip && device.userAgent === userAgent);
if (!isDeviceSeen) {
// case: unseen login ip detected for user
// -> notify user about the sign-in from new ip
user.devices = user.devices.concat([{
ip: String(ip),
userAgent,
}]);
await user.save();
const isDeviceSeen = user.devices.some(
(device) => device.ip === ip && device.userAgent === userAgent
);
// send MFA code [code] to [email]
await sendMail({
template: "newDevice.handlebars",
subjectLine: "Successful login from new device",
recipients: [user.email],
substitutions: {
email: user.email,
timestamp: new Date().toString(),
ip,
userAgent,
},
});
}
}
if (!isDeviceSeen) {
// case: unseen login ip detected for user
// -> notify user about the sign-in from new ip
user.devices = user.devices.concat([
{
ip: String(ip),
userAgent
}
]);
await user.save();
// send MFA code [code] to [email]
await sendMail({
template: "newDevice.handlebars",
subjectLine: "Successful login from new device",
recipients: [user.email],
substitutions: {
email: user.email,
timestamp: new Date().toString(),
ip,
userAgent
}
});
}
};

View File

@ -0,0 +1,17 @@
import type { Request } from "express";
import { AnyZodObject, ZodError, z } from "zod";
import { BadRequestError } from "../utils/errors";
export async function validateRequest<T extends AnyZodObject>(
schema: T,
req: Request
): Promise<z.infer<T>> {
try {
return schema.parseAsync(req);
} catch (error) {
if (error instanceof ZodError) {
throw BadRequestError({ message: error.message });
}
return BadRequestError({ message: JSON.stringify(error) });
}
}

View File

@ -24,7 +24,8 @@ import {
secretSnapshot as eeSecretSnapshotRouter,
users as eeUsersRouter,
workspace as eeWorkspaceRouter,
secretScanning as v1SecretScanningRouter,
roles as v1RoleRouter,
secretScanning as v1SecretScanningRouter
} from "./ee/routes/v1";
import {
auth as v1AuthRouter,
@ -37,7 +38,8 @@ import {
membership as v1MembershipRouter,
organization as v1OrganizationRouter,
password as v1PasswordRouter,
secretImport as v1SecretImportRouter,
secretApprovalPolicy as v1SecretApprovalPolicy,
secretImps as v1SecretImpsRouter,
secret as v1SecretRouter,
secretsFolder as v1SecretsFolder,
serviceToken as v1ServiceTokenRouter,
@ -58,7 +60,7 @@ import {
signup as v2SignupRouter,
tags as v2TagsRouter,
users as v2UsersRouter,
workspace as v2WorkspaceRouter,
workspace as v2WorkspaceRouter
} from "./routes/v2";
import {
auth as v3AuthRouter,
@ -70,14 +72,21 @@ import { healthCheck } from "./routes/status";
import { getLogger } from "./utils/logger";
import { RouteNotFoundError } from "./utils/errors";
import { requestErrorHandler } from "./middleware/requestErrorHandler";
import { getNodeEnv, getPort, getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookProxy, getSecretScanningWebhookSecret, getSiteURL } from "./config";
import {
getNodeEnv,
getPort,
getSecretScanningGitAppId,
getSecretScanningPrivateKey,
getSecretScanningWebhookProxy,
getSecretScanningWebhookSecret,
getSiteURL
} from "./config";
import { setup } from "./utils/setup";
import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices";
import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent";
const SmeeClient = require('smee-client') // eslint-disable-line
const SmeeClient = require("smee-client"); // eslint-disable-line
const main = async () => {
await setup();
await EELicenseService.initGlobalFeatureSet();
@ -94,11 +103,15 @@ const main = async () => {
})
);
if (await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey()) {
if (
(await getSecretScanningGitAppId()) &&
(await getSecretScanningWebhookSecret()) &&
(await getSecretScanningPrivateKey())
) {
const probot = new Probot({
appId: await getSecretScanningGitAppId(),
privateKey: await getSecretScanningPrivateKey(),
secret: await getSecretScanningWebhookSecret(),
secret: await getSecretScanningWebhookSecret()
});
if ((await getNodeEnv()) != "production") {
@ -106,12 +119,14 @@ const main = async () => {
source: await getSecretScanningWebhookProxy(),
target: "http://backend:4000/ss-webhook",
logger: console
})
});
smee.start()
smee.start();
}
app.use(createNodeMiddleware(GithubSecretScanningService, { probot, webhooksPath: "/ss-webhook" })); // secret scanning webhook
app.use(
createNodeMiddleware(GithubSecretScanningService, { probot, webhooksPath: "/ss-webhook" })
); // secret scanning webhook
}
if ((await getNodeEnv()) === "production") {
@ -124,6 +139,7 @@ const main = async () => {
app.use((req, res, next) => {
// default to IP address provided by Cloudflare
// #swagger.ignore = true
const cfIp = req.headers["cf-connecting-ip"];
req.realIP = Array.isArray(cfIp) ? cfIp[0] : (cfIp as string) || req.ip;
next();
@ -139,7 +155,7 @@ const main = async () => {
app.use("/api/v1/sso", eeSSORouter);
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
// v1 routes (default)
// v1 routes
app.use("/api/v1/signup", v1SignupRouter);
app.use("/api/v1/auth", v1AuthRouter);
app.use("/api/v1/bot", v1BotRouter);
@ -148,7 +164,7 @@ const main = async () => {
app.use("/api/v1/organization", v1OrganizationRouter);
app.use("/api/v1/workspace", v1WorkspaceRouter);
app.use("/api/v1/membership-org", v1MembershipOrgRouter);
app.use("/api/v1/membership", v1MembershipRouter);
app.use("/api/v1/membership", v1MembershipRouter); //
app.use("/api/v1/key", v1KeyRouter);
app.use("/api/v1/invite-org", v1InviteOrgRouter);
app.use("/api/v1/secret", v1SecretRouter); // deprecate
@ -159,7 +175,9 @@ const main = async () => {
app.use("/api/v1/folders", v1SecretsFolder);
app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
app.use("/api/v1/webhooks", v1WebhooksRouter);
app.use("/api/v1/secret-imports", v1SecretImportRouter);
app.use("/api/v1/secret-imports", v1SecretImpsRouter);
app.use("/api/v1/roles", v1RoleRouter);
app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy);
// v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter);
@ -205,10 +223,25 @@ const main = async () => {
// await createTestUserForDevelopment();
setUpHealthEndpoint(server);
server.on("close", async () => {
const serverCleanup = async () => {
await DatabaseService.closeDatabase();
syncSecretsToThirdPartyServices.close()
githubPushEventSecretScan.close()
syncSecretsToThirdPartyServices.close();
githubPushEventSecretScan.close();
process.exit(0);
}
process.on("SIGINT", function () {
server.close(async () => {
await serverCleanup()
});
});
process.on("SIGTERM", function () {
server.close(async () => {
await serverCleanup()
});
});
return server;

View File

@ -120,6 +120,7 @@ const getApps = async ({
break;
case INTEGRATION_GITLAB:
apps = await getAppsGitlab({
integrationAuth,
accessToken,
teamId,
});
@ -607,6 +608,12 @@ const getAppsLaravelForge = async ({
* @returns {String} apps.name - name of Fly.io apps
*/
const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
interface FlyioApp {
id: string;
name: string;
hostname: string;
}
const query = `
query($role: String) {
apps(type: "container", first: 400, role: $role) {
@ -619,7 +626,7 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
}
`;
const res = (
const res: FlyioApp[] = (
await standardRequest.post(
INTEGRATION_FLYIO_API_URL,
{
@ -638,8 +645,9 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
)
).data.data.apps.nodes;
const apps = res.map((a: any) => ({
const apps = res.map((a: FlyioApp) => ({
name: a.name,
appId: a.id
}));
return apps;
@ -736,12 +744,16 @@ const getAppsTerraformCloud = async ({
* @returns {String} apps.name - name of GitLab site
*/
const getAppsGitlab = async ({
integrationAuth,
accessToken,
teamId,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
teamId?: string;
}) => {
const gitLabApiUrl = integrationAuth.url ? `${integrationAuth.url}/api` : INTEGRATION_GITLAB_API_URL;
const apps: App[] = [];
let page = 1;
@ -758,7 +770,7 @@ const getAppsGitlab = async ({
});
const { data } = await standardRequest.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`,
`${gitLabApiUrl}/v4/groups/${teamId}/projects`,
{
params,
headers: {
@ -785,7 +797,7 @@ const getAppsGitlab = async ({
// case: fetch projects for individual in GitLab
const { id } = (
await standardRequest.get(`${INTEGRATION_GITLAB_API_URL}/v4/user`, {
await standardRequest.get(`${gitLabApiUrl}/v4/user`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
@ -800,7 +812,7 @@ const getAppsGitlab = async ({
});
const { data } = await standardRequest.get(
`${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`,
`${gitLabApiUrl}/v4/users/${id}/projects`,
{
params,
headers: {
@ -850,7 +862,7 @@ const getAppsTeamCity = async ({
},
})
).data.project.slice(1);
const apps = res.map((a: any) => {
return {
name: a.name,

View File

@ -4,6 +4,8 @@ import {
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GCP_TOKEN_URL,
INTEGRATION_GITHUB,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB,
@ -13,21 +15,19 @@ import {
INTEGRATION_NETLIFY,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_VERCEL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GCP_TOKEN_URL
INTEGRATION_VERCEL_TOKEN_URL
} from "../variables";
import {
getClientIdGCPSecretManager,
getClientSecretGCPSecretManager,
getClientIdAzure,
getClientIdBitBucket,
getClientIdGCPSecretManager,
getClientIdGitHub,
getClientIdGitLab,
getClientIdNetlify,
getClientIdVercel,
getClientSecretAzure,
getClientSecretBitBucket,
getClientSecretGCPSecretManager,
getClientSecretGitHub,
getClientSecretGitLab,
getClientSecretHeroku,
@ -46,6 +46,14 @@ interface ExchangeCodeAzureResponse {
id_token: string;
}
interface ExchangeCodeGCPResponse {
access_token: string;
expires_in: number;
refresh_token: string;
scope: string;
token_type: string;
}
interface ExchangeCodeHerokuResponse {
token_type: string;
access_token: string;
@ -110,9 +118,11 @@ interface ExchangeCodeBitBucketResponse {
const exchangeCode = async ({
integration,
code,
url
}: {
integration: string;
code: string;
url?: string;
}) => {
let obj = {} as any;
@ -150,6 +160,7 @@ const exchangeCode = async ({
case INTEGRATION_GITLAB:
obj = await exchangeCodeGitlab({
code,
url
});
break;
case INTEGRATION_BITBUCKET:
@ -174,7 +185,7 @@ const exchangeCode = async ({
const exchangeCodeGCP = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
const res: ExchangeCodeAzureResponse = (
const res: ExchangeCodeGCPResponse = (
await standardRequest.post(
INTEGRATION_GCP_TOKEN_URL,
new URLSearchParams({
@ -380,11 +391,17 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
* @returns {String} obj2.refreshToken - refresh token for Gitlab API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeGitlab = async ({ code }: { code: string }) => {
const exchangeCodeGitlab = async ({
code,
url
}: {
code: string,
url?: string;
}) => {
const accessExpiresAt = new Date();
const res: ExchangeCodeGitlabResponse = (
await standardRequest.post(
INTEGRATION_GITLAB_TOKEN_URL,
url ? `${url}/oauth/token` : INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code: code,
@ -406,6 +423,7 @@ const exchangeCodeGitlab = async ({ code }: { code: string }) => {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt,
url
};
};

View File

@ -1,11 +1,15 @@
import jwt from "jsonwebtoken";
import { standardRequest } from "../config/request";
import { IIntegrationAuth } from "../models";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GCP_TOKEN_URL,
INTEGRATION_GITLAB,
INTEGRATION_HEROKU,
INTEGRATION_HEROKU
} from "../variables";
import {
INTEGRATION_AZURE_TOKEN_URL,
@ -16,9 +20,11 @@ import { IntegrationService } from "../services";
import {
getClientIdAzure,
getClientIdBitBucket,
getClientIdGCPSecretManager,
getClientIdGitLab,
getClientSecretAzure,
getClientSecretBitBucket,
getClientSecretGCPSecretManager,
getClientSecretGitLab,
getClientSecretHeroku,
getSiteURL,
@ -59,6 +65,19 @@ interface RefreshTokenBitBucketResponse {
state: string;
}
interface ServiceAccountAccessTokenGCPSecretManagerResponse {
access_token: string;
expires_in: number;
token_type: string;
}
interface RefreshTokenGCPSecretManagerResponse {
access_token: string;
expires_in: number;
scope: string;
token_type: string;
}
/**
* Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration]
@ -93,6 +112,7 @@ const exchangeRefresh = async ({
break;
case INTEGRATION_GITLAB:
tokenDetails = await exchangeRefreshGitLab({
integrationAuth,
refreshToken,
});
break;
@ -101,18 +121,23 @@ const exchangeRefresh = async ({
refreshToken,
});
break;
case INTEGRATION_GCP_SECRET_MANAGER:
tokenDetails = await exchangeRefreshGCPSecretManager({
integrationAuth,
refreshToken,
});
break;
default:
throw new Error("Failed to exchange token for incompatible integration");
}
if (
tokenDetails?.accessToken &&
tokenDetails?.refreshToken &&
tokenDetails?.accessExpiresAt
tokenDetails.accessToken &&
tokenDetails.refreshToken &&
tokenDetails.accessExpiresAt
) {
await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: tokenDetails.accessToken,
accessExpiresAt: tokenDetails.accessExpiresAt,
});
@ -202,17 +227,21 @@ const exchangeRefreshHeroku = async ({
* @returns
*/
const exchangeRefreshGitLab = async ({
integrationAuth,
refreshToken,
}: {
integrationAuth: IIntegrationAuth;
refreshToken: string;
}) => {
const accessExpiresAt = new Date();
const url = integrationAuth.url;
const {
data,
}: {
data: RefreshTokenGitLabResponse;
} = await standardRequest.post(
INTEGRATION_GITLAB_TOKEN_URL,
url ? `${url}/oauth/token` : INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
@ -278,4 +307,76 @@ const exchangeRefreshBitBucket = async ({
};
};
export { exchangeRefresh };
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* GCP Secret Manager integration
* @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for GCP Secret Manager
* @returns
*/
const exchangeRefreshGCPSecretManager = async ({
integrationAuth,
refreshToken,
}: {
integrationAuth: IIntegrationAuth;
refreshToken: string;
}) => {
const accessExpiresAt = new Date();
if (integrationAuth.metadata?.authMethod === "serviceAccount") {
const serviceAccount = JSON.parse(refreshToken);
const payload = {
iss: serviceAccount.client_email,
aud: serviceAccount.token_uri,
scope: INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600,
};
const token = jwt.sign(payload, serviceAccount.private_key, { algorithm: "RS256" });
const { data }: { data: ServiceAccountAccessTokenGCPSecretManagerResponse } = await standardRequest.post(
INTEGRATION_GCP_TOKEN_URL,
new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: token
}).toString(),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
}
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken,
accessExpiresAt
};
}
const { data }: { data: RefreshTokenGCPSecretManagerResponse } = (
await standardRequest.post(
INTEGRATION_GCP_TOKEN_URL,
new URLSearchParams({
client_id: await getClientIdGCPSecretManager(),
client_secret: await getClientSecretGCPSecretManager(),
refresh_token: refreshToken,
grant_type: "refresh_token",
} as any)
)
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken,
accessExpiresAt,
};
};
export { exchangeRefresh };

View File

@ -40,6 +40,8 @@ import {
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_NORTHFLANK,
INTEGRATION_NORTHFLANK_API_URL,
INTEGRATION_QOVERY,
INTEGRATION_QOVERY_API_URL,
INTEGRATION_RAILWAY,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_RENDER,
@ -63,10 +65,10 @@ import sodium from "libsodium-wrappers";
import { standardRequest } from "../config/request";
const getSecretKeyValuePair = (
secrets: Record<string, { value: string; comment?: string } | null>
secrets: Record<string, { value: string | null; comment?: string } | null>
) =>
Object.keys(secrets).reduce<Record<string, string>>((prev, key) => {
if (secrets[key]) prev[key] = secrets[key]?.value || "";
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
prev[key] = secrets?.[key] === null ? null : secrets?.[key]?.value;
return prev;
}, {});
@ -156,6 +158,7 @@ const syncSecrets = async ({
break;
case INTEGRATION_GITLAB:
await syncSecretsGitLab({
integrationAuth,
integration,
secrets,
accessToken
@ -218,6 +221,13 @@ const syncSecrets = async ({
accessToken
});
break;
case INTEGRATION_QOVERY:
await syncSecretsQovery({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_TERRAFORM_CLOUD:
await syncSecretsTerraformCloud({
integration,
@ -315,80 +325,118 @@ const syncSecretsGCPSecretManager = async ({
name: string;
createTime: string;
}
interface GCPSMListSecretsRes {
secrets: GCPSecret[];
totalSize: number;
secrets?: GCPSecret[];
totalSize?: number;
nextPageToken?: string;
}
let gcpSecrets: GCPSecret[] = [];
const pageSize = 100;
let pageToken: string | undefined;
let hasMorePages = true;
const filterParam = integration.metadata.secretGCPLabel
? `?filter=labels.${integration.metadata.secretGCPLabel.labelName}=${integration.metadata.secretGCPLabel.labelValue}`
: "";
while (hasMorePages) {
const params = new URLSearchParams({
pageSize: String(pageSize),
...(pageToken ? { pageToken } : {})
});
const res: GCPSMListSecretsRes = (await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets`,
{
params,
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
const res: GCPSMListSecretsRes = (
await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
}
)).data;
gcpSecrets = gcpSecrets.concat(res.secrets);
)
).data;
if (res.secrets) {
const filteredSecrets = res.secrets?.filter((gcpSecret) => {
const arr = gcpSecret.name.split("/");
const key = arr[arr.length - 1];
let isValid = true;
if (
integration.metadata.secretPrefix &&
!key.startsWith(integration.metadata.secretPrefix)
) {
isValid = false;
}
if (integration.metadata.secretSuffix && !key.endsWith(integration.metadata.secretSuffix)) {
isValid = false;
}
return isValid;
});
gcpSecrets = gcpSecrets.concat(filteredSecrets);
}
if (!res.nextPageToken) {
hasMorePages = false;
}
pageToken = res.nextPageToken;
}
const res: { [key: string]: string; } = {};
const res: { [key: string]: string } = {};
interface GCPLatestSecretVersionAccess {
name: string;
payload: {
data: string;
}
};
}
for await (const gcpSecret of gcpSecrets) {
const arr = gcpSecret.name.split("/");
const key = arr[arr.length - 1];
const secretLatest: GCPLatestSecretVersionAccess = (await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
const secretLatest: GCPLatestSecretVersionAccess = (
await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
}
)).data;
)
).data;
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
}
for await (const key of Object.keys(secrets)) {
if (!(key in res)) {
// case: create secret
await standardRequest.post(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets`,
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets`,
{
replication: {
automatic: {}
}
},
...(integration.metadata.secretGCPLabel
? {
labels: {
[integration.metadata.secretGCPLabel.labelName]:
integration.metadata.secretGCPLabel.labelValue
}
}
: {})
},
{
params: {
@ -400,9 +448,9 @@ const syncSecretsGCPSecretManager = async ({
}
}
);
await standardRequest.post(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}:addVersion`,
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
{
payload: {
data: Buffer.from(secrets[key].value).toString("base64")
@ -417,12 +465,12 @@ const syncSecretsGCPSecretManager = async ({
);
}
}
for await (const key of Object.keys(res)) {
if (!(key in secrets)) {
// case: delete secret
await standardRequest.delete(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}`,
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
@ -434,7 +482,7 @@ const syncSecretsGCPSecretManager = async ({
// case: update secret
if (secrets[key].value !== res[key]) {
await standardRequest.post(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}:addVersion`,
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
{
payload: {
data: Buffer.from(secrets[key].value).toString("base64")
@ -450,7 +498,7 @@ const syncSecretsGCPSecretManager = async ({
}
}
}
}
};
/**
* Sync/push [secrets] to Azure Key Vault with vault URI [integration.app]
@ -686,17 +734,16 @@ const syncSecretsAWSParameterStore = async ({
const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters;
let awsParameterStoreSecretsObj: {
[key: string]: any; // TODO: fix type
[key: string]: any;
} = {};
if (parameterList) {
awsParameterStoreSecretsObj = parameterList.reduce(
(obj: any, secret: any) => ({
awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => {
return {
...obj,
[secret.Name.split("/").pop()]: secret
}),
{}
);
[secret.Name.substring(integration.path.length)]: secret
};
}, {});
}
// Identify secrets to create
@ -751,12 +798,12 @@ const syncSecretsAWSParameterStore = async ({
};
/**
* Sync/push [secrets] to AWS secret manager
* Sync/push [secrets] to AWS Secrets Manager
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessId - access id for AWS secret manager integration
* @param {String} obj.accessToken - access token for AWS secret manager integration
* @param {String} obj.accessId - access id for AWS Secrets Manager integration
* @param {String} obj.accessToken - access token for AWS Secrets Manager integration
*/
const syncSecretsAWSSecretManager = async ({
integration,
@ -909,7 +956,11 @@ const syncSecretsVercel = async ({
? {
teamId: integrationAuth.teamId
}
: {})
: {}),
...(integration?.path
? {
gitBranch: integration?.path
} : {})
};
const vercelSecrets: VercelSecret[] = (
@ -928,7 +979,7 @@ const syncSecretsVercel = async ({
if (
integration.targetEnvironment === "preview" &&
integration.path &&
secret.gitBranch &&
integration.path !== secret.gitBranch
) {
// case: secret on preview environment does not have same target git branch
@ -937,7 +988,7 @@ const syncSecretsVercel = async ({
return true;
});
const res: { [key: string]: VercelSecret } = {};
for await (const vercelSecret of vercelSecrets) {
@ -1809,10 +1860,12 @@ const syncSecretsTravisCI = async ({
* @param {String} obj.accessToken - access token for GitLab integration
*/
const syncSecretsGitLab = async ({
integrationAuth,
integration,
secrets,
accessToken
}: {
integrationAuth: IIntegrationAuth;
integration: IIntegration;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
@ -1823,8 +1876,11 @@ const syncSecretsGitLab = async ({
environment_scope: string;
}
const gitLabApiUrl = integrationAuth.url
? `${integrationAuth.url}/api`
: INTEGRATION_GITLAB_API_URL;
const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => {
const gitLabApiUrl = `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integrationAppId}/variables`;
const headers = {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
@ -1832,7 +1888,9 @@ const syncSecretsGitLab = async ({
};
let allEnvVariables: GitLabSecret[] = [];
let url: string | null = `${gitLabApiUrl}?per_page=100`;
let url:
| string
| null = `${gitLabApiUrl}/v4/projects/${integrationAppId}/variables?per_page=100`;
while (url) {
const response: any = await standardRequest.get(url, { headers });
@ -1852,15 +1910,33 @@ const syncSecretsGitLab = async ({
};
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables.filter(
(secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment
);
const getSecretsRes: GitLabSecret[] = allEnvVariables
.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
.filter((gitLabSecret) => {
let isValid = true;
if (
integration.metadata.secretPrefix &&
!gitLabSecret.key.startsWith(integration.metadata.secretPrefix)
) {
isValid = false;
}
if (
integration.metadata.secretSuffix &&
!gitLabSecret.key.endsWith(integration.metadata.secretSuffix)
) {
isValid = false;
}
return isValid;
});
for await (const key of Object.keys(secrets)) {
const existingSecret = getSecretsRes.find((s: any) => s.key == key);
if (!existingSecret) {
await standardRequest.post(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`,
`${gitLabApiUrl}/v4/projects/${integration?.appId}/variables`,
{
key: key,
value: secrets[key].value,
@ -1881,7 +1957,7 @@ const syncSecretsGitLab = async ({
// update secret
if (secrets[key].value !== existingSecret.value) {
await standardRequest.put(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
`${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
{
...existingSecret,
value: secrets[existingSecret.key].value
@ -1902,7 +1978,7 @@ const syncSecretsGitLab = async ({
for await (const sec of getSecretsRes) {
if (!(sec.key in secrets)) {
await standardRequest.delete(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}?filter[environment_scope]=${integration.targetEnvironment}`,
`${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${sec.key}?filter[environment_scope]=${integration.targetEnvironment}`,
{
headers: {
Authorization: `Bearer ${accessToken}`
@ -2004,7 +2080,6 @@ const syncSecretsCheckly = async ({
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
// get secrets from travis-ci
const getSecretsRes = (
await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, {
headers: {
@ -2026,7 +2101,6 @@ const syncSecretsCheckly = async ({
if (!(key in getSecretsRes)) {
// case: secret does not exist in checkly
// -> add secret
await standardRequest.post(
`${INTEGRATION_CHECKLY_API_URL}/v1/variables`,
{
@ -2079,6 +2153,97 @@ const syncSecretsCheckly = async ({
}
};
/**
* Sync/push [secrets] to Qovery app
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Qovery integration
*/
const syncSecretsQovery = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
const getSecretsRes = (
await standardRequest.get(`${INTEGRATION_QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable`, {
headers: {
Authorization: `Token ${accessToken}`,
"Accept-Encoding": "application/json"
}
})
).data.results.reduce(
(obj: any, secret: any) => ({
...obj,
[secret.key]: {"id": secret.id, "value": secret.value}
}),
{}
);
// add secrets
for await (const key of Object.keys(secrets)) {
if (!(key in getSecretsRes)) {
// case: secret does not exist in qovery
// -> add secret
await standardRequest.post(
`${INTEGRATION_QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable`,
{
key,
value: secrets[key].value
},
{
headers: {
Authorization: `Token ${accessToken}`,
Accept: "application/json",
"Content-Type": "application/json"
}
}
);
} else {
// case: secret exists in qovery
// -> update/set secret
if (secrets[key].value !== getSecretsRes[key].value) {
await standardRequest.put(
`${INTEGRATION_QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable/${getSecretsRes[key].id}`,
{
key,
value: secrets[key].value
},
{
headers: {
Authorization: `Token ${accessToken}`,
"Content-Type": "application/json",
Accept: "application/json"
}
}
);
}
}
}
// This one is dangerous because there might be a lot of qovery-specific secrets
// for await (const key of Object.keys(getSecretsRes)) {
// if (!(key in secrets)) {
// console.log(3)
// // delete secret
// await standardRequest.delete(`${INTEGRATION_QOVERY_API_URL}/application/${integration.appId}/environmentVariable/${getSecretsRes[key].id}`, {
// headers: {
// Authorization: `Token ${accessToken}`,
// Accept: "application/json",
// "X-Qovery-Account": integration.appId
// }
// });
// }
// }
};
/**
* Sync/push [secrets] to Terraform Cloud project with id [integration.appId]
* @param {Object} obj
@ -2185,7 +2350,7 @@ const syncSecretsTerraformCloud = async ({
};
/**
* Sync/push [secrets] to TeamCity project
* Sync/push [secrets] to TeamCity project (and optionally build config)
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration
@ -2207,57 +2372,126 @@ const syncSecretsTeamCity = async ({
value: string;
}
// get secrets from Teamcity
const res = (
await standardRequest.get(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
)
).data.property.reduce((obj: any, secret: TeamCitySecret) => {
const secretName = secret.name.replace(/^env\./, "");
return {
...obj,
[secretName]: secret.value
};
}, {});
for await (const key of Object.keys(secrets)) {
if (!(key in res) || (key in res && secrets[key] !== res[key])) {
// case: secret does not exist in TeamCity or secret value has changed
// -> create/update secret
await standardRequest.post(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`,
{
name: `env.${key}`,
value: secrets[key]
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
interface TeamCityBuildConfigParameter {
name: string;
value: string;
inherited: boolean;
}
interface GetTeamCityBuildConfigParametersRes {
href: string;
count: number;
property: TeamCityBuildConfigParameter[];
}
for await (const key of Object.keys(res)) {
if (!(key in secrets)) {
// delete secret
await standardRequest.delete(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters/env.${key}`,
if (integration.targetEnvironment && integration.targetEnvironmentId) {
// case: sync to specific build-config in TeamCity project
const res = (
await standardRequest.get<GetTeamCityBuildConfigParametersRes>(
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
)
).data.property
.filter((parameter) => !parameter.inherited)
.reduce((obj: any, secret: TeamCitySecret) => {
const secretName = secret.name.replace(/^env\./, "");
return {
...obj,
[secretName]: secret.value
};
}, {});
for await (const key of Object.keys(secrets)) {
if (!(key in res) || (key in res && secrets[key].value !== res[key])) {
// case: secret does not exist in TeamCity or secret value has changed
// -> create/update secret
await standardRequest.post(
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{
name: `env.${key}`,
value: secrets[key].value
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
}
for await (const key of Object.keys(res)) {
if (!(key in secrets)) {
// delete secret
await standardRequest.delete(
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters/env.${key}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
}
} else {
// case: sync to TeamCity project
const res = (
await standardRequest.get(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
)
).data.property.reduce((obj: any, secret: TeamCitySecret) => {
const secretName = secret.name.replace(/^env\./, "");
return {
...obj,
[secretName]: secret.value
};
}, {});
for await (const key of Object.keys(secrets)) {
if (!(key in res) || (key in res && secrets[key] !== res[key])) {
// case: secret does not exist in TeamCity or secret value has changed
// -> create/update secret
await standardRequest.post(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`,
{
name: `env.${key}`,
value: secrets[key].value
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
}
for await (const key of Object.keys(res)) {
if (!(key in secrets)) {
// delete secret
await standardRequest.delete(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters/env.${key}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
}
}
};

View File

@ -34,6 +34,7 @@ const getTeams = async ({
switch (integrationAuth.integration) {
case INTEGRATION_GITLAB:
teams = await getTeamsGitLab({
integrationAuth,
accessToken,
});
break;
@ -51,13 +52,17 @@ const getTeams = async ({
* @returns {String} teams.teamId - id of team
*/
const getTeamsGitLab = async ({
integrationAuth,
accessToken,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
const gitLabApiUrl = integrationAuth.url ? `${integrationAuth.url}/api` : INTEGRATION_GITLAB_API_URL;
let teams: Team[] = [];
const res = (await standardRequest.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups`,
`${gitLabApiUrl}/v4/groups`,
{
headers: {
Authorization: `Bearer ${accessToken}`,

View File

@ -16,15 +16,17 @@ export interface CreateSecretParams {
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
secretPath: string;
metadata?: {
source?: string;
}
};
}
export interface GetSecretsParams {
workspaceId: Types.ObjectId;
environment: string;
folderId?: string;
secretPath: string;
authData: AuthData;
}
@ -36,10 +38,15 @@ export interface GetSecretParams {
environment: string;
type?: "shared" | "personal";
authData: AuthData;
include_imports?: boolean;
}
export interface UpdateSecretParams {
secretName: string;
newSecretName?: string;
secretKeyCiphertext?: string;
secretKeyIV?: string;
secretKeyTag?: string;
workspaceId: Types.ObjectId;
environment: string;
type: "shared" | "personal";
@ -48,6 +55,11 @@ export interface UpdateSecretParams {
secretValueIV: string;
secretValueTag: string;
secretPath: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
tags?: string[];
}
export interface DeleteSecretParams {
@ -58,3 +70,57 @@ export interface DeleteSecretParams {
authData: AuthData;
secretPath: string;
}
export interface CreateSecretBatchParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
secretPath: string;
secrets: Array<{
secretName: string;
type: "shared" | "personal";
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
metadata?: {
source?: string;
};
}>;
}
export interface UpdateSecretBatchParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
secretPath: string;
secrets: Array<{
secretName: string;
type: "shared" | "personal";
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
tags?: string[];
}>;
}
export interface DeleteSecretBatchParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
secretPath: string;
secrets: Array<{
secretName: string;
type: "shared" | "personal";
}>;
}

View File

@ -14,6 +14,9 @@ import requireServiceAccountAuth from "./requireServiceAccountAuth";
import requireServiceAccountWorkspacePermissionAuth from "./requireServiceAccountWorkspacePermissionAuth";
import requireSecretAuth from "./requireSecretAuth";
import requireSecretsAuth from "./requireSecretsAuth";
import requireBlindIndicesEnabled from "./requireBlindIndicesEnabled";
import requireE2EEOff from "./requireE2EEOff";
import requireIPAllowlistCheck from "./requireIPAllowlistCheck";
import validateRequest from "./validateRequest";
export {
@ -33,5 +36,8 @@ export {
requireServiceAccountWorkspacePermissionAuth,
requireSecretAuth,
requireSecretsAuth,
requireBlindIndicesEnabled,
requireE2EEOff,
requireIPAllowlistCheck,
validateRequest,
};

View File

@ -0,0 +1,34 @@
import { NextFunction, Request, Response } from "express";
import { Types } from "mongoose";
import { SecretBlindIndexData } from "../models";
import { UnauthorizedRequestError } from "../utils/errors";
type req = "params" | "body" | "query";
/**
* Validate if workspace with [workspaceId] has blind indices enabled
* @param {Object} obj
* @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing
* @returns
*/
const requireBlindIndicesEnabled = ({
locationWorkspaceId
}: {
locationWorkspaceId: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const workspaceId = req[locationWorkspaceId]?.workspaceId;
const secretBlindIndexData = await SecretBlindIndexData.exists({
workspace: new Types.ObjectId(workspaceId)
});
if (!secretBlindIndexData) throw UnauthorizedRequestError({
message: "Failed workspace authorization due to blind indices not being enabled"
});
return next();
}
}
export default requireBlindIndicesEnabled;

View File

@ -0,0 +1,31 @@
import { NextFunction, Request, Response } from "express";
import { BadRequestError } from "../utils/errors";
import { BotService } from "../services";
type req = "params" | "body" | "query";
/**
* Validate if workspace with [workspaceId] has E2EE off/disabled
* @param {Object} obj
* @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing
* @returns
*/
const requireE2EEOff = ({
locationWorkspaceId
}: {
locationWorkspaceId: req;
}) => {
return async (req: Request, _: Response, next: NextFunction) => {
const workspaceId = req[locationWorkspaceId]?.workspaceId;
const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId);
if (isWorkspaceE2EE) throw BadRequestError({
message: "Failed workspace authorization due to end-to-end encryption not being disabled"
});
return next();
}
}
export default requireE2EEOff;

View File

@ -0,0 +1,55 @@
import net from "net";
import { NextFunction, Request, Response } from "express";
import { UnauthorizedRequestError } from "../utils/errors";
import { extractIPDetails } from "../utils/ip";
import { ActorType, TrustedIP } from "../ee/models";
type req = "params" | "body" | "query";
/**
* Validate if workspace with [workspaceId] has E2EE off/disabled
* @param {Object} obj
* @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing
* @returns
*/
const requireIPAllowlistCheck = ({
locationWorkspaceId
}: {
locationWorkspaceId: req;
}) => {
return async (req: Request, _: Response, next: NextFunction) => {
const workspaceId = req[locationWorkspaceId]?.workspaceId;
if (req.authData.actor.type === ActorType.SERVICE) {
const trustedIps = await TrustedIP.find({
workspace: workspaceId
});
if (trustedIps.length > 0) {
// case: check the IP address of the inbound request against trusted IPs
const blockList = new net.BlockList();
for (const trustedIp of trustedIps) {
if (trustedIp.prefix !== undefined) {
blockList.addSubnet(trustedIp.ipAddress, trustedIp.prefix, trustedIp.type);
} else {
blockList.addAddress(trustedIp.ipAddress, trustedIp.type);
}
}
const { type } = extractIPDetails(req.authData.ipAddress);
const check = blockList.check(req.authData.ipAddress, type);
if (!check)
throw UnauthorizedRequestError({
message: "Failed workspace authorization"
});
}
}
return next();
}
}
export default requireIPAllowlistCheck;

View File

@ -9,24 +9,18 @@ type req = "params" | "body" | "query";
* on request params.
* @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
* @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing
*/
const requireWorkspaceAuth = ({
acceptedRoles,
locationWorkspaceId,
locationEnvironment = undefined,
requiredPermissions = [],
requireBlindIndicesEnabled = false,
requireE2EEOff = false,
checkIPAllowlist = false
}: {
acceptedRoles: Array<"admin" | "member">;
locationWorkspaceId: req;
locationEnvironment?: req | undefined;
requiredPermissions?: string[];
requireBlindIndicesEnabled?: boolean;
requireE2EEOff?: boolean;
checkIPAllowlist?: boolean;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const workspaceId = req[locationWorkspaceId]?.workspaceId;
@ -38,10 +32,7 @@ const requireWorkspaceAuth = ({
workspaceId: new Types.ObjectId(workspaceId),
environment,
acceptedRoles,
requiredPermissions,
requireBlindIndicesEnabled,
requireE2EEOff,
checkIPAllowlist
requiredPermissions
});
if (membership) {

View File

@ -36,6 +36,4 @@ const apiKeyDataSchema = new Schema<IAPIKeyData>(
}
);
const APIKeyData = model<IAPIKeyData>("APIKeyData", apiKeyDataSchema);
export default APIKeyData;
export const APIKeyData = model<IAPIKeyData>("APIKeyData", apiKeyDataSchema);

View File

@ -68,9 +68,7 @@ const backupPrivateKeySchema = new Schema<IBackupPrivateKey>(
}
);
const BackupPrivateKey = model<IBackupPrivateKey>(
export const BackupPrivateKey = model<IBackupPrivateKey>(
"BackupPrivateKey",
backupPrivateKeySchema
);
export default BackupPrivateKey;

View File

@ -74,6 +74,4 @@ const botSchema = new Schema<IBot>(
}
);
const Bot = model<IBot>("Bot", botSchema);
export default Bot;
export const Bot = model<IBot>("Bot", botSchema);

View File

@ -40,6 +40,4 @@ const botKeySchema = new Schema<IBotKey>(
}
);
const BotKey = model<IBotKey>("BotKey", botKeySchema);
export default BotKey;
export const BotKey = model<IBotKey>("BotKey", botKeySchema);

View File

@ -93,6 +93,4 @@ const botOrgSchema = new Schema<IBotOrg>(
}
);
const BotOrg = model<IBotOrg>("BotOrg", botOrgSchema);
export default BotOrg;
export const BotOrg = model<IBotOrg>("BotOrg", botOrgSchema);

View File

@ -51,6 +51,4 @@ const folderRootSchema = new Schema<TFolderRootSchema>(
}
);
const Folder = model<TFolderRootSchema>("Folder", folderRootSchema);
export default Folder;
export const Folder = model<TFolderRootSchema>("Folder", folderRootSchema);

View File

@ -23,9 +23,7 @@ const incidentContactOrgSchema = new Schema<IIncidentContactOrg>(
}
);
const IncidentContactOrg = model<IIncidentContactOrg>(
export const IncidentContactOrg = model<IIncidentContactOrg>(
"IncidentContactOrg",
incidentContactOrgSchema
);
export default IncidentContactOrg;
);

View File

@ -1,89 +1,30 @@
import BackupPrivateKey, { IBackupPrivateKey } from "./backupPrivateKey";
import Bot, { IBot } from "./bot";
import BotOrg, { IBotOrg } from "./botOrg";
import BotKey, { IBotKey } from "./botKey";
import IncidentContactOrg, { IIncidentContactOrg } from "./incidentContactOrg";
import Integration, { IIntegration } from "./integration";
import IntegrationAuth, { IIntegrationAuth } from "./integrationAuth";
import Key, { IKey } from "./key";
import Membership, { IMembership } from "./membership";
import MembershipOrg, { IMembershipOrg } from "./membershipOrg";
import Organization, { IOrganization } from "./organization";
import Secret, { ISecret } from "./secret";
import Folder, { TFolderRootSchema, TFolderSchema } from "./folder";
import SecretImport, { ISecretImports } from "./secretImports";
import SecretBlindIndexData, { ISecretBlindIndexData } from "./secretBlindIndexData";
import ServiceToken, { IServiceToken } from "./serviceToken";
import ServiceAccount, { IServiceAccount } from "./serviceAccount"; // new
import ServiceAccountKey, { IServiceAccountKey } from "./serviceAccountKey"; // new
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from "./serviceAccountOrganizationPermission"; // new
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from "./serviceAccountWorkspacePermission"; // new
import TokenData, { ITokenData } from "./tokenData";
import User, { AuthMethod, IUser } from "./user";
import UserAction, { IUserAction } from "./userAction";
import Workspace, { IWorkspace } from "./workspace";
import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData";
import APIKeyData, { IAPIKeyData } from "./apiKeyData";
import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail";
import TokenVersion, { ITokenVersion } from "./tokenVersion";
export {
AuthMethod,
BackupPrivateKey,
IBackupPrivateKey,
Bot,
IBot,
BotOrg,
IBotOrg,
BotKey,
IBotKey,
IncidentContactOrg,
IIncidentContactOrg,
Integration,
IIntegration,
IntegrationAuth,
IIntegrationAuth,
Key,
IKey,
Membership,
IMembership,
MembershipOrg,
IMembershipOrg,
Organization,
IOrganization,
Secret,
ISecret,
Folder,
TFolderRootSchema,
TFolderSchema,
SecretImport,
ISecretImports,
SecretBlindIndexData,
ISecretBlindIndexData,
ServiceToken,
IServiceToken,
ServiceAccount,
IServiceAccount,
ServiceAccountKey,
IServiceAccountKey,
ServiceAccountOrganizationPermission,
IServiceAccountOrganizationPermission,
ServiceAccountWorkspacePermission,
IServiceAccountWorkspacePermission,
TokenData,
ITokenData,
User,
IUser,
UserAction,
IUserAction,
Workspace,
IWorkspace,
ServiceTokenData,
IServiceTokenData,
APIKeyData,
IAPIKeyData,
LoginSRPDetail,
ILoginSRPDetail,
TokenVersion,
ITokenVersion
};
export * from "./backupPrivateKey";
export * from "./bot";
export * from "./botOrg";
export * from "./botKey";
export * from "./incidentContactOrg";
export * from "./integration/integration";
export * from "./integrationAuth";
export * from "./key";
export * from "./membership";
export * from "./membershipOrg";
export * from "./organization";
export * from "./secret";
export * from "./tag";
export * from "./folder";
export * from "./secretImports";
export * from "./secretBlindIndexData";
export * from "./serviceToken";
export * from "./serviceAccount";
export * from "./serviceAccountKey";
export * from "./serviceAccountOrganizationPermission";
export * from "./serviceAccountWorkspacePermission";
export * from "./tokenData";
export * from "./user";
export * from "./userAction";
export * from "./workspace";
export * from "./serviceTokenData";
export * from "./apiKeyData";
export * from "./loginSRPDetail";
export * from "./tokenVersion";
export * from "./webhooks";

View File

@ -0,0 +1 @@
export * from "./integration";

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