Compare commits

..

233 Commits

Author SHA1 Message Date
Maidul Islam
e22557b4bb Merge pull request #1088 from adelowo/support_path_when_generating_sample_env
[ENG-179] Add suport for --path for the secrets generate-example-env command
2023-10-20 11:10:12 +01:00
Maidul Islam
cbbb12c74e Merge pull request #1099 from Infisical/jwt-refactor
Update JWT secret scheme, replace many secrets with one secret
2023-10-20 11:02:58 +01:00
Tuan Dang
60beda604f Merge branch 'jwt-refactor' of https://github.com/Infisical/infisical into jwt-refactor 2023-10-20 10:55:40 +01:00
Tuan Dang
ae50987f91 Default AUTH_SECRET to JWT_AUTH_SECRET for backwards compatibility 2023-10-20 10:55:29 +01:00
Maidul Islam
32977e06f8 add warning text for .env.example 2023-10-20 10:38:42 +01:00
Maidul Islam
1510c39631 Update Chart.yaml 2023-10-19 19:19:46 +01:00
Maidul Islam
d0579b383f update readiness probe 2023-10-19 19:19:32 +01:00
Maidul Islam
b4f4c1064d Update values.yaml 2023-10-19 19:10:22 +01:00
Maidul Islam
d72d3940e6 update img name in prod img gha 2023-10-19 17:45:29 +01:00
Maidul Islam
7217bcb3d8 Merge pull request #1100 from Infisical/migrate-to-standalone-infisical
Migrate to standalone infisical
2023-10-19 16:07:55 +01:00
Maidul Islam
2faa9222d8 update gamma gh action 2023-10-19 16:05:41 +01:00
Tuan Dang
058712e8ec Update JWT secret scheme, replace many secrets with one secret 2023-10-19 15:53:36 +01:00
Maidul Islam
589f0bc134 update staging test for img 2023-10-19 13:23:07 +01:00
Maidul Islam
bd6dc3d4c0 update gamma helm values 2023-10-19 13:10:36 +01:00
Maidul Islam
9338babda6 update infisical helm chart read me 2023-10-19 13:03:18 +01:00
Maidul Islam
6f9e8644d7 update infisical helm chart to use standalone img 2023-10-19 12:58:16 +01:00
Maidul Islam
2fdb10277e update docs to use standalone infisical 2023-10-19 12:53:20 +01:00
Maidul Islam
15d2c536ed update prod docker compose 2023-10-19 12:23:20 +01:00
BlackMagiq
a304228961 Merge pull request #1095 from akhilmhdh/feat/folder-service-api-key
feat: added api key support for folder and secret import
2023-10-18 12:36:11 +01:00
Akhil Mohan
c865b78b41 feat: added api key support for folder and secret import 2023-10-18 16:38:19 +05:30
BlackMagiq
be80ac999e Merge pull request #1094 from Infisical/fix-azure-saml-flow
Patch Azure SAML Flow
2023-10-18 11:56:45 +01:00
Tuan Dang
076fe58325 Fix azure-saml flow 2023-10-18 11:49:57 +01:00
Maidul Islam
66bfab1994 update platform version env 2023-10-18 10:41:15 +01:00
Maidul Islam
b92c50addd properly add pre baked values 2023-10-18 10:40:34 +01:00
Maidul Islam
8fbca05052 remove extract_version 2023-10-17 23:13:04 +01:00
Maidul Islam
d99830067e bake posthog key in single docker img 2023-10-17 23:08:43 +01:00
Maidul Islam
cdc8ef95ab add platform version to UI 2023-10-17 23:08:43 +01:00
Tuan Dang
072e97b956 Fix azure samlConfig 2023-10-17 16:51:46 +01:00
Tuan Dang
4f26a7cad3 Revert audience change for azure saml 2023-10-17 15:50:31 +01:00
Tuan Dang
7bb6ff3d0c Merge branch 'main' of https://github.com/Infisical/infisical 2023-10-17 15:32:42 +01:00
Tuan Dang
ecccec8e35 Attempt fix azure samlConfig 2023-10-17 15:32:34 +01:00
Maidul Islam
7fd15a06e5 conditionally build standalone infisical 2023-10-17 14:32:56 +01:00
Maidul Islam
5d4a37004e Merge pull request #1089 from G3root/serve-frontend-from-backend
feat: serve frontend from backend
2023-10-17 14:15:41 +01:00
Maidul Islam
aa61fd091d remove redundant file from docker ignore 2023-10-17 14:11:16 +01:00
Maidul Islam
04ac54bcfa run cmd as non root user and update port to non privileged 2023-10-17 14:08:22 +01:00
Tuan Dang
38dbf1e738 Add missing / to samlConfig callbackURL 2023-10-17 14:03:37 +01:00
Tuan Dang
ddf9d7848c Update protocol in samlConfig 2023-10-17 12:57:05 +01:00
Maidul Islam
0b40de49ec remove redis error logs 2023-10-17 12:24:54 +01:00
Maidul Islam
b1d16cab39 remove promise.all from closeDatabaseHelper 2023-10-17 12:24:18 +01:00
Maidul Islam
fb7c7045e9 set telemetry post frontend build in standalone docker img 2023-10-17 12:22:08 +01:00
Tuan Dang
d570828e47 Update path and callbackURL for samlConfig 2023-10-17 12:13:54 +01:00
Maidul Islam
2a92b6c787 Merge pull request #1093 from akhilmhdh/feat/secret-update-id
feat: changed secret update to use id
2023-10-17 11:14:30 +01:00
Akhil Mohan
ee54fdabe1 feat: changed secret update to use id 2023-10-17 15:38:30 +05:30
nafees nazik
136308f299 revert: temp workaround: remove use of get static prop 2023-10-16 20:37:35 +05:30
nafees nazik
ba41244877 chore: remove next.js 2023-10-16 20:35:47 +05:30
nafees nazik
c4dcf334f0 fix: remove copy config 2023-10-16 20:35:29 +05:30
nafees nazik
66bac3ef48 fix: static props not rendered 2023-10-16 20:35:16 +05:30
Maidul Islam
e5347719c3 temp workaround: remove use of get static prop 2023-10-16 15:03:55 +01:00
Tuan Dang
275416a08f Remove 1-on-1 pairing link from README 2023-10-16 10:23:24 +01:00
Maidul Islam
abe1f54aab remove nginx and pm2 2023-10-15 23:58:04 +01:00
nafees nazik
13c1e2b349 fix: docker file 2023-10-15 12:09:45 +05:30
nafees nazik
f5a9afec61 fix: pm2 config 2023-10-15 11:52:55 +05:30
nafees nazik
d0a85c98b2 chore: add docker ignore and git ignore 2023-10-15 11:47:35 +05:30
nafees nazik
e0669cae7c chore: update path 2023-10-15 11:47:06 +05:30
Lanre Adelowo
e0dfb2548f update flag help text 2023-10-15 02:55:29 +01:00
Lanre Adelowo
01997a5187 support --path when generating sample env files 2023-10-15 02:50:41 +01:00
nafees nazik
2c011b7d53 feat: add custom server 2023-10-14 13:20:27 +05:30
nafees nazik
1b24a9b6e9 chore: add next to gitignore 2023-10-14 13:20:14 +05:30
nafees nazik
00c173aead chore: add next for backend 2023-10-14 13:19:54 +05:30
nafees nazik
2e15ad0625 fix: type 2023-10-14 12:54:26 +05:30
BlackMagiq
3f0b6dc6c1 Merge pull request #1087 from Infisical/self-hosted-sso-docs-clarification
Add FAQ to self-hosted SSO docs for it not working due to misconfigur…
2023-10-13 16:00:52 +01:00
Maidul Islam
f766a1eb29 Merge pull request #1078 from akhilmhdh/feat/secret-bug
feat: added support for recursive file creation
2023-10-13 15:57:50 +01:00
Tuan Dang
543c55b5a6 Add FAQ to self-hosted SSO docs for it not working due to misconfiguration 2023-10-13 15:56:10 +01:00
BlackMagiq
cdb1d38f99 Merge pull request #1086 from Infisical/del-org-stripe
Patch delete user, org, project session impl and account for organizationId in local storage
2023-10-13 12:46:38 +01:00
Tuan Dang
0a53b72cce Patch delete user, org, project session impl and account for orgId in localStorage 2023-10-13 11:22:40 +01:00
Maidul Islam
b921c376b2 Merge pull request #1071 from ragnarbull/improve-logging
Improve-logging
2023-10-12 10:08:39 -04:00
Maidul Islam
b1ec59eb67 polish error handling 2023-10-12 15:06:37 +01:00
Maidul Islam
4e6e12932a Merge pull request #1080 from Tchoupinax/main
feat(helm-chart): allow to provide affinity values for the pods
2023-10-12 06:03:56 -04:00
Maidul Islam
792c382743 update chart version 2023-10-12 11:03:21 +01:00
Maidul Islam
f5c8e537c9 generate documentation 2023-10-12 11:01:21 +01:00
Maidul Islam
4bf09a8efc Merge pull request #1079 from Salman2301/patch-1
docs: fix missing export format `yaml`
2023-10-12 05:02:06 -04:00
Tchoupinax
001265cf2a feat(helm-chart): allow to provide affinity values for the pods 2023-10-11 21:15:21 +02:00
BlackMagiq
a56a135396 Merge pull request #1067 from Infisical/delete-org
Delete user, organization, project capabilities feature/update
2023-10-11 19:26:19 +01:00
Salman K.A.A
9838c29867 docs: fix missing export format yaml 2023-10-11 18:48:56 +05:30
Akhil Mohan
4f5946b252 feat: added support for recursive file creation 2023-10-11 17:19:34 +05:30
Tuan Dang
dc23517133 Merge remote-tracking branch 'origin' into delete-org 2023-10-10 14:54:19 +01:00
BlackMagiq
5e4d4f56e3 Merge pull request #1064 from Infisical/github-checkly-suffixes
allow multiple simultaneous integrations with checkly and github
2023-10-10 14:48:30 +01:00
BlackMagiq
a855a2cee6 Merge pull request #1063 from Infisical/ui-updates
fix styling issues
2023-10-10 13:55:40 +01:00
Tuan Dang
e86258949c Refactor learning note rendering to use react-query 2023-10-10 12:18:59 +01:00
Joel Biddle
f119c921d0 remove comment 2023-10-09 13:38:21 +00:00
Joel Biddle
b6ef55783e Fix: add stack trace errors in logging for prod 2023-10-09 13:37:16 +00:00
Tuan Dang
feade5d029 Clear react-query cache upon user logout, delete account 2023-10-09 09:00:31 +01:00
Tuan Dang
8f74d20e74 Fix redirect to unknown org in login case when user is not part of any orgs 2023-10-09 08:31:13 +01:00
Tuan Dang
0eb7896b59 Merge remote-tracking branch 'origin' into delete-org 2023-10-09 07:47:41 +01:00
Tuan Dang
9fcecc9c92 Finish preliminary delete user, organization, refactor delete project, create org page 2023-10-09 07:47:20 +01:00
Vladyslav Matsiiako
ee6afa8983 allowed creating multiple github integrations to different apps at once 2023-10-08 17:05:26 -07:00
Vladyslav Matsiiako
6f4ac02558 fix ui for env overview empty screen 2023-10-08 14:02:22 -07:00
Vladyslav Matsiiako
5971480ca9 allow multiple simultaneous integrations with checkly and github 2023-10-08 13:53:54 -07:00
Vladyslav Matsiiako
d222b09ba5 changed scroll to auto 2023-10-07 20:25:04 -07:00
Vladyslav Matsiiako
a9fd0374bd fixed signup screen scroll 2023-10-07 20:22:49 -07:00
Vladyslav Matsiiako
ca008c809a added update-blog reference and fixed login 2023-10-07 20:19:10 -07:00
Vladyslav Matsiiako
6df7590051 fix styling issues 2023-10-07 17:13:44 -07:00
vmatsiiako
60bd5e57fc Update README.md 2023-10-06 21:56:27 -07:00
Maidul Islam
703a7a316a Update values.yaml for staging 2023-10-06 16:19:10 -04:00
Maidul Islam
f4de7a2c56 Revert "disable mongo for staging"
This reverts commit 383825672b.
2023-10-06 16:18:28 -04:00
Maidul Islam
383825672b disable mongo for staging 2023-10-06 16:08:58 -04:00
Maidul Islam
c6124d7444 update memory value for backend 2023-10-06 15:56:44 -04:00
Maidul Islam
ee80f4a89b add / after cli callback host 2023-10-05 15:42:21 -04:00
Maidul Islam
0b3b014bf5 Merge pull request #1044 from akhilmhdh/feat/secret-approval-part-2
Policy based secret review system
2023-10-05 15:06:41 -04:00
Akhil Mohan
7f463cabce feat(secret-approval): moved approval code to ee 2023-10-05 21:10:18 +05:30
Maidul Islam
b1962129a3 Merge pull request #1059 from Infisical/potential-cli-login-patch
change broswer based login from localhost to 127.0.0.1
2023-10-05 11:26:59 -04:00
Maidul Islam
28ad403665 change broswer based login from localhost to 127.0.0.1 2023-10-05 11:19:29 -04:00
Akhil Mohan
cb893f71ee feat(secret-approval): conflict ui, request count and secret path in request detail 2023-10-05 20:45:16 +05:30
Maidul Islam
80a3ea42ac minor typo 2023-10-05 20:44:16 +05:30
Maidul Islam
aafd7f0884 add secretApproval to globalFeatureSet 2023-10-05 20:44:16 +05:30
Akhil Mohan
faacb75034 feat(secret-approval): added new audit log and subscription on policy creation 2023-10-05 20:44:16 +05:30
Vladyslav Matsiiako
7caac2e64c minor style updates to approvals 2023-10-05 20:34:50 +05:30
Akhil Mohan
df636c91b4 feat(secret-approval): added permission for policy management and fixed bugs on fellow user reviewing secrets 2023-10-05 20:34:50 +05:30
Akhil Mohan
9dc97f7208 feat(secret-approval): added auto naming policy and minor ux enhancements 2023-10-05 20:34:50 +05:30
Akhil Mohan
4fd227c85f feat(secret-approval): added loading and empty states for request list 2023-10-05 20:34:50 +05:30
Akhil Mohan
04c7d49477 feat(secret-approval): resolved infinite query bug and added support for closing, re-opening request, stale req ui 2023-10-05 20:34:50 +05:30
Akhil Mohan
63588b4e44 feat(secret-approval): implemented the new policy based approval system bare version 2023-10-05 20:34:50 +05:30
Akhil Mohan
fc43511f5d feat(secret-approval): implemented the base 2023-10-05 20:33:48 +05:30
BlackMagiq
a995627815 Merge pull request #1027 from Infisical/service-token-v3
Service Token V3
2023-10-05 15:42:41 +01:00
Tuan Dang
c2f7923c1d Hide ST V3 from UI, switch docs to ST V2 2023-10-05 15:23:32 +01:00
Tuan Dang
abf6034aca Adjust ST V3 2023-10-05 14:53:59 +01:00
Maidul Islam
5fce85ca41 Merge pull request #1037 from scomans/main
fix: renaming environments not updated in `secretimports` model
2023-10-04 23:12:30 -04:00
Tuan Dang
702d28faca Add URL_GITLAB_LOGIN envar to docs 2023-10-04 22:09:09 +01:00
Tuan Dang
dbeafe1f5d Attempt fix gitlab sso docs images 2023-10-04 22:03:18 +01:00
Tuan Dang
46f700023b Update README 2023-10-04 21:49:06 +01:00
BlackMagiq
25b988ca9a Merge pull request #1029 from atimapreandrew/gitlab-sso
Gitlab sso
2023-10-04 21:39:52 +01:00
Tuan Dang
41af5cea93 Move google, github, gitlab auth out of /ee 2023-10-04 21:31:50 +01:00
Tuan Dang
e21daf6771 Add docs for gitlab sso, add support for self-hosted gitlab instance sso 2023-10-04 20:48:42 +01:00
Maidul Islam
c0f81ec84e Merge pull request #1054 from RobinTail:RobinTail-patch-1
Upgrading `zod`
2023-10-04 15:22:04 -04:00
Maidul Islam
c85e2c71ca add NEXT_INFISICAL_PLATFORM_VERSION to staging 2023-10-04 15:06:59 -04:00
Maidul Islam
9ae6e5ea1c Merge pull request #1047 from ragnarbull/display-curr-version
Feat: add version tags to Docker image & display on frontend
2023-10-04 15:01:11 -04:00
Maidul Islam
d3026a98d8 add back infisical/backend:latest 2023-10-04 14:57:30 -04:00
Maidul Islam
14b8c2c12a remove unrelated changes 2023-10-04 14:29:36 -04:00
Anna Bocharova
d9a69441c4 Updating the lock file accordingly. 2023-10-04 14:57:43 +00:00
Anna Bocharova
f46a23dabf Upgrading zod in package.json 2023-10-04 16:54:39 +02:00
Tuan Dang
9e2d6daeba Merge remote-tracking branch 'origin' into gitlab-sso 2023-10-04 11:11:05 +01:00
Tuan Dang
a2bebb5afa Merge remote-tracking branch 'origin' into service-token-v3 2023-10-04 11:02:55 +01:00
BlackMagiq
9fc5303a97 Merge pull request #1053 from Infisical/update-docs
Update Platform Documentation
2023-10-04 11:01:47 +01:00
Tuan Dang
97a5b509b7 Point getting started SDK to SDK repos to avoid docs sprawl 2023-10-04 10:57:11 +01:00
Tuan Dang
7660119584 Merge remote-tracking branch 'origin' into update-docs 2023-10-04 10:44:38 +01:00
Tuan Dang
a51d202d51 Fix merge conflicts 2023-10-04 10:39:50 +01:00
Tuan Dang
34273b30f2 Finish update for platform docs 2023-10-04 10:25:54 +01:00
Joel Biddle
726f38c15f Resolve review comments 2023-10-04 04:50:16 +00:00
Joel Biddle
390c2cc4d9 Changed to pass build arg & create NEXT envar 2023-10-04 02:03:17 +00:00
Maidul Islam
49098b7693 update folder name of build tool integrations 2023-10-03 20:12:03 -04:00
Maidul Islam
501d940558 add Gradle docs 2023-10-03 20:00:50 -04:00
Maidul Islam
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
Akhil Mohan
f3908e6b2a fix: resolved permission check on imported secrets when using service token 2023-10-03 20:14:43 +05:30
Tuan Dang
cf4eb629f2 Start revising project docs 2023-10-03 08:58:13 +01:00
Joel Biddle
95af82963f Add version tags to Docker image & display on frontend 2023-10-03 05:55:36 +00:00
Maidul Islam
bd8c17d720 resolve merge conflict 2023-10-02 18:58:38 -04:00
Tuan Dang
d3bc95560c Merge remote-tracking branch 'origin' into update-docs 2023-10-02 20:57:03 +01:00
Tuan Dang
4a838b788f Start comb docs, integrations, intro, organization 2023-10-02 20:26:16 +01:00
Maidul Islam
01c655699c Merge pull request #1045 from Infisical/proper-server-cleanup
handle siginit and sigterm
2023-10-02 11:44:36 -07:00
Maidul Islam
3456dfbd86 handle siginit and sigterm 2023-10-02 11:40:00 -07:00
Maidul Islam
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
Tuan Dang
c2ca4d77fc Add ST V3 docs, update ST-handling recommendation docs 2023-10-01 20:06:36 +01:00
Tuan Dang
3b3f78ee3c Start revising docs 2023-10-01 16:42:40 +01:00
Tuan Dang
d6a5c50fd9 Fix lint issue 2023-09-30 22:04:06 +01:00
Tuan Dang
839fcc2775 Add paywall to ST V3 ip allowlisting 2023-09-30 21:55:18 +01:00
Tuan Dang
eb2f433f43 Add trusted IP rules to ST V3 2023-09-30 20:52:04 +01:00
Akhil Mohan
04c74293ed fix: resolved dashboard showing text folderName in nesting folders 2023-09-30 23:47:38 +05:30
Tuan Dang
cdf4440848 Fix lint issues 2023-09-30 13:45:37 +01:00
Tuan Dang
20b584b7b8 Fix merge conflicts 2023-09-30 12:54:43 +01:00
Tuan Dang
3779209ed5 Update permission implementation for ST V3 2023-09-30 12:52:35 +01:00
Sebastian Comans
105c2e51ee fix: renaming environments not updated in secretimports model 2023-09-30 08:54:51 +02:00
Maidul Islam
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
Maidul Islam
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
Tuan Dang
d723d26d2e Fix merge conflicts 2023-09-29 11:42:47 +01:00
Tuan Dang
8a576196a3 Return workspaceId in response for service token key copy 2023-09-29 11:16:12 +01:00
Akhil Mohan
2cf5fd80ca feat(dashboard-v3): removed a line at top on empty state 2023-09-29 14:31:31 +05:30
Akhil Mohan
74534cfbaa feat(dashboard-v3): patched dashboard copy sec bug and add secret in empty state 2023-09-29 13:34:44 +05:30
Maidul Islam
66787b1f93 fix secret scanning zod error for installationId 2023-09-28 23:09:12 -07:00
vmatsiiako
890082acbc Update service-token.mdx 2023-09-28 22:11:24 -07:00
Maidul Islam
a364b174e0 add expire time to service token create 2023-09-28 19:31:33 -07:00
Maidul Islam
2bb2ccc19e patch crypto in create service token in cli 2023-09-28 19:27:38 -07:00
Maidul Islam
3bbf770027 bug fixes for v3 secret apis 2023-09-28 12:11:26 -07:00
Maidul Islam
2610356d45 Merge pull request #1018 from akhilmhdh/feat/dashboard-v3
Feat/dashboard v3
2023-09-28 10:35:35 -07:00
Akhil Mohan
67e164e2bb feat(dashboard-v3): z-index change in tooltip for drawer 2023-09-28 22:29:50 +05:30
snyk-bot
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
Akhil Mohan
4502d12e46 feat(dashboard-v3): typo fix 2023-09-28 21:28:43 +05:30
Akhil Mohan
ef6ee6b2e6 feat(dashboard-v3): resolved create secret issue and allow empty secret values in create secret 2023-09-28 21:28:43 +05:30
Maidul Islam
e902a54af0 remove deprecated EELogService 2023-09-28 21:28:43 +05:30
Akhil Mohan
50efb8b8bd feat: resolved minor issues with dashboard v3 on feedback 2023-09-28 21:28:43 +05:30
Vladyslav Matsiiako
5450c1126a minor style updates to the dashboard 2023-09-28 21:28:43 +05:30
Akhil Mohan
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
Akhil Mohan
85378e25aa feat: updated input create secret style and some more updates on style 2023-09-28 21:28:43 +05:30
Akhil Mohan
b54c29fc48 feat(dashboard-v3): implemented new the dashboard with v3 support 2023-09-28 21:28:43 +05:30
Akhil Mohan
fcf3f2837e feat(dashboard-v3): updated ui components and hooks for new migrated apis and v3 apis 2023-09-28 21:28:43 +05:30
Akhil Mohan
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
Maidul Islam
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
Maidul Islam
4365be9b75 Merge pull request #1031 from akhilmhdh/feat/secret-approval
Secret approval policies feature
2023-09-27 23:56:16 -07:00
Akhil Mohan
b0c398688b feat(secret-approval): updated names to secret policy and fixed approval number bug 2023-09-28 12:23:01 +05:30
Maidul Islam
1141408d5b add exit codes for errors 2023-09-27 21:42:34 -07:00
Maidul Islam
b24bff5af6 Update service-token.mdx 2023-09-27 21:17:28 -07:00
Maidul Islam
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
Akhil Mohan
c67432a56f feat(secret-approval): implemented frontend ui for secret policies 2023-09-27 23:10:45 +05:30
Akhil Mohan
edeb6bbc66 feat(secret-approval): implemented backend api for secret policies 2023-09-27 23:10:28 +05:30
Andrew Atimapre
f72e240ce5 Merge branch 'main' into gitlab-sso 2023-09-27 12:57:53 +01:00
nafees nazik
77ec17ccd4 fix: update many query 2023-09-27 17:01:02 +05:30
nafees nazik
6e992858aa fix: add renamed fields to other models 2023-09-27 15:12:32 +05:30
Akhil Mohan
9cda85f03e checkpoint 2023-09-27 11:50:52 +05:30
Maidul Islam
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
Maidul Islam
8265d18934 nit: add lean option 2023-09-26 22:00:00 -07:00
Tuan Dang
c65a53f1f7 Add endpoint to get project key for ST V3 2023-09-26 14:24:55 +01:00
Tuan Dang
aa1e0b0f28 Update audit log service actor v3 filter, status toggle permissions, add JWT service token secret, for ST V3 2023-09-26 12:03:00 +01:00
Tuan Dang
4683dc7869 Scope switch cases into blocks for secrets v3 2023-09-26 10:56:08 +01:00
Akhil Mohan
4c1324baa9 feat: added support for getting imported secrets in v3 getSecret api 2023-09-26 12:25:23 +05:30
Andrew Atimapre
bc489e65ca Integrated login with Gitlab 2023-09-26 02:21:27 +01:00
Maidul Islam
5128466233 update go.mod after Infisical/go-keyring update 2023-09-25 15:28:13 -07:00
Maidul Islam
e11abb619a use v1.0.2 of internal keyring in cli 2023-09-25 15:10:59 -07:00
Maidul Islam
f51e9ba8ff add back role migration 2023-09-25 11:51:25 -07:00
Tuan Dang
45b85ab962 Fix merge conflicts 2023-09-25 13:26:09 +01:00
Tuan Dang
698a268b5f Add permissions and audit logging to service tokens v3 2023-09-25 13:24:28 +01:00
Andrew Atimapre
0d74752169 Integrated login with Gitlab 2023-09-24 22:33:00 +01:00
Andrew Atimapre
255705501f Integrated login with Gitlab 2023-09-24 15:21:23 +01:00
BlackMagiq
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
Tuan Dang
30da2e50b1 Patch Vercel integration for custom preview branches 2023-09-23 11:38:49 +01:00
Maidul Islam
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
Maidul Islam
e81ea314e1 update go minor version 2023-09-22 13:26:32 -07:00
Maidul Islam
f19aca2904 fix zod type for ToggleAutoCapitalizationV2 2023-09-22 13:10:11 -07:00
vmatsiiako
763bdabd60 Merge pull request #998 from Infisical/qovery-integration
Added Qovery integration
2023-09-22 12:44:06 -07:00
vmatsiiako
7ec708b71d Merge pull request #1019 from akhilmhdh/fix/audit-log
feat: made audit log options back
2023-09-22 12:24:42 -07:00
Akhil Mohan
3c6c1891a8 feat: made audit log options back 2023-09-22 22:07:48 +05:30
BlackMagiq
01d3d84b40 Merge pull request #1017 from Infisical/debug-self-hosted-gitlab
Patch self-hosted gitlab integration
2023-09-22 15:47:05 +01:00
Tuan Dang
32bec03adf Patch self-hosted gitlab integration 2023-09-22 15:19:26 +01:00
Tuan Dang
f59b3b3305 Add separate ServiceTokenV3 auth type 2023-09-22 14:43:42 +01:00
BlackMagiq
5b6c2e05f2 Merge pull request #1016 from Infisical/fix-teamcity
Fix TeamCity integration blank screen issue
2023-09-22 12:11:59 +01:00
Tuan Dang
c623f572b7 Fix TeamCity integration 2023-09-22 12:06:42 +01:00
Tuan Dang
53856ff868 Make more progress on service token v3 2023-09-22 11:19:14 +01:00
Tuan Dang
84d094b4d8 Finish preliminary CRUD ops for service token v3, ServiceTokenV3Key structure 2023-09-21 15:15:22 +01:00
Tuan Dang
efb14ca267 Merge remote-tracking branch 'origin' into service-token-v3 2023-09-20 17:47:55 +01:00
Tuan Dang
1896442168 Finish basic scaffolding for service token v3 2023-09-20 17:32:33 +01:00
Tuan Dang
b6219e14f0 Finish optimizing qovery integration, add docs for it 2023-09-19 14:43:20 +01:00
Tuan Dang
e3ef826f52 Update qovery integration 2023-09-19 13:50:01 +01:00
Scott Williams
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
Tuan Dang
1cdd840485 Begin service token v3 2023-09-18 22:15:48 +01:00
Vladyslav Matsiiako
0b59a92dfb Added Qovery integration 2023-09-17 18:15:47 -07:00
630 changed files with 19088 additions and 7889 deletions

View File

@@ -1,2 +1,10 @@
backend/node_modules backend/node_modules
frontend/node_modules frontend/node_modules
backend/frontend-build
**/node_modules
**/.next
.dockerignore
.git
README.md
.dockerignore
**/Dockerfile

View File

@@ -1,23 +1,12 @@
# Keys # Keys
# Required key for platform encryption/decryption ops # Required key for platform encryption/decryption ops
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NOT BE USED FOR PRODUCTION # THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# JWT # JWT
# Required secrets to sign JWT tokens # Required secrets to sign JWT tokens
JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a # THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201
# JWT lifetime
# Optional lifetimes for JWT tokens expressed in seconds or a string
# describing a time span (e.g. 60, "2 days", "10h", "7d")
JWT_AUTH_LIFETIME=
JWT_REFRESH_LIFETIME=
JWT_SIGNUP_LIFETIME=
JWT_PROVIDER_AUTH_LIFETIME=
# MongoDB # MongoDB
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref # Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
@@ -67,5 +56,12 @@ SENTRY_DSN=
POSTHOG_HOST= POSTHOG_HOST=
POSTHOG_PROJECT_API_KEY= POSTHOG_PROJECT_API_KEY=
CLIENT_ID_GOOGLE= # SSO-specific variables
CLIENT_SECRET_GOOGLE= CLIENT_ID_GOOGLE_LOGIN=
CLIENT_SECRET_GOOGLE_LOGIN=
CLIENT_ID_GITHUB_LOGIN=
CLIENT_SECRET_GITHUB_LOGIN=
CLIENT_ID_GITLAB_LOGIN=
CLIENT_SECRET_GITLAB_LOGIN=

View File

@@ -6,7 +6,7 @@ services:
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- mongo - mongo
image: infisical/backend:test image: infisical/infisical:test
command: npm run start command: npm run start
environment: environment:
- NODE_ENV=production - NODE_ENV=production

39
.github/values.yaml vendored
View File

@@ -1,29 +1,3 @@
# secretScanningGitApp:
# enabled: false
# deploymentAnnotations:
# secrets.infisical.com/auto-reload: "true"
# image:
# repository: infisical/staging_deployment_secret-scanning-git-app
frontend:
enabled: true
name: frontend
podAnnotations: {}
deploymentAnnotations:
secrets.infisical.com/auto-reload: "true"
replicaCount: 2
image:
repository: infisical/staging_deployment_frontend
tag: "latest"
pullPolicy: Always
kubeSecretRef: managed-secret-frontend
service:
annotations: {}
type: ClusterIP
nodePort: ""
frontendEnvironmentVariables: null
backend: backend:
enabled: true enabled: true
name: backend name: backend
@@ -32,7 +6,7 @@ backend:
secrets.infisical.com/auto-reload: "true" secrets.infisical.com/auto-reload: "true"
replicaCount: 2 replicaCount: 2
image: image:
repository: infisical/staging_deployment_backend repository: infisical/staging_infisical
tag: "latest" tag: "latest"
pullPolicy: Always pullPolicy: Always
kubeSecretRef: managed-backend-secret kubeSecretRef: managed-backend-secret
@@ -40,12 +14,15 @@ backend:
annotations: {} annotations: {}
type: ClusterIP type: ClusterIP
nodePort: "" nodePort: ""
resources:
limits:
memory: 300Mi
backendEnvironmentVariables: null backendEnvironmentVariables: null
## Mongo DB persistence ## Mongo DB persistence
mongodb: mongodb:
enabled: true enabled: false
persistence: persistence:
enabled: false enabled: false
@@ -62,12 +39,6 @@ ingress:
# kubernetes.io/ingress.class: "nginx" # kubernetes.io/ingress.class: "nginx"
# cert-manager.io/issuer: letsencrypt-nginx # cert-manager.io/issuer: letsencrypt-nginx
hostName: gamma.infisical.com ## <- Replace with your own domain hostName: gamma.infisical.com ## <- Replace with your own domain
frontend:
path: /
pathType: Prefix
backend:
path: /api
pathType: Prefix
tls: tls:
[] []
# - secretName: letsencrypt-nginx # - secretName: letsencrypt-nginx

View File

@@ -39,7 +39,7 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true load: true
context: backend context: backend
tags: infisical/backend:test tags: infisical/infisical:test
- name: ⏻ Spawn backend container and dependencies - name: ⏻ Spawn backend container and dependencies
run: | run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
@@ -93,6 +93,7 @@ jobs:
tags: infisical/frontend:test tags: infisical/frontend:test
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
- name: ⏻ Spawn frontend container - name: ⏻ Spawn frontend container
run: | run: |
docker run -d --rm --name infisical-frontend-test infisical/frontend:test docker run -d --rm --name infisical-frontend-test infisical/frontend:test
@@ -116,3 +117,4 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}

View File

@@ -2,7 +2,7 @@ name: Build, Publish and Deploy to Gamma
on: [workflow_dispatch] on: [workflow_dispatch]
jobs: jobs:
backend-image: infisical-image:
name: Build backend image name: Build backend image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -32,8 +32,9 @@ jobs:
project: 64mmf0n610 project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true load: true
context: backend context: .
tags: infisical/backend:test file: Dockerfile.standalone-infisical
tags: infisical/infisical:test
- name: ⏻ Spawn backend container and dependencies - name: ⏻ Spawn backend container and dependencies
run: | run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
@@ -49,66 +50,20 @@ jobs:
project: 64mmf0n610 project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true push: true
context: backend context: .
file: Dockerfile.standalone-infisical
tags: | tags: |
infisical/staging_deployment_backend:${{ steps.commit.outputs.short }} infisical/staging_infisical:${{ steps.commit.outputs.short }}
infisical/staging_deployment_backend:latest infisical/staging_infisical:latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
frontend-image:
name: Build frontend image
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build frontend and export to Docker
uses: depot/build-push-action@v1
with:
load: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
project: 64mmf0n610
context: frontend
tags: infisical/staging_deployment_frontend:test
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
- name: ⏻ Spawn frontend container
run: |
docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test
- name: 🧪 Test frontend image
run: |
./.github/resources/healthcheck.sh infisical-frontend-test
- name: ⏻ Shut down frontend container
run: |
docker stop infisical-frontend-test
- name: 🏗️ Build frontend and push
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend
tags: |
infisical/staging_deployment_frontend:${{ steps.commit.outputs.short }}
infisical/staging_deployment_frontend:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
gamma-deployment: gamma-deployment:
name: Deploy to gamma name: Deploy to gamma
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [frontend-image, backend-image] needs: [infisical-image]
steps: steps:
- name: ☁️ Checkout source - name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3

View File

@@ -73,3 +73,6 @@ jobs:
infisical/infisical:${{ steps.extract_version.outputs.version }} infisical/infisical:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
file: Dockerfile.standalone-infisical file: Dockerfile.standalone-infisical
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}

4
.gitignore vendored
View File

@@ -33,7 +33,7 @@ reports
junit.xml junit.xml
# next.js # next.js
/.next/ .next/
/out/ /out/
# production # production
@@ -60,3 +60,5 @@ yarn-error.log*
# Editor specific # Editor specific
.vscode/* .vscode/*
frontend-build

View File

@@ -1,7 +1,13 @@
ARG POSTHOG_HOST=https://app.posthog.com ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id
FROM node:16-alpine AS frontend-dependencies FROM node:16-alpine AS base
FROM base AS frontend-dependencies
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
@@ -11,7 +17,7 @@ COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
RUN npm ci --only-production --ignore-scripts RUN npm ci --only-production --ignore-scripts
# Rebuild the source code only when needed # Rebuild the source code only when needed
FROM node:16-alpine AS frontend-builder FROM base AS frontend-builder
WORKDIR /app WORKDIR /app
# Copy dependencies # Copy dependencies
@@ -27,41 +33,38 @@ ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
# Build # Build
RUN npm run build RUN npm run build
# Production image # Production image
FROM node:16-alpine AS frontend-runner FROM base AS frontend-runner
WORKDIR /app WORKDIR /app
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser --system --uid 1001 non-root-user
RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
VOLUME /app/.next/cache/images VOLUME /app/.next/cache/images
ARG POSTHOG_API_KEY COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public COPY --from=frontend-builder /app/public ./public
RUN chown nextjs:nodejs ./public/data RUN chown non-root-user:nodejs ./public/data
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
USER nextjs USER non-root-user
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED 1
## ##
## BACKEND ## BACKEND
## ##
FROM node:16-alpine AS backend-build FROM base AS backend-build
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user
WORKDIR /app WORKDIR /app
@@ -69,10 +72,11 @@ COPY backend/package*.json ./
RUN npm ci --only-production RUN npm ci --only-production
COPY /backend . COPY /backend .
COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh
RUN npm run build RUN npm run build
# Production stage # Production stage
FROM node:16-alpine AS backend-runner FROM base AS backend-runner
WORKDIR /app WORKDIR /app
@@ -81,27 +85,44 @@ RUN npm ci --only-production
COPY --from=backend-build /app . COPY --from=backend-build /app .
RUN mkdir frontend-build
# Production stage # Production stage
FROM node:16-alpine AS production FROM base AS production
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user
## set pre baked keys
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
WORKDIR / WORKDIR /
# Install PM2
RUN npm install -g pm2
# Copy ecosystem.config.js
COPY ecosystem.config.js .
RUN apk add --no-cache nginx
COPY nginx/default-stand-alone-docker.conf /etc/nginx/nginx.conf
COPY --from=backend-runner /app /backend COPY --from=backend-runner /app /backend
COPY --from=frontend-runner /app/ /app/ COPY --from=frontend-runner /app ./backend/frontend-build
EXPOSE 80 ENV PORT 8080
ENV HTTPS_ENABLED false ENV HTTPS_ENABLED false
ENV NODE_ENV production
ENV STANDALONE_BUILD true
WORKDIR /backend
ENV TELEMETRY_ENABLED true
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js
EXPOSE 8080
USER non-root-user
CMD ["./standalone-entrypoint.sh"]
CMD ["pm2-runtime", "start", "ecosystem.config.js"]

View File

@@ -1,9 +1,8 @@
<h1 align="center"> <h1 align="center">
<img width="300" src="/img/logoname-black.svg#gh-light-mode-only" alt="infisical">
<img width="300" src="/img/logoname-white.svg#gh-dark-mode-only" alt="infisical"> <img width="300" src="/img/logoname-white.svg#gh-dark-mode-only" alt="infisical">
</h1> </h1>
<p align="center"> <p align="center">
<p align="center"><b>Open-source, end-to-end encrypted secret management platform</b>: distribute secrets/configs across your team/infrastructure and prevent secret leaks.</p> <p align="center"><b>The open-source secret management platform</b>: Sync secrets/configs across your team/infrastructure and prevent secret leaks.</p>
</p> </p>
<h4 align="center"> <h4 align="center">
@@ -34,7 +33,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" /> <img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a> </a>
<a href="https://cloudsmith.io/~infisical/repos/"> <a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-1.38M-orange" alt="Cloudsmith downloads" /> <img src="https://img.shields.io/badge/Downloads-2.58M-orange" alt="Cloudsmith downloads" />
</a> </a>
<a href="https://infisical.com/slack"> <a href="https://infisical.com/slack">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" /> <img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
@@ -44,11 +43,11 @@
</a> </a>
</h4> </h4>
<img src="/img/infisical_github_repo.png" width="100%" alt="Dashboard" /> <img src="/img/infisical_github_repo2.png" width="100%" alt="Dashboard" />
## Introduction ## Introduction
**[Infisical](https://infisical.com)** is an open source, end-to-end encrypted secret management platform that teams use to centralize their secrets like API keys, database credentials, and configurations. **[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their secrets like API keys, database credentials, and configurations.
We're on a mission to make secret management more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up. We're on a mission to make secret management more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up.
@@ -134,7 +133,6 @@ Whether it's big or small, we love contributions. Check out our guide to see how
Not sure where to get started? You can: Not sure where to get started? You can:
- [Book a free, non-pressure pairing session / code walkthrough with one of our teammates](https://cal.com/tony-infisical/30-min-meeting-contributing)!
- Join our <a href="https://infisical.com/slack">Slack</a>, and ask us any questions there. - Join our <a href="https://infisical.com/slack">Slack</a>, and ask us any questions there.
- Join our [community calls](https://us06web.zoom.us/j/82623506356) every Wednesday at 11am EST to ask any questions, provide feedback, hangout and more. - Join our [community calls](https://us06web.zoom.us/j/82623506356) every Wednesday at 11am EST to ask any questions, provide feedback, hangout and more.

View File

@@ -26,6 +26,7 @@
], ],
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error", "unused-imports/no-unused-imports": "error",
"@typescript-eslint/no-extra-semi": "off", // added to be able to push
"unused-imports/no-unused-vars": [ "unused-imports/no-unused-vars": [
"warn", "warn",
{ {

View File

@@ -50,6 +50,7 @@
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0", "posthog-node": "^2.6.0",
"probot": "^12.3.1", "probot": "^12.3.1",
@@ -63,7 +64,7 @@
"utility-types": "^3.10.0", "utility-types": "^3.10.0",
"winston": "^3.8.2", "winston": "^3.8.2",
"winston-loki": "^6.0.6", "winston-loki": "^6.0.6",
"zod": "^3.21.4" "zod": "^3.22.3"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.3.1", "@jest/globals": "^29.3.1",
@@ -13727,6 +13728,17 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/passport-gitlab2": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz",
"integrity": "sha512-cXQMgM6JQx9wHVh7JLH30D8fplfwjsDwRz+zS0pqC8JS+4bNmc1J04NGp5g2M4yfwylH9kQRrMN98GxMw7q7cg==",
"dependencies": {
"passport-oauth2": "^1.4.0"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/passport-google-oauth20": { "node_modules/passport-google-oauth20": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
@@ -16684,9 +16696,9 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.21.4", "version": "3.22.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
@@ -27163,6 +27175,14 @@
"passport-oauth2": "1.x.x" "passport-oauth2": "1.x.x"
} }
}, },
"passport-gitlab2": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz",
"integrity": "sha512-cXQMgM6JQx9wHVh7JLH30D8fplfwjsDwRz+zS0pqC8JS+4bNmc1J04NGp5g2M4yfwylH9kQRrMN98GxMw7q7cg==",
"requires": {
"passport-oauth2": "^1.4.0"
}
},
"passport-google-oauth20": { "passport-google-oauth20": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
@@ -29384,9 +29404,9 @@
"dev": true "dev": true
}, },
"zod": { "zod": {
"version": "3.21.4", "version": "3.22.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="
} }
} }
} }

View File

@@ -41,6 +41,7 @@
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0", "posthog-node": "^2.6.0",
"probot": "^12.3.1", "probot": "^12.3.1",
@@ -54,7 +55,7 @@
"utility-types": "^3.10.0", "utility-types": "^3.10.0",
"winston": "^3.8.2", "winston": "^3.8.2",
"winston-loki": "^6.0.6", "winston-loki": "^6.0.6",
"zod": "^3.21.4" "zod": "^3.22.3"
}, },
"name": "infisical-api", "name": "infisical-api",
"version": "1.0.0", "version": "1.0.0",

View File

@@ -1,3 +1,5 @@
import { GITLAB_URL } from "../variables";
import InfisicalClient from "infisical-node"; import InfisicalClient from "infisical-node";
export const client = new InfisicalClient({ export const client = new InfisicalClient({
@@ -15,17 +17,14 @@ export const getRootEncryptionKey = async () => {
} }
export const getInviteOnlySignup = async () => (await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true" export const getInviteOnlySignup = async () => (await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true"
export const getSaltRounds = async () => parseInt((await client.getSecret("SALT_ROUNDS")).secretValue) || 10; export const getSaltRounds = async () => parseInt((await client.getSecret("SALT_ROUNDS")).secretValue) || 10;
export const getAuthSecret = async () => (await client.getSecret("JWT_AUTH_SECRET")).secretValue ?? (await client.getSecret("AUTH_SECRET")).secretValue;
export const getJwtAuthLifetime = async () => (await client.getSecret("JWT_AUTH_LIFETIME")).secretValue || "10d"; export const getJwtAuthLifetime = async () => (await client.getSecret("JWT_AUTH_LIFETIME")).secretValue || "10d";
export const getJwtAuthSecret = async () => (await client.getSecret("JWT_AUTH_SECRET")).secretValue;
export const getJwtMfaLifetime = async () => (await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m"; export const getJwtMfaLifetime = async () => (await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m";
export const getJwtMfaSecret = async () => (await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m";
export const getJwtRefreshLifetime = async () => (await client.getSecret("JWT_REFRESH_LIFETIME")).secretValue || "90d"; export const getJwtRefreshLifetime = async () => (await client.getSecret("JWT_REFRESH_LIFETIME")).secretValue || "90d";
export const getJwtRefreshSecret = async () => (await client.getSecret("JWT_REFRESH_SECRET")).secretValue; export const getJwtServiceSecret = async () => (await client.getSecret("JWT_SERVICE_SECRET")).secretValue; // TODO: deprecate (related to ST V1)
export const getJwtServiceSecret = async () => (await client.getSecret("JWT_SERVICE_SECRET")).secretValue;
export const getJwtSignupLifetime = async () => (await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m"; export const getJwtSignupLifetime = async () => (await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m";
export const getJwtProviderAuthSecret = async () => (await client.getSecret("JWT_PROVIDER_AUTH_SECRET")).secretValue;
export const getJwtProviderAuthLifetime = async () => (await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m"; export const getJwtProviderAuthLifetime = async () => (await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m";
export const getJwtSignupSecret = async () => (await client.getSecret("JWT_SIGNUP_SECRET")).secretValue; export const getJwtServiceTokenSecret = async () => (await client.getSecret("JWT_SERVICE_TOKEN_SECRET")).secretValue;
export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue; export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue;
export const getNodeEnv = async () => (await client.getSecret("NODE_ENV")).secretValue || "production"; export const getNodeEnv = async () => (await client.getSecret("NODE_ENV")).secretValue || "production";
export const getVerboseErrorOutput = async () => (await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true; export const getVerboseErrorOutput = async () => (await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true;
@@ -52,6 +51,9 @@ export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIEN
export const getClientSecretGoogleLogin = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue; export const getClientSecretGoogleLogin = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
export const getClientIdGitHubLogin = async () => (await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue; export const getClientIdGitHubLogin = async () => (await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
export const getClientSecretGitHubLogin = async () => (await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue; export const getClientSecretGitHubLogin = async () => (await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue;
export const getClientIdGitLabLogin = async () => (await client.getSecret("CLIENT_ID_GITLAB_LOGIN")).secretValue;
export const getClientSecretGitLabLogin = async () => (await client.getSecret("CLIENT_SECRET_GITLAB_LOGIN")).secretValue;
export const getUrlGitLabLogin = async () => (await client.getSecret("URL_GITLAB_LOGIN")).secretValue || GITLAB_URL;
export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com"; export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE"; export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";

View File

@@ -6,15 +6,18 @@ const jsrp = require("jsrp");
import { LoginSRPDetail, TokenVersion, User } from "../../models"; import { LoginSRPDetail, TokenVersion, User } from "../../models";
import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth"; import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth";
import { checkUserDevice } from "../../helpers/user"; import { checkUserDevice } from "../../helpers/user";
import { ACTION_LOGIN, ACTION_LOGOUT } from "../../variables"; import {
ACTION_LOGIN,
ACTION_LOGOUT,
AuthTokenType
} from "../../variables";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { EELogService } from "../../ee/services"; import { EELogService } from "../../ee/services";
import { getUserAgentType } from "../../utils/posthog"; import { getUserAgentType } from "../../utils/posthog";
import { import {
getAuthSecret,
getHttpsEnabled, getHttpsEnabled,
getJwtAuthLifetime, getJwtAuthLifetime
getJwtAuthSecret,
getJwtRefreshSecret
} from "../../config"; } from "../../config";
import { ActorType } from "../../ee/models"; import { ActorType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
@@ -238,6 +241,7 @@ export const checkAuth = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getNewToken = async (req: Request, res: Response) => { export const getNewToken = async (req: Request, res: Response) => {
const refreshToken = req.cookies.jid; const refreshToken = req.cookies.jid;
if (!refreshToken) if (!refreshToken)
@@ -245,7 +249,9 @@ export const getNewToken = async (req: Request, res: Response) => {
message: "Failed to find refresh token in request cookies" 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 getAuthSecret());
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) throw UnauthorizedRequestError();
const user = await User.findOne({ const user = await User.findOne({
_id: decodedToken.userId _id: decodedToken.userId
@@ -268,12 +274,13 @@ export const getNewToken = async (req: Request, res: Response) => {
const token = createToken({ const token = createToken({
payload: { payload: {
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: decodedToken.userId, userId: decodedToken.userId,
tokenVersionId: tokenVersion._id.toString(), tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.refreshVersion accessVersion: tokenVersion.refreshVersion
}, },
expiresIn: await getJwtAuthLifetime(), expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret() secret: await getAuthSecret()
}); });
return res.status(200).send({ return res.status(200).send({

View File

@@ -16,7 +16,6 @@ import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController"; import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController"; import * as webhookController from "./webhookController";
import * as secretImpsController from "./secretImpsController"; import * as secretImpsController from "./secretImpsController";
export { export {
authController, authController,
botController, botController,

View File

@@ -12,6 +12,7 @@ import {
INTEGRATION_BITBUCKET_API_URL, INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_NORTHFLANK_API_URL, INTEGRATION_NORTHFLANK_API_URL,
INTEGRATION_QOVERY_API_URL,
INTEGRATION_RAILWAY_API_URL, INTEGRATION_RAILWAY_API_URL,
INTEGRATION_SET, INTEGRATION_SET,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
@@ -344,6 +345,362 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon
}); });
}; };
/**
* Return list of Qovery Orgs for a specific user
* @param req
* @param res
*/
export const getIntegrationAuthQoveryOrgs = async (req: Request, res: Response) => {
const {
params: { integrationAuthId }
} = await validateRequest(reqValidator.GetIntegrationAuthQoveryOrgsV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/organization`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
interface QoveryOrg {
id: string;
name: string;
}
const orgs = data.results.map((a: QoveryOrg) => {
return {
name: a.name,
orgId: a.id,
};
});
return res.status(200).send({
orgs
});
};
/**
* Return list of Qovery Projects for a specific orgId
* @param req
* @param res
*/
export const getIntegrationAuthQoveryProjects = async (req: Request, res: Response) => {
const {
params: { integrationAuthId },
query: { orgId }
} = await validateRequest(reqValidator.GetIntegrationAuthQoveryProjectsV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface Project {
name: string;
projectId: string;
}
interface QoveryProject {
id: string;
name: string;
}
let projects: Project[] = [];
if (orgId && orgId !== "") {
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/organization/${orgId}/project`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
projects = data.results.map((a: QoveryProject) => {
return {
name: a.name,
projectId: a.id,
};
});
}
return res.status(200).send({
projects
});
};
/**
* Return list of Qovery environments for project with id [projectId]
* @param req
* @param res
*/
export const getIntegrationAuthQoveryEnvironments = async (req: Request, res: Response) => {
const {
params: { integrationAuthId },
query: { projectId }
} = await validateRequest(reqValidator.GetIntegrationAuthQoveryEnvironmentsV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface Environment {
name: string;
environmentId: string;
}
interface QoveryEnvironment {
id: string;
name: string;
}
let environments: Environment[] = [];
if (projectId && projectId !== "" && projectId !== "none") { // TODO: fix
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/project/${projectId}/environment`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
environments = data.results.map((a: QoveryEnvironment) => {
return {
name: a.name,
environmentId: a.id,
};
});
}
return res.status(200).send({
environments
});
};
/**
* Return list of Qovery apps for environment with id [environmentId]
* @param req
* @param res
*/
export const getIntegrationAuthQoveryApps = async (req: Request, res: Response) => {
const {
params: { integrationAuthId },
query: { environmentId }
} = await validateRequest(reqValidator.GetIntegrationAuthQoveryScopesV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface App {
name: string;
appId: string;
}
interface QoveryApp {
id: string;
name: string;
}
let apps: App[] = [];
if (environmentId && environmentId !== "") {
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/environment/${environmentId}/application`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
apps = data.results.map((a: QoveryApp) => {
return {
name: a.name,
appId: a.id,
};
});
}
return res.status(200).send({
apps
});
};
/**
* Return list of Qovery containers for environment with id [environmentId]
* @param req
* @param res
*/
export const getIntegrationAuthQoveryContainers = async (req: Request, res: Response) => {
const {
params: { integrationAuthId },
query: { environmentId }
} = await validateRequest(reqValidator.GetIntegrationAuthQoveryScopesV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface Container {
name: string;
appId: string;
}
interface QoveryContainer {
id: string;
name: string;
}
let containers: Container[] = [];
if (environmentId && environmentId !== "") {
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/environment/${environmentId}/container`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
containers = data.results.map((a: QoveryContainer) => {
return {
name: a.name,
appId: a.id,
};
});
}
return res.status(200).send({
containers
});
};
/**
* Return list of Qovery jobs for environment with id [environmentId]
* @param req
* @param res
*/
export const getIntegrationAuthQoveryJobs = async (req: Request, res: Response) => {
const {
params: { integrationAuthId },
query: { environmentId }
} = await validateRequest(reqValidator.GetIntegrationAuthQoveryScopesV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface Job {
name: string;
appId: string;
}
interface QoveryJob {
id: string;
name: string;
}
let jobs: Job[] = [];
if (environmentId && environmentId !== "") {
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/environment/${environmentId}/job`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
jobs = data.results.map((a: QoveryJob) => {
return {
name: a.name,
appId: a.id,
};
});
}
return res.status(200).send({
jobs
});
};
/** /**
* Return list of Railway environments for Railway project with * Return list of Railway environments for Railway project with
* id [appId] * id [appId]
@@ -713,11 +1070,12 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res
*/ */
export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: Response) => { export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: Response) => {
const { const {
params: { integrationAuthId, appId } params: { integrationAuthId },
query: { appId }
} = await validateRequest(reqValidator.GetIntegrationAuthTeamCityBuildConfigsV1, req); } = await validateRequest(reqValidator.GetIntegrationAuthTeamCityBuildConfigsV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth } = await getIntegrationAuthAccessHelper({ const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId) integrationAuthId: new ObjectId(integrationAuthId)
}); });
@@ -749,13 +1107,13 @@ export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res:
const { const {
data: { buildType } data: { buildType }
} = await standardRequest.get<GetTeamCityBuildConfigsRes>( } = await standardRequest.get<GetTeamCityBuildConfigsRes>(
`${req.integrationAuth.url}/app/rest/buildTypes`, `${integrationAuth.url}/app/rest/buildTypes`,
{ {
params: { params: {
locator: `project:${appId}` locator: `project:${appId}`
}, },
headers: { headers: {
Authorization: `Bearer ${req.accessToken}`, Authorization: `Bearer ${accessToken}`,
Accept: "application/json" Accept: "application/json"
} }
} }

View File

@@ -34,6 +34,7 @@ export const createIntegration = async (req: Request, res: Response) => {
appId, appId,
owner, owner,
region, region,
scope,
targetService, targetService,
targetServiceId, targetServiceId,
integrationAuthId, integrationAuthId,
@@ -90,6 +91,7 @@ export const createIntegration = async (req: Request, res: Response) => {
owner, owner,
path, path,
region, region,
scope,
secretPath, secretPath,
integration: integrationAuth.integration, integration: integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId), integrationAuth: new Types.ObjectId(integrationAuthId),

View File

@@ -1,7 +1,7 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models"; import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models";
import { EventType } from "../../ee/models"; import { EventType, Role } from "../../ee/models";
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership"; import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
import { sendMail } from "../../helpers/nodemailer"; import { sendMail } from "../../helpers/nodemailer";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables"; import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
@@ -15,7 +15,6 @@ import {
getUserProjectPermissions getUserProjectPermissions
} from "../../ee/services/ProjectRoleService"; } from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import Role from "../../ee/models/role";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
import { InviteUserToWorkspaceV1 } from "../../validation/workspace"; import { InviteUserToWorkspaceV1 } from "../../validation/workspace";

View File

@@ -8,11 +8,11 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
import { sendMail } from "../../helpers/nodemailer"; import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services"; import { TokenService } from "../../services";
import { EELicenseService } from "../../ee/services"; import { EELicenseService } from "../../ee/services";
import { ACCEPTED, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables"; import { ACCEPTED, AuthTokenType, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables";
import * as reqValidator from "../../validation/membershipOrg"; import * as reqValidator from "../../validation/membershipOrg";
import { import {
getAuthSecret,
getJwtSignupLifetime, getJwtSignupLifetime,
getJwtSignupSecret,
getSiteURL, getSiteURL,
getSmtpConfigured getSmtpConfigured
} from "../../config"; } from "../../config";
@@ -272,10 +272,11 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
// generate temporary signup token // generate temporary signup token
const token = createToken({ const token = createToken({
payload: { payload: {
authTokenType: AuthTokenType.SIGNUP_TOKEN,
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: await getJwtSignupLifetime(), expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret() secret: await getAuthSecret()
}); });
return res.status(200).send({ return res.status(200).send({

View File

@@ -6,13 +6,11 @@ import {
Organization, Organization,
Workspace Workspace
} from "../../models"; } from "../../models";
import { createOrganization as create } from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { ACCEPTED, ADMIN } from "../../variables";
import { getLicenseServerUrl, getSiteURL } from "../../config"; import { getLicenseServerUrl, getSiteURL } from "../../config";
import { licenseServerKeyRequest } from "../../config/request"; import { licenseServerKeyRequest } from "../../config/request";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/organization"; import * as reqValidator from "../../validation/organization";
import { ACCEPTED } from "../../variables";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
@@ -34,36 +32,6 @@ export const getOrganizations = async (req: Request, res: Response) => {
}); });
}; };
/**
* Create new organization named [organizationName]
* and add user as owner
* @param req
* @param res
* @returns
*/
export const createOrganization = async (req: Request, res: Response) => {
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
});
await addMembershipsOrg({
userIds: [req.user._id.toString()],
organizationId: organization._id.toString(),
roles: [ADMIN],
statuses: [ACCEPTED]
});
return res.status(200).send({
organization
});
};
/** /**
* Return organization with id [organizationId] * Return organization with id [organizationId]
* @param req * @param req

View File

@@ -5,12 +5,12 @@ import * as bigintConversion from "bigint-conversion";
import { BackupPrivateKey, LoginSRPDetail, User } from "../../models"; import { BackupPrivateKey, LoginSRPDetail, User } from "../../models";
import { clearTokens, createToken, sendMail } from "../../helpers"; import { clearTokens, createToken, sendMail } from "../../helpers";
import { TokenService } from "../../services"; import { TokenService } from "../../services";
import { TOKEN_EMAIL_PASSWORD_RESET } from "../../variables"; import { AuthTokenType, TOKEN_EMAIL_PASSWORD_RESET } from "../../variables";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
import { import {
getAuthSecret,
getHttpsEnabled, getHttpsEnabled,
getJwtSignupLifetime, getJwtSignupLifetime,
getJwtSignupSecret,
getSiteURL getSiteURL
} from "../../config"; } from "../../config";
import { ActorType } from "../../ee/models"; import { ActorType } from "../../ee/models";
@@ -88,10 +88,11 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
// generate temporary password-reset token // generate temporary password-reset token
const token = createToken({ const token = createToken({
payload: { payload: {
authTokenType: AuthTokenType.SIGNUP_TOKEN,
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: await getJwtSignupLifetime(), expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret() secret: await getAuthSecret()
}); });
return res.status(200).send({ return res.status(200).send({

View File

@@ -2,7 +2,7 @@ import { Request, Response } from "express";
import { isValidScope } from "../../helpers"; import { isValidScope } from "../../helpers";
import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models"; import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models";
import { getAllImportedSecrets } from "../../services/SecretImportService"; import { getAllImportedSecrets } from "../../services/SecretImportService";
import { getFolderWithPathFromId } from "../../services/FolderService"; import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
import { import {
BadRequestError, BadRequestError,
ResourceNotFoundError, ResourceNotFoundError,
@@ -95,37 +95,12 @@ export const createSecretImp = async (req: Request, res: Response) => {
*/ */
const { const {
body: { workspaceId, environment, folderId, secretImport } body: { workspaceId, environment, directory, secretImport }
} = await validateRequest(reqValidator.CreateSecretImportV1, req); } = await validateRequest(reqValidator.CreateSecretImportV1, req);
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) { if (req.authData.authPayload instanceof ServiceTokenData) {
// root check // root check
let isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
isValidScopeAccess = isValidScope(
req.authData.authPayload,
secretImport.environment,
secretImport.secretPath
);
if (!isValidScopeAccess) { if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
@@ -133,27 +108,31 @@ export const createSecretImp = async (req: Request, res: Response) => {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: secretImport.environment,
secretPath: secretImport.secretPath
})
); );
} }
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({ const importSecDoc = await SecretImport.findOne({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folderId folderId
}); });
const importToSecretPath = folders
? getFolderWithPathFromId(folders.nodes, folderId).folderPath
: "/";
if (!importSecDoc) { if (!importSecDoc) {
const doc = new SecretImport({ const doc = new SecretImport({
workspace: workspaceId, workspace: workspaceId,
@@ -173,7 +152,7 @@ export const createSecretImp = async (req: Request, res: Response) => {
importFromEnvironment: secretImport.environment, importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath, importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment, importToEnvironment: environment,
importToSecretPath importToSecretPath: directory
} }
}, },
{ {
@@ -206,7 +185,7 @@ export const createSecretImp = async (req: Request, res: Response) => {
importFromEnvironment: secretImport.environment, importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath, importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment, importToEnvironment: environment,
importToSecretPath importToSecretPath: directory
} }
}, },
{ {
@@ -563,8 +542,38 @@ export const getSecretImports = async (req: Request, res: Response) => {
} }
*/ */
const { const {
query: { workspaceId, environment, folderId } query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetSecretImportsV1, req); } = 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({ const importSecDoc = await SecretImport.findOne({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
@@ -575,41 +584,6 @@ export const getSecretImports = async (req: Request, res: Response) => {
return res.status(200).json({ secretImport: {} }); return res.status(200).json({ secretImport: {} });
} }
// 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.Read,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
}
return res.status(200).json({ secretImport: importSecDoc }); return res.status(200).json({ secretImport: importSecDoc });
}; };
@@ -621,9 +595,39 @@ export const getSecretImports = async (req: Request, res: Response) => {
*/ */
export const getAllSecretsFromImport = async (req: Request, res: Response) => { export const getAllSecretsFromImport = async (req: Request, res: Response) => {
const { const {
query: { workspaceId, environment, folderId } query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req); } = 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({ const importSecDoc = await SecretImport.findOne({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
@@ -634,11 +638,6 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => {
return res.status(200).json({ secrets: [] }); return res.status(200).json({ secrets: [] });
} }
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/"; let secretPath = "/";
if (folders) { if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);

View File

@@ -1,12 +1,15 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import GitAppInstallationSession from "../../ee/models/gitAppInstallationSession"; import {
GitAppInstallationSession,
GitAppOrganizationInstallation,
GitRisks
} from "../../ee/models";
import crypto from "crypto"; import crypto from "crypto";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors"; import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
import GitAppOrganizationInstallation from "../../ee/models/gitAppOrganizationInstallation";
import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository"; import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository";
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
import GitRisks, { import {
STATUS_RESOLVED_FALSE_POSITIVE, STATUS_RESOLVED_FALSE_POSITIVE,
STATUS_RESOLVED_NOT_REVOKED, STATUS_RESOLVED_NOT_REVOKED,
STATUS_RESOLVED_REVOKED STATUS_RESOLVED_REVOKED

View File

@@ -9,12 +9,9 @@ import { Secret, ServiceTokenData } from "../../models";
import { Folder } from "../../models/folder"; import { Folder } from "../../models/folder";
import { import {
appendFolder, appendFolder,
deleteFolderById,
generateFolderId,
getAllFolderIds, getAllFolderIds,
getFolderByPath, getFolderByPath,
getFolderWithPathFromId, getFolderWithPathFromId,
getParentFromFolderId,
validateFolderName validateFolderName
} from "../../services/FolderService"; } from "../../services/FolderService";
import { import {
@@ -25,13 +22,9 @@ import {
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/folders"; import * as reqValidator from "../../validation/folders";
/** const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "The folder doesn't exist" });
* Create folder with name [folderName] for workspace with id [workspaceId]
* and environment [environment] // verify workspace id/environment
* @param req
* @param res
* @returns
*/
export const createFolder = async (req: Request, res: Response) => { export const createFolder = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Create a folder' #swagger.summary = 'Create a folder'
@@ -107,7 +100,7 @@ export const createFolder = async (req: Request, res: Response) => {
} }
*/ */
const { const {
body: { workspaceId, environment, folderName, parentFolderId } body: { workspaceId, environment, folderName, directory }
} = await validateRequest(reqValidator.CreateFolderV1, req); } = await validateRequest(reqValidator.CreateFolderV1, req);
if (!validateFolderName(folderName)) { if (!validateFolderName(folderName)) {
@@ -116,35 +109,28 @@ export const createFolder = async (req: Request, res: Response) => {
}); });
} }
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({ const folders = await Folder.findOne({
workspace: workspaceId, workspace: workspaceId,
environment environment
}).lean(); }).lean();
if (req.user) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const secretPath =
folders && parentFolderId
? getFolderWithPathFromId(folders.nodes, parentFolderId).folderPath
: "/";
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
// space has no folders initialized // space has no folders initialized
if (!folders) { 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" });
}
}
const id = generateFolderId();
const folder = new Folder({ const folder = new Folder({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
@@ -152,14 +138,15 @@ export const createFolder = async (req: Request, res: Response) => {
id: "root", id: "root",
name: "root", name: "root",
version: 1, version: 1,
children: [{ id, name: folderName, children: [], version: 1 }] children: []
} }
}); });
const { parent, child } = appendFolder(folder.nodes, { folderName, directory });
await folder.save(); await folder.save();
const folderVersion = new FolderVersion({ const folderVersion = new FolderVersion({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
nodes: folder.nodes nodes: parent
}); });
await folderVersion.save(); await folderVersion.save();
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
@@ -173,9 +160,9 @@ export const createFolder = async (req: Request, res: Response) => {
type: EventType.CREATE_FOLDER, type: EventType.CREATE_FOLDER,
metadata: { metadata: {
environment, environment,
folderId: id, folderId: child.id,
folderName, folderName,
folderPath: `root/${folderName}` folderPath: directory
} }
}, },
{ {
@@ -183,56 +170,37 @@ export const createFolder = async (req: Request, res: Response) => {
} }
); );
return res.json({ folder: { id, name: folderName } }); return res.json({ folder: { id: child.id, name: folderName } });
} }
const folder = appendFolder(folders.nodes, { folderName, parentFolderId }); const { parent, child, hasCreated } = appendFolder(folders.nodes, { folderName, directory });
await Folder.findByIdAndUpdate(folders._id, folders); if (!hasCreated) return res.json({ folder: child });
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" });
}
}
await Folder.findByIdAndUpdate(folders._id, folders); await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({ const folderVersion = new FolderVersion({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
nodes: parentFolder nodes: parent
}); });
await folderVersion.save(); await folderVersion.save();
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folderId: parentFolderId folderId: child.id
}); });
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
{ {
type: EventType.CREATE_FOLDER, type: EventType.CREATE_FOLDER,
metadata: { metadata: {
environment, environment,
folderId: folder.id, folderId: child.id,
folderName, folderName,
folderPath folderPath: directory
} }
}, },
{ {
@@ -240,7 +208,7 @@ export const createFolder = async (req: Request, res: Response) => {
} }
); );
return res.json({ folder }); return res.json({ folder: child });
}; };
/** /**
@@ -332,8 +300,8 @@ export const updateFolderById = async (req: Request, res: Response) => {
} }
*/ */
const { const {
body: { workspaceId, environment, name }, body: { workspaceId, environment, name, directory },
params: { folderId } params: { folderName }
} = await validateRequest(reqValidator.UpdateFolderV1, req); } = await validateRequest(reqValidator.UpdateFolderV1, req);
if (!validateFolderName(name)) { if (!validateFolderName(name)) {
@@ -342,38 +310,31 @@ export const updateFolderById = async (req: Request, res: Response) => {
}); });
} }
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 }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) { if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" }); throw BadRequestError({ message: "The folder doesn't exist" });
} }
const parentFolder = getParentFromFolderId(folders.nodes, folderId); const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) { if (!parentFolder) {
throw BadRequestError({ message: "The folder doesn't exist" }); throw BadRequestError({ message: "The folder doesn't exist" });
} }
if (req.user) { const folder = parentFolder.children.find(({ name }) => name === folderName);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); if (!folder) throw ERR_FOLDER_NOT_FOUND;
const secretPath = getFolderWithPathFromId(folders.nodes, parentFolder.id).folderPath;
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
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 oldFolderName = folder.name; const oldFolderName = folder.name;
parentFolder.version += 1; parentFolder.version += 1;
@@ -505,24 +466,12 @@ export const deleteFolder = async (req: Request, res: Response) => {
} }
*/ */
const { const {
params: { folderId }, params: { folderName },
body: { environment, workspaceId } body: { environment, workspaceId, directory }
} = await validateRequest(reqValidator.DeleteFolderV1, req); } = await validateRequest(reqValidator.DeleteFolderV1, req);
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const delOp = deleteFolderById(folders.nodes, folderId);
if (!delOp) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const { deletedNode: delFolder, parent: parentFolder } = delOp;
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) { if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
@@ -531,12 +480,23 @@ export const deleteFolder = async (req: Request, res: Response) => {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) 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; parentFolder.version += 1;
const delFolderIds = getAllFolderIds(delFolder); const delFolderIds = getAllFolderIds(deletedFolder);
await Folder.findByIdAndUpdate(folders._id, folders); await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({ const folderVersion = new FolderVersion({
@@ -565,9 +525,9 @@ export const deleteFolder = async (req: Request, res: Response) => {
type: EventType.DELETE_FOLDER, type: EventType.DELETE_FOLDER,
metadata: { metadata: {
environment, environment,
folderId, folderId: deletedFolder.id,
folderName: delFolder.name, folderName: deletedFolder.name,
folderPath: secretPath folderPath: directory
} }
}, },
{ {
@@ -575,7 +535,7 @@ 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 });
}; };
/** /**
@@ -677,69 +637,27 @@ export const getFolders = async (req: Request, res: Response) => {
} }
*/ */
const { const {
query: { workspaceId, environment, parentFolderId, parentFolderPath } query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetFoldersV1, req); } = await validateRequest(reqValidator.GetFoldersV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
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);
}
const folders = await Folder.findOne({ workspace: workspaceId, environment }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (req.user) await getUserProjectPermissions(req.user._id, workspaceId);
if (!folders) { if (!folders) {
res.send({ folders: [], dir: [] }); return res.send({ folders: [], dir: [] });
return;
} }
// if instead of parentFolderId given a path like /folder1/folder2 const folder = getFolderByPath(folders.nodes, directory);
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) { return res.send({
res.send({ folders: [], dir: [] }); folders: folder?.children?.map(({ id, name }) => ({ id, name })) || []
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" });
}
}
const rootFolders = folders.nodes.children.map(({ id, name }) => ({
id,
name
}));
res.send({ folders: rootFolders });
return;
}
const { folder, folderPath, dir } = getFolderWithPathFromId(folders.nodes, parentFolderId);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, folderPath);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir
}); });
}; };

View File

@@ -4,14 +4,15 @@ import { checkEmailVerification, sendEmailVerification } from "../../helpers/sig
import { createToken } from "../../helpers/auth"; import { createToken } from "../../helpers/auth";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
import { import {
getAuthSecret,
getInviteOnlySignup, getInviteOnlySignup,
getJwtSignupLifetime, getJwtSignupLifetime,
getJwtSignupSecret,
getSmtpConfigured getSmtpConfigured
} from "../../config"; } from "../../config";
import { validateUserEmail } from "../../validation"; import { validateUserEmail } from "../../validation";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth"; import * as reqValidator from "../../validation/auth";
import { AuthTokenType } from "../../variables";
/** /**
* Signup step 1: Initialize account for user under email [email] and send a verification code * Signup step 1: Initialize account for user under email [email] and send a verification code
@@ -95,10 +96,11 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
// generate temporary signup token // generate temporary signup token
const token = createToken({ const token = createToken({
payload: { payload: {
authTokenType: AuthTokenType.SIGNUP_TOKEN,
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: await getJwtSignupLifetime(), expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret() secret: await getAuthSecret()
}); });
return res.status(200).send({ return res.status(200).send({

View File

@@ -202,12 +202,12 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
); );
// delete workspace // delete workspace
await deleteWork({ const workspace = await deleteWork({
id: workspaceId workspaceId: new Types.ObjectId(workspaceId)
}); });
return res.status(200).send({ return res.status(200).send({
message: "Successfully deleted workspace" workspace
}); });
}; };

View File

@@ -10,9 +10,9 @@ import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services"; import { TokenService } from "../../services";
import { EELogService } from "../../ee/services"; import { EELogService } from "../../ee/services";
import { BadRequestError, InternalServerError } from "../../utils/errors"; import { BadRequestError, InternalServerError } from "../../utils/errors";
import { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables"; import { ACTION_LOGIN, AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables";
import { getUserAgentType } from "../../utils/posthog"; // TODO: move this import { getUserAgentType } from "../../utils/posthog"; // TODO: move this
import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config"; import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth"; import * as reqValidator from "../../validation/auth";
@@ -109,10 +109,11 @@ export const login2 = async (req: Request, res: Response) => {
// generate temporary MFA token // generate temporary MFA token
const token = createToken({ const token = createToken({
payload: { payload: {
authTokenType: AuthTokenType.MFA_TOKEN,
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: await getJwtMfaLifetime(), expiresIn: await getJwtMfaLifetime(),
secret: await getJwtMfaSecret() secret: await getAuthSecret()
}); });
const code = await TokenService.createToken({ const code = await TokenService.createToken({

View File

@@ -1,6 +1,7 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import {
Folder,
Integration, Integration,
Membership, Membership,
Secret, Secret,
@@ -21,6 +22,9 @@ import {
getUserProjectPermissions getUserProjectPermissions
} from "../../ee/services/ProjectRoleService"; } from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { SecretImport } from "../../models";
import { ServiceAccountWorkspacePermission } from "../../models";
import { Webhook } from "../../models";
/** /**
* Create new workspace environment named [environmentName] * Create new workspace environment named [environmentName]
@@ -369,13 +373,43 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
{ environment: environmentSlug } { environment: environmentSlug }
); );
await ServiceTokenData.updateMany( 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( await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug }, { workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug } { environment: environmentSlug }
); );
await Folder.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretImport.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretImport.updateMany(
{ workspace: workspaceId, "imports.environment": oldEnvironmentSlug },
{ $set: { "imports.$[element].environment": environmentSlug } },
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] },
);
await ServiceAccountWorkspacePermission.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Webhook.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Membership.updateMany( await Membership.updateMany(
{ {
workspace: workspaceId, workspace: workspaceId,

View File

@@ -1,11 +1,25 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Membership, MembershipOrg, ServiceAccount, Workspace } from "../../models"; import {
Membership,
MembershipOrg,
ServiceAccount,
Workspace
} from "../../models";
import { Role } from "../../ee/models";
import { deleteMembershipOrg } from "../../helpers/membershipOrg"; import { deleteMembershipOrg } from "../../helpers/membershipOrg";
import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; import {
import Role from "../../ee/models/role"; createOrganization as create,
import { BadRequestError } from "../../utils/errors"; deleteOrganization,
import { CUSTOM } from "../../variables"; updateSubscriptionOrgQuantity
} from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import {
ACCEPTED,
ADMIN,
CUSTOM
} from "../../variables";
import * as reqValidator from "../../validation/organization"; import * as reqValidator from "../../validation/organization";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import { import {
@@ -332,3 +346,60 @@ export const getOrganizationServiceAccounts = async (req: Request, res: Response
serviceAccounts serviceAccounts
}); });
}; };
/**
* Create new organization named [organizationName]
* and add user as owner
* @param req
* @param res
* @returns
*/
export const createOrganization = async (req: Request, res: Response) => {
const {
body: { name }
} = await validateRequest(reqValidator.CreateOrgv2, req);
// create organization and add user as member
const organization = await create({
email: req.user.email,
name
});
await addMembershipsOrg({
userIds: [req.user._id.toString()],
organizationId: organization._id.toString(),
roles: [ADMIN],
statuses: [ACCEPTED]
});
return res.status(200).send({
organization
});
};
/**
* Delete organization with id [organizationId]
* @param req
* @param res
*/
export const deleteOrganizationById = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.DeleteOrgv2, req);
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: new Types.ObjectId(organizationId),
role: ADMIN
});
if (!membershipOrg) throw UnauthorizedRequestError();
const organization = await deleteOrganization({
organizationId: new Types.ObjectId(organizationId)
});
return res.status(200).send({
organization
});
}

View File

@@ -5,49 +5,9 @@ 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 { getSaltRounds } from "../../config";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import { deleteUser } from "../../helpers/user";
import * as reqValidator from "../../validation"; import * as reqValidator from "../../validation";
/**
* Return the current user.
* @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"
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"user": {
"type": "object",
$ref: "#/components/schemas/CurrentUser",
"description": "Current user on request"
}
}
}
}
}
}
*/
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]. * Update the current user's MFA-enabled status [isMfaEnabled].
* Note: Infisical currently only supports email-based 2FA only; this will expand to * Note: Infisical currently only supports email-based 2FA only; this will expand to
@@ -296,3 +256,59 @@ export const deleteMySessions = async (req: Request, res: Response) => {
message: "Successfully revoked all sessions" message: "Successfully revoked all sessions"
}); });
}; };
/**
* Return the current user.
* @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"
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"user": {
"type": "object",
$ref: "#/components/schemas/CurrentUser",
"description": "Current user on request"
}
}
}
}
}
}
*/
const user = await User.findById(req.user._id).select(
"+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"
);
return res.status(200).send({
user
});
};
/**
* Delete the current user.
* @param req
* @param res
*/
export const deleteMe = async (req: Request, res: Response) => {
const user = await deleteUser({
userId: req.user._id
});
return res.status(200).send({
user
});
}

View File

@@ -10,9 +10,9 @@ import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services"; import { TokenService } from "../../services";
import { EELogService } from "../../ee/services"; import { EELogService } from "../../ee/services";
import { BadRequestError, InternalServerError } from "../../utils/errors"; import { BadRequestError, InternalServerError } from "../../utils/errors";
import { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables"; import { ACTION_LOGIN, AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables";
import { getUserAgentType } from "../../utils/posthog"; // TODO: move this import { getUserAgentType } from "../../utils/posthog"; // TODO: move this
import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config"; import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config";
import { AuthMethod } from "../../models/user"; import { AuthMethod } from "../../models/user";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth"; import * as reqValidator from "../../validation/auth";
@@ -134,10 +134,11 @@ export const login2 = async (req: Request, res: Response) => {
// generate temporary MFA token // generate temporary MFA token
const token = createToken({ const token = createToken({
payload: { payload: {
authTokenType: AuthTokenType.MFA_TOKEN,
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: await getJwtMfaLifetime(), expiresIn: await getJwtMfaLifetime(),
secret: await getJwtMfaSecret() secret: await getAuthSecret()
}); });
const code = await TokenService.createToken({ const code = await TokenService.createToken({

View File

@@ -7,5 +7,5 @@ export {
authController, authController,
secretsController, secretsController,
signupController, signupController,
workspacesController, workspacesController
} }

View File

@@ -3,10 +3,11 @@ import { Types } from "mongoose";
import { EventService, SecretService } from "../../services"; import { EventService, SecretService } from "../../services";
import { eventPushSecrets } from "../../events"; import { eventPushSecrets } from "../../events";
import { BotService } from "../../services"; import { BotService } from "../../services";
import { containsGlobPatterns, isValidScope, repackageSecretToRaw } from "../../helpers/secrets"; import { containsGlobPatterns, isValidScopeV3, repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService"; import { getAllImportedSecrets } from "../../services/SecretImportService";
import { Folder, IServiceTokenData } from "../../models"; import { Folder, IMembership, IServiceTokenData, IServiceTokenDataV3 } from "../../models";
import { Permission } from "../../models/serviceTokenDataV3";
import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService"; import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
@@ -17,8 +18,115 @@ import {
getUserProjectPermissions getUserProjectPermissions
} from "../../ee/services/ProjectRoleService"; } from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { validateServiceTokenDataClientForWorkspace } from "../../validation"; import {
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataV3ClientForWorkspace
} from "../../validation";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
import { ActorType } from "../../ee/models";
import { UnauthorizedRequestError } from "../../utils/errors";
import { AuthData } from "../../interfaces/middleware";
import {
generateSecretApprovalRequest,
getSecretPolicyOfBoard
} from "../../ee/services/SecretApprovalService";
import { CommitType } from "../../ee/models/secretApprovalRequest";
import { IRole } from "../../ee/models/role";
const checkSecretsPermission = async ({
authData,
workspaceId,
environment,
secretPath,
secretAction
}: {
authData: AuthData;
workspaceId: string;
environment: string;
secretPath: string;
secretAction: ProjectPermissionActions; // CRUD
}): Promise<{
authVerifier: (env: string, secPath: string) => boolean;
membership?: Omit<IMembership, "customRole"> & { customRole: IRole };
}> => {
let STV2RequiredPermissions = [];
let STV3RequiredPermissions: Permission[] = [];
switch (secretAction) {
case ProjectPermissionActions.Create:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
case ProjectPermissionActions.Read:
STV2RequiredPermissions = [PERMISSION_READ_SECRETS];
STV3RequiredPermissions = [Permission.READ];
break;
case ProjectPermissionActions.Edit:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
case ProjectPermissionActions.Delete:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
}
switch (authData.actor.type) {
case ActorType.USER: {
const { permission, membership } = await getUserProjectPermissions(
authData.actor.metadata.userId,
workspaceId
);
ForbiddenError.from(permission).throwUnlessCan(
secretAction,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
return {
authVerifier: (env: string, secPath: string) =>
permission.can(
secretAction,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
),
membership
};
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV2RequiredPermissions
});
return { authVerifier: () => true };
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData,
serviceTokenData: authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV3RequiredPermissions
});
return {
authVerifier: (env: string, secPath: string) =>
isValidScopeV3({
authPayload: authData.authPayload as IServiceTokenDataV3,
environment: env,
secretPath: secPath,
requiredPermissions: STV3RequiredPermissions
})
};
}
default: {
throw UnauthorizedRequestError();
}
}
};
/** /**
* Return secrets for workspace with id [workspaceId] and environment * Return secrets for workspace with id [workspaceId] and environment
@@ -58,32 +166,13 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
if (!environment || !workspaceId) if (!environment || !workspaceId)
throw BadRequestError({ message: "Missing environment or workspace id" }); throw BadRequestError({ message: "Missing environment or workspace id" });
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret const { authVerifier: permissionCheckFn } = await checkSecretsPermission({
if (req.user?._id) { authData: req.authData,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); 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, environment,
secretPath, secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS] secretAction: ProjectPermissionActions.Read
}); });
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
}
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -146,25 +235,17 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
*/ */
export const getSecretByNameRaw = async (req: Request, res: Response) => { export const getSecretByNameRaw = async (req: Request, res: Response) => {
const { const {
query: { secretPath, environment, workspaceId, type }, query: { secretPath, environment, workspaceId, type, include_imports },
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameRawV3, req); } = await validateRequest(reqValidator.GetSecretByNameRawV3, req);
if (req.user?._id) { await checkSecretsPermission({
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); authData: req.authData,
ForbiddenError.from(permission).throwUnlessCan( workspaceId,
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS] secretAction: ProjectPermissionActions.Read
}); });
}
const secret = await SecretService.getSecret({ const secret = await SecretService.getSecret({
secretName, secretName,
@@ -172,7 +253,8 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
environment, environment,
type, type,
secretPath, secretPath,
authData: req.authData authData: req.authData,
include_imports
}); });
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
@@ -195,24 +277,24 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
export const createSecretRaw = async (req: Request, res: Response) => { export const createSecretRaw = async (req: Request, res: Response) => {
const { const {
params: { secretName }, params: { secretName },
body: { secretPath, environment, workspaceId, type, secretValue, secretComment } body: {
secretPath,
environment,
workspaceId,
type,
secretValue,
secretComment,
skipMultilineEncoding
}
} = await validateRequest(reqValidator.CreateSecretRawV3, req); } = await validateRequest(reqValidator.CreateSecretRawV3, req);
if (req.user?._id) { await checkSecretsPermission({
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); authData: req.authData,
ForbiddenError.from(permission).throwUnlessCan( workspaceId,
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS] secretAction: ProjectPermissionActions.Create
}); });
}
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
@@ -248,7 +330,8 @@ export const createSecretRaw = async (req: Request, res: Response) => {
secretPath, secretPath,
secretCommentCiphertext: secretCommentEncrypted.ciphertext, secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv, secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag secretCommentTag: secretCommentEncrypted.tag,
skipMultilineEncoding
}); });
await EventService.handleEvent({ await EventService.handleEvent({
@@ -278,24 +361,16 @@ export const createSecretRaw = async (req: Request, res: Response) => {
export const updateSecretByNameRaw = async (req: Request, res: Response) => { export const updateSecretByNameRaw = async (req: Request, res: Response) => {
const { const {
params: { secretName }, params: { secretName },
body: { secretValue, environment, secretPath, type, workspaceId } body: { secretValue, environment, secretPath, type, workspaceId, skipMultilineEncoding }
} = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req); } = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req);
if (req.user?._id) { await checkSecretsPermission({
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); authData: req.authData,
ForbiddenError.from(permission).throwUnlessCan( workspaceId,
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS] secretAction: ProjectPermissionActions.Edit
}); });
}
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
@@ -315,7 +390,8 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
secretValueCiphertext: secretValueEncrypted.ciphertext, secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv, secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag, secretValueTag: secretValueEncrypted.tag,
secretPath secretPath,
skipMultilineEncoding
}); });
await EventService.handleEvent({ await EventService.handleEvent({
@@ -345,21 +421,13 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
body: { environment, secretPath, type, workspaceId } body: { environment, secretPath, type, workspaceId }
} = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req); } = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req);
if (req.user?._id) { await checkSecretsPermission({
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); authData: req.authData,
ForbiddenError.from(permission).throwUnlessCan( workspaceId,
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS] secretAction: ProjectPermissionActions.Delete
}); });
}
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
@@ -408,37 +476,18 @@ export const getSecrets = async (req: Request, res: Response) => {
if (folderId && folderId !== "root") { if (folderId && folderId !== "root") {
const folder = await Folder.findOne({ workspace: workspaceId, environment }); const folder = await Folder.findOne({ workspace: workspaceId, environment });
if (!folder) throw BadRequestError({ message: "Folder not found" }); if (!folder) return res.send({ secrets: [] });
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath; secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
} }
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret const { authVerifier: permissionCheckFn } = await checkSecretsPermission({
if (req.user?._id) { authData: req.authData,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); 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, environment,
secretPath, secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS] secretAction: ProjectPermissionActions.Read
}); });
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
}
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -483,25 +532,17 @@ export const getSecrets = async (req: Request, res: Response) => {
*/ */
export const getSecretByName = async (req: Request, res: Response) => { export const getSecretByName = async (req: Request, res: Response) => {
const { const {
query: { secretPath, environment, workspaceId, type }, query: { secretPath, environment, workspaceId, type, include_imports },
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameV3, req); } = await validateRequest(reqValidator.GetSecretByNameV3, req);
if (req.user?._id) { await checkSecretsPermission({
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); authData: req.authData,
ForbiddenError.from(permission).throwUnlessCan( workspaceId,
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS] secretAction: ProjectPermissionActions.Read
}); });
}
const secret = await SecretService.getSecret({ const secret = await SecretService.getSecret({
secretName, secretName,
@@ -509,7 +550,8 @@ export const getSecretByName = async (req: Request, res: Response) => {
environment, environment,
type, type,
secretPath, secretPath,
authData: req.authData authData: req.authData,
include_imports
}); });
return res.status(200).send({ return res.status(200).send({
@@ -538,25 +580,50 @@ export const createSecret = async (req: Request, res: Response) => {
secretCommentTag, secretCommentTag,
secretKeyCiphertext, secretKeyCiphertext,
secretValueCiphertext, secretValueCiphertext,
secretCommentCiphertext secretCommentCiphertext,
skipMultilineEncoding
}, },
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.CreateSecretV3, req); } = await validateRequest(reqValidator.CreateSecretV3, req);
if (req.user?._id) { const { membership } = await checkSecretsPermission({
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); authData: req.authData,
ForbiddenError.from(permission).throwUnlessCan( workspaceId,
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS] secretAction: ProjectPermissionActions.Create
}); });
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
authData: req.authData,
data: {
[CommitType.CREATE]: [
{
secretName,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
}
]
}
});
return res.send({ approval: secretApprovalRequest });
}
} }
const secret = await SecretService.createSecret({ const secret = await SecretService.createSecret({
@@ -575,7 +642,8 @@ export const createSecret = async (req: Request, res: Response) => {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
metadata metadata,
skipMultilineEncoding
}); });
await EventService.handleEvent({ await EventService.handleEvent({
@@ -605,28 +673,68 @@ export const updateSecretByName = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueTag, secretValueTag,
secretValueIV, secretValueIV,
secretId,
type, type,
environment, environment,
secretPath, secretPath,
workspaceId workspaceId,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
secretName: newSecretName,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
skipMultilineEncoding
}, },
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.UpdateSecretByNameV3, req); } = await validateRequest(reqValidator.UpdateSecretByNameV3, req);
if (req.user?._id) { if (newSecretName && (!secretKeyIV || !secretKeyTag || !secretKeyCiphertext)) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); throw BadRequestError({ message: "Missing encrypted key" });
ForbiddenError.from(permission).throwUnlessCan( }
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) const { membership } = await checkSecretsPermission({
); authData: req.authData,
} else { workspaceId,
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS] secretAction: ProjectPermissionActions.Edit
}); });
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
authData: req.authData,
data: {
[CommitType.UPDATE]: [
{
secretName,
newSecretName,
secretValueCiphertext,
secretValueIV,
secretValueTag,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
}
]
}
});
return res.send({ approval: secretApprovalRequest });
}
} }
const secret = await SecretService.updateSecret({ const secret = await SecretService.updateSecret({
@@ -634,11 +742,21 @@ export const updateSecretByName = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
secretId,
authData: req.authData, authData: req.authData,
newSecretName,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath secretPath,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
}); });
await EventService.handleEvent({ await EventService.handleEvent({
@@ -661,28 +779,43 @@ export const updateSecretByName = async (req: Request, res: Response) => {
*/ */
export const deleteSecretByName = async (req: Request, res: Response) => { export const deleteSecretByName = async (req: Request, res: Response) => {
const { const {
body: { type, environment, secretPath, workspaceId }, body: { type, environment, secretPath, workspaceId, secretId },
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.DeleteSecretByNameV3, req); } = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
if (req.user?._id) { const { membership } = await checkSecretsPermission({
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); authData: req.authData,
ForbiddenError.from(permission).throwUnlessCan( workspaceId,
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS] secretAction: ProjectPermissionActions.Delete
}); });
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
authData: req.authData,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.DELETE]: [
{
secretName
}
]
}
});
return res.send({ approval: secretApprovalRequest });
}
} }
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
secretId,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
@@ -702,3 +835,135 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
secret secret
}); });
}; };
export const createSecretByNameBatch = async (req: Request, res: Response) => {
const {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req);
const { membership } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Create
});
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
authData: req.authData,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.CREATE]: secrets.filter(({ type }) => type === "shared")
}
});
return res.send({ approval: secretApprovalRequest });
}
}
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);
const { membership } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Edit
});
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.UPDATE]: secrets.filter(({ type }) => type === "shared")
},
authData: req.authData
});
return res.send({ approval: secretApprovalRequest });
}
}
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);
const { membership } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Delete
});
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.DELETE]: secrets.filter(({ type }) => type === "shared")
},
authData: req.authData
});
return res.send({ approval: secretApprovalRequest });
}
}
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

@@ -5,10 +5,10 @@ import { MembershipOrg, User } from "../../models";
import { completeAccount } from "../../helpers/user"; import { completeAccount } from "../../helpers/user";
import { initializeDefaultOrg } from "../../helpers/signup"; import { initializeDefaultOrg } from "../../helpers/signup";
import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth"; import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth";
import { ACCEPTED, INVITED } from "../../variables"; import { ACCEPTED, AuthTokenType, INVITED } from "../../variables";
import { standardRequest } from "../../config/request"; import { standardRequest } from "../../config/request";
import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config"; import { getAuthSecret, getHttpsEnabled, getLoopsApiKey } from "../../config";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { TelemetryService } from "../../services"; import { TelemetryService } from "../../services";
import { AuthMethod } from "../../models"; import { AuthMethod } from "../../models";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
@@ -78,12 +78,11 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
} }
const decodedToken = <jwt.UserIDJwtPayload>( const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret()) jwt.verify(AUTH_TOKEN_VALUE, await getAuthSecret())
); );
if (decodedToken.userId !== user.id) { if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw UnauthorizedRequestError();
throw BadRequestError(); if (decodedToken.userId !== user.id) throw UnauthorizedRequestError();
}
} }
// complete setting up user's account // complete setting up user's account

View File

@@ -1,7 +1,7 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import { Secret } from "../../models"; import { Secret, ServiceTokenDataV3 } from "../../models";
import { SecretService } from "../../services"; import { SecretService } from "../../services";
import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService"; import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService";
import { UnauthorizedRequestError } from "../../utils/errors"; import { UnauthorizedRequestError } from "../../utils/errors";
@@ -101,3 +101,17 @@ export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
message: "Successfully named workspace secrets" message: "Successfully named workspace secrets"
}); });
}; };
export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceServiceTokenDataV3, req);
const serviceTokenData = await ServiceTokenDataV3.find({
workspace: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
serviceTokenData
});
}

View File

@@ -8,6 +8,8 @@ import * as actionController from "./actionController";
import * as membershipController from "./membershipController"; import * as membershipController from "./membershipController";
import * as cloudProductsController from "./cloudProductsController"; import * as cloudProductsController from "./cloudProductsController";
import * as roleController from "./roleController"; import * as roleController from "./roleController";
import * as secretApprovalPolicyController from "./secretApprovalPolicyController";
import * as secretApprovalRequestController from "./secretApprovalRequestsController";
export { export {
secretController, secretController,
@@ -19,5 +21,7 @@ export {
actionController, actionController,
membershipController, membershipController,
cloudProductsController, cloudProductsController,
roleController roleController,
secretApprovalPolicyController,
secretApprovalRequestController
}; };

View File

@@ -23,7 +23,7 @@ import {
memberPermissions memberPermissions
} from "../../services/RoleService"; } from "../../services/RoleService";
import { BadRequestError } from "../../../utils/errors"; import { BadRequestError } from "../../../utils/errors";
import Role from "../../models/role"; import { Role } from "../../models";
import { validateRequest } from "../../../helpers/validation"; import { validateRequest } from "../../../helpers/validation";
import { packRules } from "@casl/ability/extra"; import { packRules } from "@casl/ability/extra";
@@ -212,6 +212,7 @@ export const getUserPermissions = async (req: Request, res: Response) => {
const { const {
params: { orgId } params: { orgId }
} = await validateRequest(GetUserPermission, req); } = await validateRequest(GetUserPermission, req);
const { permission } = await getUserOrgPermissions(req.user._id, orgId); const { permission } = await getUserOrgPermissions(req.user._id, orgId);
res.status(200).json({ res.status(200).json({

View File

@@ -0,0 +1,128 @@
import { ForbiddenError, subject } from "@casl/ability";
import { Request, Response } from "express";
import { nanoid } from "nanoid";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../services/ProjectRoleService";
import { validateRequest } from "../../../helpers/validation";
import { SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
import { getSecretPolicyOfBoard } from "../../services/SecretApprovalService";
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, name }
} = 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,
name: name ?? `${environment}-${nanoid(3)}`,
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, name },
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,
name: (name || secretApproval?.name) ?? `${secretApproval.environment}-${nanoid(3)}`,
...(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
});
};
export const getSecretApprovalPolicyOfBoard = async (req: Request, res: Response) => {
const {
query: { workspaceId, environment, secretPath }
} = await validateRequest(reqValidator.GetSecretApprovalPolicyOfABoard, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { secretPath, environment })
);
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
return res.send({ policy: secretApprovalPolicy });
};

View File

@@ -0,0 +1,333 @@
import { Request, Response } from "express";
import { getUserProjectPermissions } from "../../services/ProjectRoleService";
import { validateRequest } from "../../../helpers/validation";
import { Folder } from "../../../models";
import { ApprovalStatus, SecretApprovalRequest } from "../../models/secretApprovalRequest";
import * as reqValidator from "../../validation/secretApprovalRequest";
import { getFolderWithPathFromId } from "../../../services/FolderService";
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
import { performSecretApprovalRequestMerge } from "../../services/SecretApprovalService";
import { Types } from "mongoose";
import { EEAuditLogService } from "../../services";
import { EventType } from "../../models";
export const getSecretApprovalRequestCount = async (req: Request, res: Response) => {
const {
query: { workspaceId }
} = await validateRequest(reqValidator.getSecretApprovalRequestCount, req);
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
const approvalRequestCount = await SecretApprovalRequest.aggregate([
{
$match: {
workspace: new Types.ObjectId(workspaceId)
}
},
{
$lookup: {
from: SecretApprovalPolicy.collection.name,
localField: "policy",
foreignField: "_id",
as: "policy"
}
},
{ $unwind: "$policy" },
...(membership.role !== "admin"
? [
{
$match: {
$or: [
{ committer: new Types.ObjectId(membership.id) },
{ "policy.approvers": new Types.ObjectId(membership.id) }
]
}
}
]
: []),
{
$group: {
_id: "$status",
count: { $sum: 1 }
}
}
]);
const openRequests = approvalRequestCount.find(({ _id }) => _id === "open");
const closedRequests = approvalRequestCount.find(({ _id }) => _id === "close");
return res.send({
approvals: { open: openRequests?.count || 0, closed: closedRequests?.count || 0 }
});
};
export const getSecretApprovalRequests = async (req: Request, res: Response) => {
const {
query: { status, committer, workspaceId, environment, limit, offset }
} = await validateRequest(reqValidator.getSecretApprovalRequests, req);
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
const query = {
workspace: new Types.ObjectId(workspaceId),
environment,
committer: committer ? new Types.ObjectId(committer) : undefined,
status
};
// to strip of undefined in query we use es6 spread to ignore those fields
Object.entries(query).forEach(
([key, value]) => value === undefined && delete query[key as keyof typeof query]
);
const approvalRequests = await SecretApprovalRequest.aggregate([
{
$match: query
},
{ $sort: { createdAt: -1 } },
{
$lookup: {
from: SecretApprovalPolicy.collection.name,
localField: "policy",
foreignField: "_id",
as: "policy"
}
},
{ $unwind: "$policy" },
...(membership.role !== "admin"
? [
{
$match: {
$or: [
{ committer: new Types.ObjectId(membership.id) },
{ "policy.approvers": new Types.ObjectId(membership.id) }
]
}
}
]
: []),
{ $skip: offset },
{ $limit: limit }
]);
if (!approvalRequests.length) return res.send({ approvals: [] });
const unqiueEnvs = environment ?? {
$in: [...new Set(approvalRequests.map(({ environment }) => environment))]
};
const approvalRootFolders = await Folder.find({
workspace: workspaceId,
environment: unqiueEnvs
}).lean();
const formatedApprovals = approvalRequests.map((el) => {
let secretPath = "/";
const folders = approvalRootFolders.find(({ environment }) => environment === el.environment);
if (folders) {
secretPath = getFolderWithPathFromId(folders?.nodes, el.folderId)?.folderPath || "/";
}
return { ...el, secretPath };
});
return res.send({
approvals: formatedApprovals
});
};
export const getSecretApprovalRequestDetails = async (req: Request, res: Response) => {
const {
params: { id }
} = await validateRequest(reqValidator.getSecretApprovalRequestDetails, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id)
.populate<{ policy: ISecretApprovalPolicy }>("policy")
.populate({
path: "commits.secretVersion",
populate: {
path: "tags"
}
})
.populate("commits.secret", "version")
.populate("commits.newVersion.tags")
.lean();
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
// allow to fetch only if its admin or is the committer or approver
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find(
(approverId) => approverId.toString() === membership._id.toString()
)
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
let secretPath = "/";
const approvalRootFolders = await Folder.findOne({
workspace: secretApprovalRequest.workspace,
environment: secretApprovalRequest.environment
}).lean();
if (approvalRootFolders) {
secretPath =
getFolderWithPathFromId(approvalRootFolders?.nodes, secretApprovalRequest.folderId)
?.folderPath || "/";
}
return res.send({
approval: { ...secretApprovalRequest, secretPath }
});
};
export const updateSecretApprovalReviewStatus = async (req: Request, res: Response) => {
const {
body: { status },
params: { id }
} = await validateRequest(reqValidator.updateSecretApprovalReviewStatus, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
policy: ISecretApprovalPolicy;
}>("policy");
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
const reviewerPos = secretApprovalRequest.reviewers.findIndex(
({ member }) => member.toString() === membership._id.toString()
);
if (reviewerPos !== -1) {
secretApprovalRequest.reviewers[reviewerPos].status = status;
} else {
secretApprovalRequest.reviewers.push({ member: membership._id, status });
}
await secretApprovalRequest.save();
return res.send({ status });
};
export const mergeSecretApprovalRequest = async (req: Request, res: Response) => {
const {
params: { id }
} = await validateRequest(reqValidator.mergeSecretApprovalRequest, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
policy: ISecretApprovalPolicy;
}>("policy");
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
(prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status }),
{}
);
const hasMinApproval =
secretApprovalRequest.policy.approvals <=
secretApprovalRequest.policy.approvers.filter(
(approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
).length;
if (!hasMinApproval) throw BadRequestError({ message: "Doesn't have minimum approvals needed" });
const approval = await performSecretApprovalRequestMerge(
id,
req.authData,
membership._id.toString()
);
return res.send({ approval });
};
export const updateSecretApprovalRequestStatus = async (req: Request, res: Response) => {
const {
body: { status },
params: { id }
} = await validateRequest(reqValidator.updateSecretApprovalRequestStatus, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
policy: ISecretApprovalPolicy;
}>("policy");
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership._id))
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
if (secretApprovalRequest.hasMerged)
throw BadRequestError({ message: "Approval request has been merged" });
if (secretApprovalRequest.status === "close" && status === "close")
throw BadRequestError({ message: "Approval request is already closed" });
if (secretApprovalRequest.status === "open" && status === "open")
throw BadRequestError({ message: "Approval request is already open" });
const updatedRequest = await SecretApprovalRequest.findByIdAndUpdate(
id,
{ status, statusChangeBy: membership._id },
{ new: true }
);
if (status === "close") {
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.SECRET_APPROVAL_CLOSED,
metadata: {
closedBy: membership._id.toString(),
secretApprovalRequestId: id,
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId: secretApprovalRequest.workspace
}
);
} else {
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.SECRET_APPROVAL_REOPENED,
metadata: {
reopenedBy: membership._id.toString(),
secretApprovalRequestId: id,
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId: secretApprovalRequest.workspace
}
);
}
return res.send({ approval: updatedRequest });
};

View File

@@ -5,6 +5,7 @@ import {
Membership, Membership,
Secret, Secret,
ServiceTokenData, ServiceTokenData,
ServiceTokenDataV3,
TFolderSchema, TFolderSchema,
User, User,
Workspace Workspace
@@ -20,6 +21,7 @@ import {
SecretSnapshot, SecretSnapshot,
SecretVersion, SecretVersion,
ServiceActor, ServiceActor,
ServiceActorV3,
TFolderRootVersionSchema, TFolderRootVersionSchema,
TrustedIP, TrustedIP,
UserActor UserActor
@@ -27,7 +29,7 @@ import {
import { EESecretService } from "../../services"; import { EESecretService } from "../../services";
import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
// import Folder, { TFolderSchema } from "../../../models/folder"; // import Folder, { TFolderSchema } from "../../../models/folder";
import { searchByFolderId } from "../../../services/FolderService"; import { getFolderByPath, searchByFolderId } from "../../../services/FolderService";
import { EEAuditLogService, EELicenseService } from "../../services"; import { EEAuditLogService, EELicenseService } from "../../services";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
import { validateRequest } from "../../../helpers/validation"; import { validateRequest } from "../../../helpers/validation";
@@ -104,7 +106,7 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
*/ */
const { const {
params: { workspaceId }, params: { workspaceId },
query: { environment, folderId, offset, limit } query: { environment, directory, offset, limit }
} = await validateRequest(GetWorkspaceSecretSnapshotsV1, req); } = await validateRequest(GetWorkspaceSecretSnapshotsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
@@ -113,10 +115,20 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
ProjectPermissionSub.SecretRollback 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({ const secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folderId: folderId || "root" folderId
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.skip(offset) .skip(offset)
@@ -135,7 +147,7 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => { export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
const { const {
params: { workspaceId }, params: { workspaceId },
query: { environment, folderId } query: { environment, directory }
} = await validateRequest(GetWorkspaceSecretSnapshotsCountV1, req); } = await validateRequest(GetWorkspaceSecretSnapshotsCountV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
@@ -144,10 +156,20 @@ export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Respon
ProjectPermissionSub.SecretRollback 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({ const count = await SecretSnapshot.countDocuments({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folderId: folderId || "root" folderId
}); });
return res.status(200).send({ return res.status(200).send({
@@ -215,7 +237,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
const { const {
params: { workspaceId }, params: { workspaceId },
body: { folderId, environment, version } body: { directory, environment, version }
} = await validateRequest(RollbackWorkspaceSecretSnapshotV1, req); } = await validateRequest(RollbackWorkspaceSecretSnapshotV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
@@ -224,6 +246,16 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
ProjectPermissionSub.SecretRollback 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 // validate secret snapshot
const secretSnapshot = await SecretSnapshot.findOne({ const secretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId, workspace: workspaceId,
@@ -668,13 +700,13 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
: {}), : {}),
...(actor ...(actor
? { ? {
"actor.type": actor.split("-", 2)[0], "actor.type": actor.substring(0, actor.lastIndexOf("-")),
...(actor.split("-", 2)[0] === ActorType.USER ...(actor.split("-", 2)[0] === ActorType.USER
? { ? {
"actor.metadata.userId": actor.split("-", 2)[1] "actor.metadata.userId": actor.substring(actor.lastIndexOf("-") + 1)
} }
: { : {
"actor.metadata.serviceId": actor.split("-", 2)[1] "actor.metadata.serviceId": actor.substring(actor.lastIndexOf("-") + 1)
}) })
} }
: {}), : {}),
@@ -743,8 +775,26 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res
} }
})); }));
const serviceV3Actors: ServiceActorV3[] = (
await ServiceTokenDataV3.find({
workspace: new Types.ObjectId(workspaceId)
})
).map((serviceTokenData) => ({
type: ActorType.SERVICE_V3,
metadata: {
serviceId: serviceTokenData._id.toString(),
name: serviceTokenData.name
}
}));
const actors = [
...userActors,
...serviceActors,
...serviceV3Actors
];
return res.status(200).send({ return res.status(200).send({
actors: [...userActors, ...serviceActors] actors
}); });
}; };

View File

@@ -0,0 +1,5 @@
import * as serviceTokenDataController from "./serviceTokenDataController";
export {
serviceTokenDataController
}

View File

@@ -0,0 +1,325 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
IServiceTokenDataV3,
IUser,
ServiceTokenDataV3,
ServiceTokenDataV3Key,
Workspace
} from "../../../models";
import {
IServiceTokenV3Scope,
IServiceTokenV3TrustedIp
} from "../../../models/serviceTokenDataV3";
import {
ActorType,
EventType
} from "../../models";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/serviceTokenDataV3";
import { createToken } from "../../../helpers/auth";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError, ResourceNotFoundError } from "../../../utils/errors";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
import { EEAuditLogService, EELicenseService } from "../../services";
import { getJwtServiceTokenSecret } from "../../../config";
/**
* Return project key for service token
* @param req
* @param res
*/
export const getServiceTokenDataKey = async (req: Request, res: Response) => {
const key = await ServiceTokenDataV3Key.findOne({
serviceTokenData: (req.authData.authPayload as IServiceTokenDataV3)._id
}).populate<{ sender: IUser }>("sender", "publicKey");
if (!key) throw ResourceNotFoundError({
message: "Failed to find project key for service token"
});
const { _id, workspace, encryptedKey, nonce, sender: { publicKey } } = key;
return res.status(200).send({
key: {
_id,
workspace,
encryptedKey,
publicKey,
nonce
}
});
}
/**
* Create service token data
* @param req
* @param res
* @returns
*/
export const createServiceTokenData = async (req: Request, res: Response) => {
const {
body: {
name,
workspaceId,
publicKey,
scopes,
trustedIps,
expiresIn,
encryptedKey, // for ServiceTokenDataV3Key
nonce // for ServiceTokenDataV3Key
}
} = await validateRequest(reqValidator.CreateServiceTokenV3, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.ServiceTokens
);
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
const plan = await EELicenseService.getPlan(workspace.organization);
// validate trusted ips
const reformattedTrustedIps = trustedIps.map((trustedIp) => {
if (!plan.ipAllowlisting && trustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({
message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
});
const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress);
if (!isValidIPOrCidr) return res.status(400).send({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(trustedIp.ipAddress);
});
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
let user;
if (req.authData.actor.type === ActorType.USER) {
user = req.authData.authPayload._id;
}
const isActive = true;
const serviceTokenData = await new ServiceTokenDataV3({
name,
user,
workspace: new Types.ObjectId(workspaceId),
publicKey,
usageCount: 0,
trustedIps: reformattedTrustedIps,
scopes,
isActive,
expiresAt
}).save();
await new ServiceTokenDataV3Key({
encryptedKey,
nonce,
sender: req.user._id,
serviceTokenData: serviceTokenData._id,
workspace: new Types.ObjectId(workspaceId)
}).save();
const token = createToken({
payload: {
_id: serviceTokenData._id.toString()
},
expiresIn,
secret: await getJwtServiceTokenSecret()
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SERVICE_TOKEN_V3,
metadata: {
name,
isActive,
scopes: scopes as Array<IServiceTokenV3Scope>,
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
expiresAt
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
serviceTokenData,
serviceToken: `stv3.${token}`
});
}
/**
* Update service token data with id [serviceTokenDataId]
* @param req
* @param res
* @returns
*/
export const updateServiceTokenData = async (req: Request, res: Response) => {
const {
params: { serviceTokenDataId },
body: {
name,
isActive,
scopes,
trustedIps,
expiresIn
}
} = await validateRequest(reqValidator.UpdateServiceTokenV3, req);
let serviceTokenData = await ServiceTokenDataV3.findById(serviceTokenDataId);
if (!serviceTokenData) throw ResourceNotFoundError({
message: "Service token not found"
});
const { permission } = await getUserProjectPermissions(
req.user._id,
serviceTokenData.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.ServiceTokens
);
const workspace = await Workspace.findById(serviceTokenData.workspace);
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
const plan = await EELicenseService.getPlan(workspace.organization);
// validate trusted ips
let reformattedTrustedIps;
if (trustedIps) {
reformattedTrustedIps = trustedIps.map((trustedIp) => {
if (!plan.ipAllowlisting && trustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({
message: "Failed to update IP access range to service token due to plan restriction. Upgrade plan to update IP access range."
});
const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress);
if (!isValidIPOrCidr) return res.status(400).send({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(trustedIp.ipAddress);
});
}
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenDataId,
{
name,
isActive,
scopes,
trustedIps: reformattedTrustedIps,
expiresAt
},
{
new: true
}
);
if (!serviceTokenData) throw BadRequestError({
message: "Failed to update service token"
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_SERVICE_TOKEN_V3,
metadata: {
name: serviceTokenData.name,
isActive,
scopes: scopes as Array<IServiceTokenV3Scope>,
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
expiresAt
}
},
{
workspaceId: serviceTokenData.workspace
}
);
return res.status(200).send({
serviceTokenData
});
}
/**
* Delete service token data with id [serviceTokenDataId]
* @param req
* @param res
* @returns
*/
export const deleteServiceTokenData = async (req: Request, res: Response) => {
const {
params: { serviceTokenDataId }
} = await validateRequest(reqValidator.DeleteServiceTokenV3, req);
let serviceTokenData = await ServiceTokenDataV3.findById(serviceTokenDataId);
if (!serviceTokenData) throw ResourceNotFoundError({
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 ServiceTokenDataV3.findByIdAndDelete(serviceTokenDataId);
if (!serviceTokenData) throw BadRequestError({
message: "Failed to delete service token"
});
await ServiceTokenDataV3Key.findOneAndDelete({
serviceTokenData: serviceTokenData._id
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_SERVICE_TOKEN_V3,
metadata: {
name: serviceTokenData.name,
isActive: serviceTokenData.isActive,
scopes: serviceTokenData.scopes as Array<IServiceTokenV3Scope>,
trustedIps: serviceTokenData.trustedIps as Array<IServiceTokenV3TrustedIp>,
expiresAt: serviceTokenData.expiresAt
}
},
{
workspaceId: serviceTokenData.workspace
}
);
return res.status(200).send({
serviceTokenData
});
}

View File

@@ -1,6 +1,7 @@
export enum ActorType { export enum ActorType {
USER = "user", USER = "user",
SERVICE = "service" SERVICE = "service",
SERVICE_V3 = "service-v3"
} }
export enum UserAgentType { export enum UserAgentType {
@@ -15,8 +16,11 @@ export enum EventType {
GET_SECRET = "get-secret", GET_SECRET = "get-secret",
REVEAL_SECRET = "reveal-secret", REVEAL_SECRET = "reveal-secret",
CREATE_SECRET = "create-secret", CREATE_SECRET = "create-secret",
CREATE_SECRETS = "create-secrets",
UPDATE_SECRET = "update-secret", UPDATE_SECRET = "update-secret",
UPDATE_SECRETS = "update-secrets",
DELETE_SECRET = "delete-secret", DELETE_SECRET = "delete-secret",
DELETE_SECRETS = "delete-secrets",
GET_WORKSPACE_KEY = "get-workspace-key", GET_WORKSPACE_KEY = "get-workspace-key",
AUTHORIZE_INTEGRATION = "authorize-integration", AUTHORIZE_INTEGRATION = "authorize-integration",
UNAUTHORIZE_INTEGRATION = "unauthorize-integration", UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
@@ -25,8 +29,11 @@ export enum EventType {
ADD_TRUSTED_IP = "add-trusted-ip", ADD_TRUSTED_IP = "add-trusted-ip",
UPDATE_TRUSTED_IP = "update-trusted-ip", UPDATE_TRUSTED_IP = "update-trusted-ip",
DELETE_TRUSTED_IP = "delete-trusted-ip", DELETE_TRUSTED_IP = "delete-trusted-ip",
CREATE_SERVICE_TOKEN = "create-service-token", CREATE_SERVICE_TOKEN = "create-service-token", // v2
DELETE_SERVICE_TOKEN = "delete-service-token", DELETE_SERVICE_TOKEN = "delete-service-token", // v2
CREATE_SERVICE_TOKEN_V3 = "create-service-token-v3", // v3
UPDATE_SERVICE_TOKEN_V3 = "update-service-token-v3", // v3
DELETE_SERVICE_TOKEN_V3 = "delete-service-token-v3", // v3
CREATE_ENVIRONMENT = "create-environment", CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment", UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment", DELETE_ENVIRONMENT = "delete-environment",
@@ -43,5 +50,9 @@ export enum EventType {
UPDATE_SECRET_IMPORT = "update-secret-import", UPDATE_SECRET_IMPORT = "update-secret-import",
DELETE_SECRET_IMPORT = "delete-secret-import", DELETE_SECRET_IMPORT = "delete-secret-import",
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role", UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions" UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions",
SECRET_APPROVAL_MERGED = "secret-approval-merged",
SECRET_APPROVAL_REQUEST = "secret-approval-request",
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
SECRET_APPROVAL_REOPENED = "secret-approval-reopened"
} }

View File

@@ -2,6 +2,10 @@ import {
ActorType, ActorType,
EventType EventType
} from "./enums"; } from "./enums";
import {
IServiceTokenV3Scope,
IServiceTokenV3TrustedIp
} from "../../../models/serviceTokenDataV3";
interface UserActorMetadata { interface UserActorMetadata {
userId: string; userId: string;
@@ -23,9 +27,15 @@ export interface ServiceActor {
metadata: ServiceActorMetadata; metadata: ServiceActorMetadata;
} }
export interface ServiceActorV3 {
type: ActorType.SERVICE_V3;
metadata: ServiceActorMetadata;
}
export type Actor = export type Actor =
| UserActor | UserActor
| ServiceActor; | ServiceActor
| ServiceActorV3;
interface GetSecretsEvent { interface GetSecretsEvent {
type: EventType.GET_SECRETS; type: EventType.GET_SECRETS;
@@ -55,7 +65,16 @@ interface CreateSecretEvent {
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
} };
}
interface CreateSecretBatchEvent {
type: EventType.CREATE_SECRETS;
metadata: {
environment: string;
secretPath: string;
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
};
} }
interface UpdateSecretEvent { interface UpdateSecretEvent {
@@ -66,7 +85,16 @@ interface UpdateSecretEvent {
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
} };
}
interface UpdateSecretBatchEvent {
type: EventType.UPDATE_SECRETS;
metadata: {
environment: string;
secretPath: string;
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
};
} }
interface DeleteSecretEvent { interface DeleteSecretEvent {
@@ -77,28 +105,37 @@ interface DeleteSecretEvent {
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
} };
}
interface DeleteSecretBatchEvent {
type: EventType.DELETE_SECRETS;
metadata: {
environment: string;
secretPath: string;
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
};
} }
interface GetWorkspaceKeyEvent { interface GetWorkspaceKeyEvent {
type: EventType.GET_WORKSPACE_KEY, type: EventType.GET_WORKSPACE_KEY;
metadata: { metadata: {
keyId: string; keyId: string;
} };
} }
interface AuthorizeIntegrationEvent { interface AuthorizeIntegrationEvent {
type: EventType.AUTHORIZE_INTEGRATION; type: EventType.AUTHORIZE_INTEGRATION;
metadata: { metadata: {
integration: string; integration: string;
} };
} }
interface UnauthorizeIntegrationEvent { interface UnauthorizeIntegrationEvent {
type: EventType.UNAUTHORIZE_INTEGRATION; type: EventType.UNAUTHORIZE_INTEGRATION;
metadata: { metadata: {
integration: string; integration: string;
} };
} }
interface CreateIntegrationEvent { interface CreateIntegrationEvent {
@@ -117,7 +154,7 @@ interface CreateIntegrationEvent {
targetServiceId?: string; targetServiceId?: string;
path?: string; path?: string;
region?: string; region?: string;
} };
} }
interface DeleteIntegrationEvent { interface DeleteIntegrationEvent {
@@ -136,7 +173,7 @@ interface DeleteIntegrationEvent {
targetServiceId?: string; targetServiceId?: string;
path?: string; path?: string;
region?: string; region?: string;
} };
} }
interface AddTrustedIPEvent { interface AddTrustedIPEvent {
@@ -145,7 +182,7 @@ interface AddTrustedIPEvent {
trustedIpId: string; trustedIpId: string;
ipAddress: string; ipAddress: string;
prefix?: number; prefix?: number;
} };
} }
interface UpdateTrustedIPEvent { interface UpdateTrustedIPEvent {
@@ -154,7 +191,7 @@ interface UpdateTrustedIPEvent {
trustedIpId: string; trustedIpId: string;
ipAddress: string; ipAddress: string;
prefix?: number; prefix?: number;
} };
} }
interface DeleteTrustedIPEvent { interface DeleteTrustedIPEvent {
@@ -163,7 +200,7 @@ interface DeleteTrustedIPEvent {
trustedIpId: string; trustedIpId: string;
ipAddress: string; ipAddress: string;
prefix?: number; prefix?: number;
} };
} }
interface CreateServiceTokenEvent { interface CreateServiceTokenEvent {
@@ -174,7 +211,7 @@ interface CreateServiceTokenEvent {
environment: string; environment: string;
secretPath: string; secretPath: string;
}>; }>;
} };
} }
interface DeleteServiceTokenEvent { interface DeleteServiceTokenEvent {
@@ -185,6 +222,39 @@ interface DeleteServiceTokenEvent {
environment: string; environment: string;
secretPath: string; secretPath: string;
}>; }>;
};
}
interface CreateServiceTokenV3Event {
type: EventType.CREATE_SERVICE_TOKEN_V3;
metadata: {
name: string;
isActive: boolean;
scopes: Array<IServiceTokenV3Scope>;
trustedIps: Array<IServiceTokenV3TrustedIp>;
expiresAt?: Date;
}
}
interface UpdateServiceTokenV3Event {
type: EventType.UPDATE_SERVICE_TOKEN_V3;
metadata: {
name?: string;
isActive?: boolean;
scopes?: Array<IServiceTokenV3Scope>;
trustedIps?: Array<IServiceTokenV3TrustedIp>;
expiresAt?: Date;
}
}
interface DeleteServiceTokenV3Event {
type: EventType.DELETE_SERVICE_TOKEN_V3;
metadata: {
name: string;
isActive: boolean;
scopes: Array<IServiceTokenV3Scope>;
expiresAt?: Date;
trustedIps: Array<IServiceTokenV3TrustedIp>;
} }
} }
@@ -193,7 +263,7 @@ interface CreateEnvironmentEvent {
metadata: { metadata: {
name: string; name: string;
slug: string; slug: string;
} };
} }
interface UpdateEnvironmentEvent { interface UpdateEnvironmentEvent {
@@ -203,7 +273,7 @@ interface UpdateEnvironmentEvent {
newName: string; newName: string;
oldSlug: string; oldSlug: string;
newSlug: string; newSlug: string;
} };
} }
interface DeleteEnvironmentEvent { interface DeleteEnvironmentEvent {
@@ -211,7 +281,7 @@ interface DeleteEnvironmentEvent {
metadata: { metadata: {
name: string; name: string;
slug: string; slug: string;
} };
} }
interface AddWorkspaceMemberEvent { interface AddWorkspaceMemberEvent {
@@ -219,7 +289,7 @@ interface AddWorkspaceMemberEvent {
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
} };
} }
interface RemoveWorkspaceMemberEvent { interface RemoveWorkspaceMemberEvent {
@@ -227,7 +297,7 @@ interface RemoveWorkspaceMemberEvent {
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
} };
} }
interface CreateFolderEvent { interface CreateFolderEvent {
@@ -237,7 +307,7 @@ interface CreateFolderEvent {
folderId: string; folderId: string;
folderName: string; folderName: string;
folderPath: string; folderPath: string;
} };
} }
interface UpdateFolderEvent { interface UpdateFolderEvent {
@@ -248,7 +318,7 @@ interface UpdateFolderEvent {
oldFolderName: string; oldFolderName: string;
newFolderName: string; newFolderName: string;
folderPath: string; folderPath: string;
} };
} }
interface DeleteFolderEvent { interface DeleteFolderEvent {
@@ -258,54 +328,54 @@ interface DeleteFolderEvent {
folderId: string; folderId: string;
folderName: string; folderName: string;
folderPath: string; folderPath: string;
} };
} }
interface CreateWebhookEvent { interface CreateWebhookEvent {
type: EventType.CREATE_WEBHOOK, type: EventType.CREATE_WEBHOOK;
metadata: { metadata: {
webhookId: string; webhookId: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
webhookUrl: string; webhookUrl: string;
isDisabled: boolean; isDisabled: boolean;
} };
} }
interface UpdateWebhookStatusEvent { interface UpdateWebhookStatusEvent {
type: EventType.UPDATE_WEBHOOK_STATUS, type: EventType.UPDATE_WEBHOOK_STATUS;
metadata: { metadata: {
webhookId: string; webhookId: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
webhookUrl: string; webhookUrl: string;
isDisabled: boolean; isDisabled: boolean;
} };
} }
interface DeleteWebhookEvent { interface DeleteWebhookEvent {
type: EventType.DELETE_WEBHOOK, type: EventType.DELETE_WEBHOOK;
metadata: { metadata: {
webhookId: string; webhookId: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
webhookUrl: string; webhookUrl: string;
isDisabled: boolean; isDisabled: boolean;
} };
} }
interface GetSecretImportsEvent { interface GetSecretImportsEvent {
type: EventType.GET_SECRET_IMPORTS, type: EventType.GET_SECRET_IMPORTS;
metadata: { metadata: {
environment: string; environment: string;
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
numberOfImports: number; numberOfImports: number;
} };
} }
interface CreateSecretImportEvent { interface CreateSecretImportEvent {
type: EventType.CREATE_SECRET_IMPORT, type: EventType.CREATE_SECRET_IMPORT;
metadata: { metadata: {
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
@@ -313,11 +383,11 @@ interface CreateSecretImportEvent {
importFromSecretPath: string; importFromSecretPath: string;
importToEnvironment: string; importToEnvironment: string;
importToSecretPath: string; importToSecretPath: string;
} };
} }
interface UpdateSecretImportEvent { interface UpdateSecretImportEvent {
type: EventType.UPDATE_SECRET_IMPORT, type: EventType.UPDATE_SECRET_IMPORT;
metadata: { metadata: {
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
@@ -326,16 +396,16 @@ interface UpdateSecretImportEvent {
orderBefore: { orderBefore: {
environment: string; environment: string;
secretPath: string; secretPath: string;
}[], }[];
orderAfter: { orderAfter: {
environment: string; environment: string;
secretPath: string; secretPath: string;
}[] }[];
} };
} }
interface DeleteSecretImportEvent { interface DeleteSecretImportEvent {
type: EventType.DELETE_SECRET_IMPORT, type: EventType.DELETE_SECRET_IMPORT;
metadata: { metadata: {
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
@@ -343,17 +413,17 @@ interface DeleteSecretImportEvent {
importFromSecretPath: string; importFromSecretPath: string;
importToEnvironment: string; importToEnvironment: string;
importToSecretPath: string; importToSecretPath: string;
} };
} }
interface UpdateUserRole { interface UpdateUserRole {
type: EventType.UPDATE_USER_WORKSPACE_ROLE, type: EventType.UPDATE_USER_WORKSPACE_ROLE;
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
oldRole: string; oldRole: string;
newRole: string; newRole: string;
} };
} }
interface UpdateUserDeniedPermissions { interface UpdateUserDeniedPermissions {
@@ -367,13 +437,51 @@ interface UpdateUserDeniedPermissions {
}[] }[]
} }
} }
interface SecretApprovalMerge {
type: EventType.SECRET_APPROVAL_MERGED;
metadata: {
mergedBy: string;
secretApprovalRequestSlug: string;
secretApprovalRequestId: string;
};
}
interface SecretApprovalClosed {
type: EventType.SECRET_APPROVAL_CLOSED;
metadata: {
closedBy: string;
secretApprovalRequestSlug: string;
secretApprovalRequestId: string;
};
}
interface SecretApprovalReopened {
type: EventType.SECRET_APPROVAL_REOPENED;
metadata: {
reopenedBy: string;
secretApprovalRequestSlug: string;
secretApprovalRequestId: string;
};
}
interface SecretApprovalRequest {
type: EventType.SECRET_APPROVAL_REQUEST;
metadata: {
committedBy: string;
secretApprovalRequestSlug: string;
secretApprovalRequestId: string;
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
| CreateSecretEvent | CreateSecretEvent
| CreateSecretBatchEvent
| UpdateSecretEvent | UpdateSecretEvent
| UpdateSecretBatchEvent
| DeleteSecretEvent | DeleteSecretEvent
| DeleteSecretBatchEvent
| GetWorkspaceKeyEvent | GetWorkspaceKeyEvent
| AuthorizeIntegrationEvent | AuthorizeIntegrationEvent
| UnauthorizeIntegrationEvent | UnauthorizeIntegrationEvent
@@ -384,6 +492,9 @@ export type Event =
| DeleteTrustedIPEvent | DeleteTrustedIPEvent
| CreateServiceTokenEvent | CreateServiceTokenEvent
| DeleteServiceTokenEvent | DeleteServiceTokenEvent
| CreateServiceTokenV3Event
| UpdateServiceTokenV3Event
| DeleteServiceTokenV3Event
| CreateEnvironmentEvent | CreateEnvironmentEvent
| UpdateEnvironmentEvent | UpdateEnvironmentEvent
| DeleteEnvironmentEvent | DeleteEnvironmentEvent
@@ -400,4 +511,8 @@ export type Event =
| UpdateSecretImportEvent | UpdateSecretImportEvent
| DeleteSecretImportEvent | DeleteSecretImportEvent
| UpdateUserRole | UpdateUserRole
| UpdateUserDeniedPermissions; | UpdateUserDeniedPermissions
| SecretApprovalMerge
| SecretApprovalClosed
| SecretApprovalRequest
| SecretApprovalReopened;

View File

@@ -29,6 +29,4 @@ const gitAppInstallationSession = new Schema<GitAppInstallationSession>({
}); });
const GitAppInstallationSession = model<GitAppInstallationSession>("git_app_installation_session", gitAppInstallationSession); export const GitAppInstallationSession = model<GitAppInstallationSession>("git_app_installation_session", gitAppInstallationSession);
export default GitAppInstallationSession;

View File

@@ -26,6 +26,4 @@ const gitAppOrganizationInstallation = new Schema<Installation>({
}); });
const GitAppOrganizationInstallation = model<Installation>("git_app_organization_installation", gitAppOrganizationInstallation); export const GitAppOrganizationInstallation = model<Installation>("git_app_organization_installation", gitAppOrganizationInstallation);
export default GitAppOrganizationInstallation;

View File

@@ -5,7 +5,7 @@ export const STATUS_RESOLVED_REVOKED = "RESOLVED_REVOKED";
export const STATUS_RESOLVED_NOT_REVOKED = "RESOLVED_NOT_REVOKED"; export const STATUS_RESOLVED_NOT_REVOKED = "RESOLVED_NOT_REVOKED";
export const STATUS_UNRESOLVED = "UNRESOLVED"; export const STATUS_UNRESOLVED = "UNRESOLVED";
export type GitRisks = { export type IGitRisks = {
id: string; id: string;
description: string; description: string;
startLine: string; startLine: string;
@@ -42,7 +42,7 @@ export type GitRisks = {
organization: Schema.Types.ObjectId, organization: Schema.Types.ObjectId,
} }
const gitRisks = new Schema<GitRisks>({ const gitRisks = new Schema<IGitRisks>({
id: { id: {
type: String, type: String,
}, },
@@ -147,6 +147,4 @@ const gitRisks = new Schema<GitRisks>({
} }
}, { timestamps: true }); }, { timestamps: true });
const GitRisks = model<GitRisks>("GitRisks", gitRisks); export const GitRisks = model<IGitRisks>("GitRisks", gitRisks);
export default GitRisks;

View File

@@ -2,6 +2,7 @@ export * from "./secretSnapshot";
export * from "./secretVersion"; export * from "./secretVersion";
export * from "./folderVersion"; export * from "./folderVersion";
export * from "./log"; export * from "./log";
export * from "./role";
export * from "./action"; export * from "./action";
export * from "./ssoConfig"; export * from "./ssoConfig";
export * from "./trustedIp"; export * from "./trustedIp";
@@ -9,3 +10,5 @@ export * from "./auditLog";
export * from "./gitRisks"; export * from "./gitRisks";
export * from "./gitAppOrganizationInstallation"; export * from "./gitAppOrganizationInstallation";
export * from "./gitAppInstallationSession"; export * from "./gitAppInstallationSession";
export * from "./secretApprovalPolicy";
export * from "./secretApprovalRequest";

View File

@@ -50,6 +50,4 @@ const roleSchema = new Schema<IRole>(
roleSchema.index({ organization: 1, workspace: 1 }); roleSchema.index({ organization: 1, workspace: 1 });
const Role = model<IRole>("Role", roleSchema); export const Role = model<IRole>("Role", roleSchema);
export default Role;

View File

@@ -0,0 +1,51 @@
import { Schema, Types, model } from "mongoose";
export interface ISecretApprovalPolicy {
_id: Types.ObjectId;
workspace: Types.ObjectId;
name: string;
environment: string;
secretPath?: string;
approvers: Types.ObjectId[];
approvals: number;
}
const secretApprovalPolicySchema = new Schema<ISecretApprovalPolicy>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
approvers: [
{
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: "Membership"
}
],
name: {
type: String
},
environment: {
type: String,
required: true
},
secretPath: {
type: String,
required: false
},
approvals: {
type: Number,
default: 1
}
},
{
timestamps: true
}
);
export const SecretApprovalPolicy = model<ISecretApprovalPolicy>(
"SecretApprovalPolicy",
secretApprovalPolicySchema
);

View File

@@ -0,0 +1,203 @@
import { Schema, Types, model } from "mongoose";
import { customAlphabet } from "nanoid";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8
} from "../../variables";
export enum ApprovalStatus {
PENDING = "pending",
APPROVED = "approved",
REJECTED = "rejected"
}
export enum CommitType {
DELETE = "delete",
UPDATE = "update",
CREATE = "create"
}
const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const nanoId = customAlphabet(SLUG_ALPHABETS, 10);
export interface ISecretApprovalSecChange {
_id: Types.ObjectId;
version: number;
secretBlindIndex?: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentCiphertext?: string;
skipMultilineEncoding?: boolean;
algorithm?: "aes-256-gcm";
keyEncoding?: "utf8" | "base64";
tags?: string[];
}
export type ISecretCommits<T = Types.ObjectId, J = Types.ObjectId> = Array<
| {
newVersion: ISecretApprovalSecChange;
op: CommitType.CREATE;
}
| {
// secret is recorded to get the latest version, we can keep ref to secret for pulling change as it will also get changed
// on merge
secretVersion: J;
secret: T;
newVersion: Partial<Omit<ISecretApprovalSecChange, "_id">> & { _id: Types.ObjectId };
op: CommitType.UPDATE;
}
| {
secret: T;
secretVersion: J;
op: CommitType.DELETE;
}
>;
export interface ISecretApprovalRequest {
_id: Types.ObjectId;
committer: Types.ObjectId;
slug: string;
statusChangeBy: Types.ObjectId;
reviewers: {
member: Types.ObjectId;
status: ApprovalStatus;
}[];
workspace: Types.ObjectId;
environment: string;
folderId: string;
hasMerged: boolean;
status: "open" | "close";
policy: Types.ObjectId;
commits: ISecretCommits;
conflicts: Array<{ secretId: string; op: CommitType }>;
}
const secretApprovalSecretChangeSchema = new Schema<ISecretApprovalSecChange>({
version: {
type: Number,
default: 1,
required: true
},
secretBlindIndex: {
type: String,
select: false
},
secretKeyCiphertext: {
type: String,
required: true
},
secretKeyIV: {
type: String, // symmetric
required: true
},
secretKeyTag: {
type: String, // symmetric
required: true
},
secretValueCiphertext: {
type: String,
required: true
},
secretValueIV: {
type: String, // symmetric
required: true
},
secretValueTag: {
type: String, // symmetric
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
},
keyEncoding: {
type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true,
default: ENCODING_SCHEME_UTF8
},
tags: {
ref: "Tag",
type: [Schema.Types.ObjectId],
default: []
}
});
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
environment: {
type: String,
required: true
},
folderId: {
type: String,
required: true,
default: "root"
},
slug: {
type: String,
default: () => nanoId()
},
reviewers: {
type: [
{
member: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: "Membership"
},
status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING }
}
],
default: []
},
policy: { type: Schema.Types.ObjectId, ref: "SecretApprovalPolicy" },
hasMerged: { type: Boolean, default: false },
status: { type: String, enum: ["close", "open"], default: "open" },
committer: { type: Schema.Types.ObjectId, ref: "Membership" },
statusChangeBy: { type: Schema.Types.ObjectId, ref: "Membership" },
commits: [
{
secret: { type: Types.ObjectId, ref: "Secret" },
newVersion: secretApprovalSecretChangeSchema,
secretVersion: { type: Types.ObjectId, ref: "SecretVersion" },
op: { type: String, enum: [CommitType], required: true }
}
],
conflicts: {
type: [
{
secretId: { type: String, required: true },
op: { type: String, enum: [CommitType], required: true }
}
],
default: []
}
},
{
timestamps: true
}
);
export const SecretApprovalRequest = model<ISecretApprovalRequest>(
"SecretApprovalRequest",
secretApprovalRequestSchema
);

View File

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

View File

@@ -8,6 +8,8 @@ import action from "./action";
import cloudProducts from "./cloudProducts"; import cloudProducts from "./cloudProducts";
import secretScanning from "./secretScanning"; import secretScanning from "./secretScanning";
import roles from "./role"; import roles from "./role";
import secretApprovalPolicy from "./secretApprovalPolicy";
import secretApprovalRequest from "./secretApprovalRequest";
export { export {
secret, secret,
@@ -19,5 +21,7 @@ export {
action, action,
cloudProducts, cloudProducts,
secretScanning, secretScanning,
roles roles,
secretApprovalPolicy,
secretApprovalRequest
}; };

View File

@@ -0,0 +1,47 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../../middleware";
import { secretApprovalPolicyController } from "../../controllers/v1";
import { AuthMode } from "../../../variables";
router.get(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalPolicyController.getSecretApprovalPolicy
);
router.get(
"/board",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalPolicyController.getSecretApprovalPolicyOfBoard
);
router.post(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalPolicyController.createSecretApprovalPolicy
);
router.patch(
"/:id",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalPolicyController.updateSecretApprovalPolicy
);
router.delete(
"/:id",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalPolicyController.deleteSecretApprovalPolicy
);
export default router;

View File

@@ -0,0 +1,55 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../../middleware";
import { secretApprovalRequestController } from "../../controllers/v1";
import { AuthMode } from "../../../variables";
router.get(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.getSecretApprovalRequests
);
router.get(
"/count",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.getSecretApprovalRequestCount
);
router.get(
"/:id",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.getSecretApprovalRequestDetails
);
router.post(
"/:id/merge",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.mergeSecretApprovalRequest
);
router.post(
"/:id/review",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.updateSecretApprovalReviewStatus
);
router.post(
"/:id/status",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.updateSecretApprovalRequestStatus
);
export default router;

View File

@@ -6,57 +6,22 @@ import { ssoController } from "../../controllers/v1";
import { authLimiter } from "../../../helpers/rateLimiter"; import { authLimiter } from "../../../helpers/rateLimiter";
import { AuthMode } 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( router.get(
"/google", "/redirect/saml2/:ssoIdentifier",
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(
"/github",
authLimiter, authLimiter,
passport.authenticate("github", { (req, res, next) => {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get("/redirect/saml2/:ssoIdentifier", authLimiter, (req, res, next) => {
const options = { const options = {
failureRedirect: "/", failureRedirect: "/",
additionalParams: { additionalParams: {
RelayState: req.query.callback_port ?? "" RelayState: JSON.stringify({
} spInitiated: true,
callbackPort: req.query.callback_port ?? ""
})
},
}; };
passport.authenticate("saml", options)(req, res, next); passport.authenticate("saml", options)(req, res, next);
}); }
);
router.post( router.post(
"/saml2/:ssoIdentifier", "/saml2/:ssoIdentifier",

View File

@@ -0,0 +1,5 @@
import serviceTokenData from "./serviceTokenData";
export {
serviceTokenData
}

View File

@@ -0,0 +1,39 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../../middleware";
import { AuthMode } from "../../../variables";
import { serviceTokenDataController } from "../../controllers/v3";
router.get(
"/me/key",
requireAuth({
acceptedAuthModes: [AuthMode.SERVICE_TOKEN_V3]
}),
serviceTokenDataController.getServiceTokenDataKey
);
router.post(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
serviceTokenDataController.createServiceTokenData
);
router.patch(
"/:serviceTokenDataId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
serviceTokenDataController.updateServiceTokenData
);
router.delete(
"/:serviceTokenDataId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
serviceTokenDataController.deleteServiceTokenData
);
export default router;

View File

@@ -37,6 +37,7 @@ interface FeatureSet {
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null; status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
trial_end: number | null; trial_end: number | null;
has_used_trial: boolean; has_used_trial: boolean;
secretApproval: boolean;
} }
/** /**
@@ -64,7 +65,7 @@ class EELicenseService {
secretVersioning: true, secretVersioning: true,
pitRecovery: false, pitRecovery: false,
ipAllowlisting: false, ipAllowlisting: false,
rbac: true, rbac: false,
customRateLimits: false, customRateLimits: false,
customAlerts: false, customAlerts: false,
auditLogs: false, auditLogs: false,
@@ -72,7 +73,8 @@ class EELicenseService {
samlSSO: false, samlSSO: false,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true has_used_trial: true,
secretApproval: false
} }
public localFeatureSet: NodeCache; public localFeatureSet: NodeCache;

View File

@@ -1,6 +1,8 @@
import { Probot } from "probot"; import { Probot } from "probot";
import GitRisks from "../../models/gitRisks"; import {
import GitAppOrganizationInstallation from "../../models/gitAppOrganizationInstallation"; GitAppOrganizationInstallation,
GitRisks
} from "../../models";
import { scanGithubPushEventForSecretLeaks } from "../../../queues/secret-scanning/githubScanPushEvent"; import { scanGithubPushEventForSecretLeaks } from "../../../queues/secret-scanning/githubScanPushEvent";
export default async (app: Probot) => { export default async (app: Probot) => {
app.on("installation.deleted", async (context) => { app.on("installation.deleted", async (context) => {

View File

@@ -49,7 +49,8 @@ export enum ProjectPermissionSub {
IpAllowList = "ip-allowlist", IpAllowList = "ip-allowlist",
Workspace = "workspace", Workspace = "workspace",
Secrets = "secrets", Secrets = "secrets",
SecretRollback = "secret-rollback" SecretRollback = "secret-rollback",
SecretApproval = "secret-approval"
} }
type SubjectFields = { type SubjectFields = {
@@ -72,6 +73,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
@@ -85,6 +87,11 @@ const buildAdminPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Delete, 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.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
@@ -154,6 +161,8 @@ const buildMemberPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
@@ -203,6 +212,7 @@ const buildViewerPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility); const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);

View File

@@ -0,0 +1,656 @@
import picomatch from "picomatch";
import { Types } from "mongoose";
import {
containsGlobPatterns,
generateSecretBlindIndexWithSaltHelper,
getSecretBlindIndexSaltHelper
} from "../../helpers/secrets";
import { Folder, ISecret, Secret } from "../../models";
import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../models/secretApprovalPolicy";
import {
CommitType,
ISecretApprovalRequest,
ISecretApprovalSecChange,
ISecretCommits,
SecretApprovalRequest
} from "../models/secretApprovalRequest";
import { BadRequestError } from "../../utils/errors";
import { getFolderByPath } from "../../services/FolderService";
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, SECRET_SHARED } from "../../variables";
import TelemetryService from "../../services/TelemetryService";
import { EEAuditLogService, EESecretService } from "../services";
import { EventType, SecretVersion } from "../models";
import { AuthData } from "../../interfaces/middleware";
// if glob pattern score is 1, if not exist score is 0 and if its not both then its exact path meaning score 2
const getPolicyScore = (policy: ISecretApprovalPolicy) =>
policy.secretPath ? (containsGlobPatterns(policy.secretPath) ? 1 : 2) : 0;
// this will fetch the policy that gets priority for an environment and secret path
export const getSecretPolicyOfBoard = async (
workspaceId: string,
environment: string,
secretPath: string
) => {
const policies = await SecretApprovalPolicy.find({ workspace: workspaceId, environment });
if (!policies) return;
// this will filter policies either without scoped to secret path or the one that matches with secret path
const policiesFilteredByPath = policies.filter(
({ secretPath: policyPath }) =>
!policyPath || picomatch.isMatch(secretPath, policyPath, { strictSlashes: false })
);
// now sort by priority. exact secret path gets first match followed by glob followed by just env scoped
// if that is tie get by first createdAt
const policiesByPriority = policiesFilteredByPath.sort(
(a, b) => getPolicyScore(b) - getPolicyScore(a)
);
const finalPolicy = policiesByPriority.shift();
return finalPolicy;
};
const getLatestSecretVersion = async (secretIds: Types.ObjectId[]) => {
const latestSecretVersions = await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
},
type: SECRET_SHARED
}
},
{
$sort: { version: -1 }
},
{
$group: {
_id: "$secret",
version: { $max: "$version" },
versionId: { $max: "$_id" }, // id of latest secret versionId
secret: { $first: "$$ROOT" }
}
}
]).exec();
// reduced with secret id and latest version as document
return latestSecretVersions.reduce(
(prev, curr) => ({ ...prev, [curr._id.toString()]: curr.secret }),
{}
);
};
type TApprovalCreateSecret = Omit<ISecretApprovalSecChange, "_id" | "version"> & {
secretName: string;
};
type TApprovalUpdateSecret = Partial<Omit<ISecretApprovalSecChange, "_id" | "version">> & {
secretName: string;
newSecretName?: string;
};
type TGenerateSecretApprovalRequestArg = {
workspaceId: string;
environment: string;
secretPath: string;
policy: ISecretApprovalPolicy;
data: {
[CommitType.CREATE]?: TApprovalCreateSecret[];
[CommitType.UPDATE]?: TApprovalUpdateSecret[];
[CommitType.DELETE]?: { secretName: string }[];
};
commiterMembershipId: string;
authData: AuthData;
};
export const generateSecretApprovalRequest = async ({
workspaceId,
environment,
secretPath,
policy,
data,
commiterMembershipId,
authData
}: TGenerateSecretApprovalRequestArg) => {
// calculate folder id from secret path
let folderId = "root";
const rootFolder = await Folder.findOne({ workspace: workspaceId, environment });
if (!rootFolder && secretPath !== "/") throw BadRequestError({ message: "Folder not found" });
if (rootFolder) {
const folder = getFolderByPath(rootFolder.nodes, secretPath);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
// generate secret blindIndexes
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const commits: ISecretApprovalRequest["commits"] = [];
// -----
// for created secret approval change
const createdSecret = data[CommitType.CREATE];
if (createdSecret && createdSecret?.length) {
// validation checks whether secret exists for creation
const secretBlindIndexes = await Promise.all(
createdSecret.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[createdSecret[i].secretName] = curr;
return prev;
}, {})
);
// check created secret exists
const exists = await Secret.exists({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
createdSecret.map(({ secretName }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: SECRET_SHARED
}))
)
.exec();
if (exists) throw BadRequestError({ message: "Secrets already exist" });
commits.push(
...createdSecret.map((el) => ({
op: CommitType.CREATE as const,
newVersion: {
...el,
version: 0,
_id: new Types.ObjectId(),
secretBlindIndex: secretBlindIndexes[el.secretName]
}
}))
);
}
// ----
// updated secrets approval change
const updatedSecret = data[CommitType.UPDATE];
if (updatedSecret && updatedSecret?.length) {
// validation checks whether secret doesn't exists for update
const secretBlindIndexes = await Promise.all(
updatedSecret.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[updatedSecret[i].secretName] = curr;
return prev;
}, {})
);
// check update secret exists
const oldSecrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment,
type: SECRET_SHARED,
secretBlindIndex: {
$in: updatedSecret.map(({ secretName }) => secretBlindIndexes[secretName])
}
})
.select("+secretBlindIndex")
.lean()
.exec();
if (oldSecrets.length !== updatedSecret.length)
throw BadRequestError({ message: "Secrets already exist" });
// finally check updating blindindex exist
const nameUpdatedSecrets = updatedSecret.filter(({ newSecretName }) => Boolean(newSecretName));
const newSecretBlindIndexes = await Promise.all(
nameUpdatedSecrets.map(({ newSecretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName: newSecretName as string,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[nameUpdatedSecrets[i].secretName] = curr;
return prev;
}, {})
);
const doesAnySecretExistWithNewIndex = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment,
secretBlindIndex: { $in: Object.values(newSecretBlindIndexes) }
});
if (doesAnySecretExistWithNewIndex.length)
throw BadRequestError({ message: "Secret with new name already exist" });
const oldSecretsGroupById = oldSecrets.reduce<Record<string, ISecret>>(
(prev, curr) => ({ ...prev, [curr?.secretBlindIndex || ""]: curr }),
{}
);
const latestSecretVersions = await getLatestSecretVersion(
updatedSecret.map((el) => oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id)
);
commits.push(
...updatedSecret.map((el) => {
const secretId = oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id;
return {
op: CommitType.UPDATE as const,
secret: secretId,
secretVersion: latestSecretVersions[secretId.toString()]._id,
newVersion: {
...el,
secretBlindIndex: newSecretBlindIndexes?.[el.secretName],
_id: new Types.ObjectId(),
version: oldSecretsGroupById[secretBlindIndexes[el.secretName]].version || 1
}
};
})
);
}
// -----
// deleted secrets
const deletedSecrets = data[CommitType.DELETE];
if (deletedSecrets && deletedSecrets.length) {
const secretBlindIndexes = await Promise.all(
deletedSecrets.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[deletedSecrets[i].secretName] = curr;
return prev;
}, {})
);
const secretsToDelete = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment,
type: SECRET_SHARED,
secretBlindIndex: {
$in: deletedSecrets.map(({ secretName }) => secretBlindIndexes[secretName])
}
})
.select({ secretBlindIndex: 1, _id: 1 })
.lean()
.exec();
if (secretsToDelete.length !== deletedSecrets.length)
throw BadRequestError({ message: "Deleted secrets not found" });
const oldSecretsGroupById = secretsToDelete.reduce<Record<string, ISecret>>(
(prev, curr) => ({ ...prev, [curr?.secretBlindIndex || ""]: curr }),
{}
);
const latestSecretVersions = await getLatestSecretVersion(
deletedSecrets.map((el) => oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id)
);
commits.push(
...deletedSecrets.map((el) => {
const secretId = oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id;
return {
op: CommitType.DELETE as const,
secret: secretId,
secretVersion: latestSecretVersions[secretId.toString()]
};
})
);
}
const secretApprovalRequest = new SecretApprovalRequest({
workspace: workspaceId,
environment,
folderId,
policy,
commits,
committer: commiterMembershipId
});
await secretApprovalRequest.save();
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: commiterMembershipId,
secretApprovalRequestId: secretApprovalRequest._id.toString(),
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId: secretApprovalRequest.workspace
}
);
return secretApprovalRequest;
};
// validation for a merge conditions happen in another function in controller
export const performSecretApprovalRequestMerge = async (
id: string,
authData: AuthData,
userMembershipId: string
) => {
const secretApprovalRequest = await SecretApprovalRequest.findById(id)
.populate<{ commits: ISecretCommits<ISecret> }>({
path: "commits.secret",
select: "+secretBlindIndex",
populate: {
path: "tags"
}
})
.select("+commits.newVersion.secretBlindIndex");
if (!secretApprovalRequest) throw BadRequestError({ message: "Approval request not found" });
const workspaceId = secretApprovalRequest.workspace;
const environment = secretApprovalRequest.environment;
const folderId = secretApprovalRequest.folderId;
const postHogClient = await TelemetryService.getPostHogClient();
const conflicts: Array<{ secretId: string; op: CommitType }> = [];
const secretCreationCommits = secretApprovalRequest.commits.filter(
({ op }) => op === CommitType.CREATE
) as Array<{ op: CommitType.CREATE; newVersion: ISecretApprovalSecChange }>;
if (secretCreationCommits.length) {
// the created secrets already exist thus creation conflict ones
const conflictedSecrets = await Secret.find({
workspace: workspaceId,
environment,
folder: folderId,
secretBlindIndex: {
$in: secretCreationCommits.map(({ newVersion }) => newVersion.secretBlindIndex)
}
})
.select("+secretBlindIndex")
.lean();
const conflictGroupByBlindIndex = conflictedSecrets.reduce<Record<string, boolean>>(
(prev, curr) => ({ ...prev, [curr.secretBlindIndex || ""]: true }),
{}
);
const nonConflictSecrets = secretCreationCommits.filter(
({ newVersion }) => !conflictGroupByBlindIndex[newVersion.secretBlindIndex || ""]
);
secretCreationCommits
.filter(({ newVersion }) => conflictGroupByBlindIndex[newVersion.secretBlindIndex || ""])
.forEach((el) => {
conflicts.push({ op: CommitType.CREATE, secretId: el.newVersion._id.toString() });
});
// create secret
const newlyCreatedSecrets: ISecret[] = await Secret.insertMany(
nonConflictSecrets.map(
({
newVersion: {
secretKeyIV,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex,
algorithm,
keyEncoding,
tags
}
}) => ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
environment,
type: SECRET_SHARED,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
folder: folderId,
algorithm: algorithm || ALGORITHM_AES_256_GCM,
keyEncoding: keyEncoding || ENCODING_SCHEME_UTF8,
tags,
skipMultilineEncoding,
secretBlindIndex
})
)
);
await EESecretService.addSecretVersions({
secretVersions: newlyCreatedSecrets.map(
(secret) =>
new SecretVersion({
secret: secret._id,
version: secret.version,
workspace: secret.workspace,
type: secret.type,
folder: folderId,
tags: secret.tags,
skipMultilineEncoding: secret?.skipMultilineEncoding,
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
})
)
});
}
const secretUpdationCommits = secretApprovalRequest.commits.filter(
({ op }) => op === CommitType.UPDATE
) as Array<{
op: CommitType.UPDATE;
newVersion: Partial<Omit<ISecretApprovalSecChange, "_id">> & { _id: Types.ObjectId };
secret: ISecret;
}>;
if (secretUpdationCommits.length) {
const conflictedByNewBlindIndex = await Secret.find({
workspace: workspaceId,
environment,
folder: folderId,
secretBlindIndex: {
$in: secretUpdationCommits
.map(({ newVersion }) => newVersion?.secretBlindIndex)
.filter(Boolean)
}
})
.select("+secretBlindIndex")
.lean();
const conflictGroupByBlindIndex = conflictedByNewBlindIndex.reduce<Record<string, boolean>>(
(prev, curr) => (curr?.secretBlindIndex ? { ...prev, [curr.secretBlindIndex]: true } : prev),
{}
);
secretUpdationCommits
.filter(
({ newVersion, secret }) =>
(newVersion.secretBlindIndex && conflictGroupByBlindIndex[newVersion.secretBlindIndex]) ||
!secret
)
.forEach((el) => {
conflicts.push({ op: CommitType.UPDATE, secretId: el.newVersion._id.toString() });
});
const nonConflictSecrets = secretUpdationCommits.filter(
({ newVersion, secret }) =>
Boolean(secret) &&
(newVersion?.secretBlindIndex
? !conflictGroupByBlindIndex[newVersion.secretBlindIndex]
: true)
);
await Secret.bulkWrite(
// id and version are stripped off
nonConflictSecrets.map(
({
newVersion: {
secretKeyIV,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex,
tags
},
secret
}) => ({
updateOne: {
filter: {
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
secretBlindIndex: secret.secretBlindIndex,
type: SECRET_SHARED
},
update: {
$inc: {
version: 1
},
secretKeyIV,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex,
tags,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}
}
})
)
);
await EESecretService.addSecretVersions({
secretVersions: nonConflictSecrets.map(({ newVersion, secret }) => {
return new SecretVersion({
secret: secret._id,
version: secret.version + 1,
workspace: workspaceId,
type: SECRET_SHARED,
folder: folderId,
environment,
isDeleted: false,
secretBlindIndex: newVersion?.secretBlindIndex ?? secret.secretBlindIndex,
secretKeyCiphertext: newVersion?.secretKeyCiphertext ?? secret.secretKeyCiphertext,
secretKeyIV: newVersion?.secretKeyIV ?? secret.secretKeyCiphertext,
secretKeyTag: newVersion?.secretKeyTag ?? secret.secretKeyTag,
secretValueCiphertext: newVersion?.secretValueCiphertext ?? secret.secretValueCiphertext,
secretValueIV: newVersion?.secretValueIV ?? secret.secretValueIV,
secretValueTag: newVersion?.secretValueTag ?? secret.secretValueTag,
tags: newVersion?.tags ?? secret.tags,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
skipMultilineEncoding: newVersion?.skipMultilineEncoding ?? secret.skipMultilineEncoding
});
})
});
}
const secretDeletionCommits = secretApprovalRequest.commits.filter(
({ op }) => op === CommitType.DELETE
) as Array<{
op: CommitType.DELETE;
secret: ISecret;
}>;
if (secretDeletionCommits.length) {
await Secret.deleteMany({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
secretDeletionCommits.map(({ secret: { secretBlindIndex } }) => ({
secretBlindIndex,
type: { $in: ["shared", "personal"] }
}))
)
.exec();
await EESecretService.markDeletedSecretVersions({
secretIds: secretDeletionCommits.map(({ secret }) => secret._id)
});
}
const updatedSecretApproval = await SecretApprovalRequest.findByIdAndUpdate(
id,
{
conflicts,
hasMerged: true,
status: "close",
statusChangeBy: userMembershipId
},
{ new: true }
);
if (postHogClient) {
if (postHogClient) {
postHogClient.capture({
event: "secrets merged",
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: secretApprovalRequest.commits.length,
environment,
workspaceId,
folderId,
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
}
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId
});
// question to team where to keep secretKey
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.SECRET_APPROVAL_MERGED,
metadata: {
mergedBy: userMembershipId,
secretApprovalRequestId: id,
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId
}
);
return updatedSecretApproval;
};

View File

@@ -0,0 +1,54 @@
import { z } from "zod";
export const GetSecretApprovalRuleList = z.object({
query: z.object({
workspaceId: z.string().trim()
})
});
export const GetSecretApprovalPolicyOfABoard = z.object({
query: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim()
})
});
export const CreateSecretApprovalRule = z.object({
body: z
.object({
workspaceId: z.string(),
name: z.string().optional(),
environment: z.string(),
secretPath: z.string().optional().nullable(),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
})
});
export const UpdateSecretApprovalRule = z.object({
params: z.object({
id: z.string()
}),
body: z
.object({
name: z.string().optional(),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1),
secretPath: z.string().optional().nullable()
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
})
});
export const DeleteSecretApprovalRule = z.object({
params: z.object({
id: z.string()
})
});

View File

@@ -0,0 +1,49 @@
import { z } from "zod";
import { ApprovalStatus } from "../models/secretApprovalRequest";
export const getSecretApprovalRequests = z.object({
query: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim().optional(),
committer: z.string().trim().optional(),
status: z.enum(["open", "close"]).optional(),
limit: z.coerce.number().default(20),
offset: z.coerce.number().default(0)
})
});
export const getSecretApprovalRequestCount = z.object({
query: z.object({
workspaceId: z.string().trim()
})
});
export const getSecretApprovalRequestDetails = z.object({
params: z.object({
id: z.string().trim()
})
});
export const updateSecretApprovalReviewStatus = z.object({
body: z.object({
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
}),
params: z.object({
id: z.string().trim()
})
});
export const mergeSecretApprovalRequest = z.object({
params: z.object({
id: z.string().trim()
})
});
export const updateSecretApprovalRequestStatus = z.object({
params: z.object({
id: z.string().trim()
}),
body: z.object({
status: z.enum(["open", "close"])
})
});

View File

@@ -7,6 +7,7 @@ import {
ITokenVersion, ITokenVersion,
IUser, IUser,
ServiceTokenData, ServiceTokenData,
ServiceTokenDataV3,
TokenVersion, TokenVersion,
User, User,
} from "../models"; } from "../models";
@@ -18,17 +19,18 @@ import {
UnauthorizedRequestError, UnauthorizedRequestError,
} from "../utils/errors"; } from "../utils/errors";
import { import {
getAuthSecret,
getJwtAuthLifetime, getJwtAuthLifetime,
getJwtAuthSecret,
getJwtProviderAuthSecret,
getJwtRefreshLifetime, getJwtRefreshLifetime,
getJwtRefreshSecret, getJwtServiceTokenSecret
} from "../config"; } from "../config";
import { import {
AuthMode AuthMode,
AuthTokenType
} from "../variables"; } from "../variables";
import { import {
ServiceTokenAuthData, ServiceTokenAuthData,
ServiceTokenV3AuthData,
UserAuthData UserAuthData
} from "../interfaces/middleware"; } from "../interfaces/middleware";
@@ -47,6 +49,7 @@ export const validateAuthMode = ({
headers: { [key: string]: string | string[] | undefined }, headers: { [key: string]: string | string[] | undefined },
acceptedAuthModes: AuthMode[] acceptedAuthModes: AuthMode[]
}) => { }) => {
const apiKey = headers["x-api-key"]; const apiKey = headers["x-api-key"];
const authHeader = headers["authorization"]; const authHeader = headers["authorization"];
@@ -65,6 +68,7 @@ export const validateAuthMode = ({
if (typeof authHeader === "string") { if (typeof authHeader === "string") {
// case: treat request authentication type as via Authorization header (i.e. either JWT or service token) // case: treat request authentication type as via Authorization header (i.e. either JWT or service token)
const [tokenType, tokenValue] = <[string, string]>authHeader.split(" ", 2) ?? [null, null] const [tokenType, tokenValue] = <[string, string]>authHeader.split(" ", 2) ?? [null, null]
if (tokenType === null) if (tokenType === null)
throw BadRequestError({ message: "Missing Authorization Header in the request header." }); throw BadRequestError({ message: "Missing Authorization Header in the request header." });
if (tokenType.toLowerCase() !== "bearer") if (tokenType.toLowerCase() !== "bearer")
@@ -72,16 +76,22 @@ export const validateAuthMode = ({
if (tokenValue === null) if (tokenValue === null)
throw BadRequestError({ message: "Missing Authorization Body in the request header." }); throw BadRequestError({ message: "Missing Authorization Body in the request header." });
switch (tokenValue.split(".", 1)[0]) { const parts = tokenValue.split(".");
switch (parts[0]) {
case "st": case "st":
authMode = AuthMode.SERVICE_TOKEN; authMode = AuthMode.SERVICE_TOKEN;
authTokenValue = tokenValue;
break;
case "stv3":
authMode = AuthMode.SERVICE_TOKEN_V3;
authTokenValue = parts.slice(1).join(".");
break; break;
default: default:
authMode = AuthMode.JWT; authMode = AuthMode.JWT;
}
authTokenValue = tokenValue; authTokenValue = tokenValue;
} }
}
if (!authMode || !authTokenValue) throw BadRequestError({ message: "Missing valid Authorization or X-API-KEY in request header." }); if (!authMode || !authTokenValue) throw BadRequestError({ message: "Missing valid Authorization or X-API-KEY in request header." });
@@ -107,9 +117,11 @@ export const getAuthUserPayload = async ({
authTokenValue: string; authTokenValue: string;
}): Promise<UserAuthData> => { }): Promise<UserAuthData> => {
const decodedToken = <jwt.UserIDJwtPayload>( const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, await getJwtAuthSecret()) jwt.verify(authTokenValue, await getAuthSecret())
); );
if (decodedToken.authTokenType !== AuthTokenType.ACCESS_TOKEN) throw UnauthorizedRequestError();
const user = await User.findOne({ const user = await User.findOne({
_id: new Types.ObjectId(decodedToken.userId), _id: new Types.ObjectId(decodedToken.userId),
}).select("+publicKey +accessVersion"); }).select("+publicKey +accessVersion");
@@ -146,11 +158,6 @@ export const getAuthUserPayload = async ({
userAgent: req.headers["user-agent"] ?? "", userAgent: req.headers["user-agent"] ?? "",
userAgentType: getUserAgentType(req.headers["user-agent"]) userAgentType: getUserAgentType(req.headers["user-agent"])
} }
// return ({
// user,
// tokenVersionId: tokenVersion._id, // what to do with this? // move this out
// });
} }
/** /**
@@ -211,8 +218,73 @@ export const getAuthSTDPayload = async ({
userAgent: req.headers["user-agent"] ?? "", userAgent: req.headers["user-agent"] ?? "",
userAgentType: getUserAgentType(req.headers["user-agent"]) userAgentType: getUserAgentType(req.headers["user-agent"])
} }
}
// return serviceTokenDataToReturn; /**
* Return service token data V3 payload corresponding to service token [authTokenValue]
* @param {Object} obj
* @param {String} obj.authTokenValue - service token value
* @returns {ServiceTokenData} serviceTokenData - service token data
*/
export const getAuthSTDV3Payload = async ({
req,
authTokenValue,
}: {
req: Request,
authTokenValue: string;
}): Promise<ServiceTokenV3AuthData> => {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, await getJwtServiceTokenSecret())
);
const serviceTokenData = await ServiceTokenDataV3.findOneAndUpdate(
{
_id: new Types.ObjectId(decodedToken._id),
isActive: true
},
{
lastUsed: new Date(),
$inc: { usageCount: 1 }
},
{
new: true
}
);
if (!serviceTokenData) {
throw UnauthorizedRequestError({
message: "Failed to authenticate"
});
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired
await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenData._id,
{
isActive: false
},
{
new: true
}
);
throw UnauthorizedRequestError({
message: "Failed to authenticate",
});
}
return {
actor: {
type: ActorType.SERVICE_V3,
metadata: {
serviceId: serviceTokenData._id.toString(),
name: serviceTokenData.name
}
},
authPayload: serviceTokenData,
ipAddress: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgentType: getUserAgentType(req.headers["user-agent"])
}
} }
/** /**
@@ -326,22 +398,24 @@ export const issueAuthTokens = async ({
// issue tokens // issue tokens
const token = createToken({ const token = createToken({
payload: { payload: {
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId, userId,
tokenVersionId: tokenVersion._id.toString(), tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.accessVersion, accessVersion: tokenVersion.accessVersion,
}, },
expiresIn: await getJwtAuthLifetime(), expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret(), secret: await getAuthSecret(),
}); });
const refreshToken = createToken({ const refreshToken = createToken({
payload: { payload: {
authTokenType: AuthTokenType.REFRESH_TOKEN,
userId, userId,
tokenVersionId: tokenVersion._id.toString(), tokenVersionId: tokenVersion._id.toString(),
refreshVersion: tokenVersion.refreshVersion, refreshVersion: tokenVersion.refreshVersion,
}, },
expiresIn: await getJwtRefreshLifetime(), expiresIn: await getJwtRefreshLifetime(),
secret: await getJwtRefreshSecret(), secret: await getAuthSecret(),
}); });
return { return {
@@ -373,7 +447,7 @@ export const clearTokens = async (tokenVersionId: Types.ObjectId): Promise<void>
* bearer/auth, refresh, and temporary signup tokens * bearer/auth, refresh, and temporary signup tokens
* @param {Object} obj * @param {Object} obj
* @param {Object} obj.payload - payload of (JWT) token * @param {Object} obj.payload - payload of (JWT) token
* @param {String} obj.secret - (JWT) secret such as [JWT_AUTH_SECRET] * @param {String} obj.secret - (JWT) secret such as [AUTH_SECRET]
* @param {String} obj.expiresIn - string describing time span such as '10h' or '7d' * @param {String} obj.expiresIn - string describing time span such as '10h' or '7d'
*/ */
export const createToken = ({ export const createToken = ({
@@ -382,11 +456,15 @@ export const createToken = ({
secret, secret,
}: { }: {
payload: any; payload: any;
expiresIn: string | number; expiresIn?: string | number;
secret: string; secret: string;
}) => { }) => {
return jwt.sign(payload, secret, { return jwt.sign(payload, secret, {
expiresIn, ...(
(expiresIn !== undefined && expiresIn !== null)
? { expiresIn }
: {}
)
}); });
}; };
@@ -397,14 +475,17 @@ export const validateProviderAuthToken = async ({
email: string; email: string;
providerAuthToken?: string; providerAuthToken?: string;
}) => { }) => {
if (!providerAuthToken) { if (!providerAuthToken) {
throw new Error("Invalid authentication request."); throw new Error("Invalid authentication request.");
} }
const decodedToken = <jwt.ProviderAuthJwtPayload>( const decodedToken = <jwt.ProviderAuthJwtPayload>(
jwt.verify(providerAuthToken, await getJwtProviderAuthSecret()) jwt.verify(providerAuthToken, await getAuthSecret())
); );
if (decodedToken.authTokenType !== AuthTokenType.PROVIDER_TOKEN) throw UnauthorizedRequestError();
if (decodedToken.email !== email) { if (decodedToken.email !== email) {
throw new Error("Invalid authentication credentials.") throw new Error("Invalid authentication credentials.")
} }

View File

@@ -103,7 +103,10 @@ export const getSecretsBotHelper = async ({
environment: string; environment: string;
secretPath: 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 }); const key = await getKey({ workspaceId: workspaceId });
let folderId = "root"; let folderId = "root";
@@ -165,6 +168,8 @@ export const getSecretsBotHelper = async ({
}); });
content[secretKey].comment = commentValue; content[secretKey].comment = commentValue;
} }
content[secretKey].skipMultilineEncoding = secret.skipMultilineEncoding;
}); });
}); });
@@ -194,6 +199,8 @@ export const getSecretsBotHelper = async ({
}); });
content[secretKey].comment = commentValue; content[secretKey].comment = commentValue;
} }
content[secretKey].skipMultilineEncoding = secret.skipMultilineEncoding;
}); });
await expandSecrets(workspaceId.toString(), key, content); await expandSecrets(workspaceId.toString(), key, content);

View File

@@ -31,14 +31,10 @@ export const initDatabaseHelper = async ({
* Close database conection * Close database conection
*/ */
export const closeDatabaseHelper = async () => { export const closeDatabaseHelper = async () => {
return Promise.all([ if (mongoose.connection && mongoose.connection.readyState === 1) {
new Promise((resolve) => { await mongoose.connection.close();
if (mongoose.connection && mongoose.connection.readyState == 1) { return "Database connection closed";
mongoose.connection.close()
.then(() => resolve("Database connection closed"));
} else { } else {
resolve("Database connection already closed"); return "Database connection already closed";
} }
}), };
]);
}

View File

@@ -15,6 +15,7 @@ import { IntegrationAuthMetadata } from "../models/integrationAuth/types";
interface Update { interface Update {
workspace: string; workspace: string;
integration: string; integration: string;
url?: string;
teamId?: string; teamId?: string;
accountId?: string; accountId?: string;
metadata?: IntegrationAuthMetadata metadata?: IntegrationAuthMetadata
@@ -64,6 +65,10 @@ export const handleOAuthExchangeHelper = async ({
integration integration
}; };
if (res.url) {
update.url = res.url;
}
switch (integration) { switch (integration) {
case INTEGRATION_VERCEL: case INTEGRATION_VERCEL:
update.teamId = res.teamId; update.teamId = res.teamId;
@@ -160,7 +165,7 @@ export const getIntegrationAuthAccessHelper = async ({
let accessId; let accessId;
let accessToken; let accessToken;
const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select( const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select(
"workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt +refreshCiphertext +refreshIV +refreshTag +accessIdCiphertext +accessIdIV +accessIdTag metadata teamId" "workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt +refreshCiphertext +refreshIV +refreshTag +accessIdCiphertext +accessIdIV +accessIdTag metadata teamId url"
); );
if (!integrationAuth) if (!integrationAuth)

View File

@@ -1,5 +1,43 @@
import { Types } from "mongoose"; import mongoose, { Types, mongo } from "mongoose";
import { MembershipOrg, Organization } from "../models"; import {
Bot,
BotKey,
BotOrg,
Folder,
IncidentContactOrg,
Integration,
IntegrationAuth,
Key,
Membership,
MembershipOrg,
Organization,
Secret,
SecretBlindIndexData,
SecretImport,
ServiceToken,
ServiceTokenData,
ServiceTokenDataV3,
ServiceTokenDataV3Key,
Tag,
Webhook,
Workspace
} from "../models";
import {
Action,
AuditLog,
FolderVersion,
GitAppInstallationSession,
GitAppOrganizationInstallation,
GitRisks,
Log,
Role,
SSOConfig,
SecretApprovalPolicy,
SecretApprovalRequest,
SecretSnapshot,
SecretVersion,
TrustedIP
} from "../ee/models";
import { import {
ACCEPTED, ACCEPTED,
} from "../variables"; } from "../variables";
@@ -17,6 +55,7 @@ import {
import { import {
createBotOrg createBotOrg
} from "./botOrg"; } from "./botOrg";
import { InternalServerError, ResourceNotFoundError } from "../utils/errors";
/** /**
* Create an organization with name [name] * Create an organization with name [name]
@@ -65,6 +104,320 @@ export const createOrganization = async ({
return organization; return organization;
}; };
/**
* Delete organization with id [organizationId]
* @param {Object} obj
* @param {Types.ObjectId} obj.organizationId - id of organization to delete
* @returns
*/
export const deleteOrganization = async ({
organizationId,
existingSession
}: {
organizationId: Types.ObjectId;
existingSession?: mongo.ClientSession;
}) => {
let session;
if (existingSession) {
session = existingSession;
} else {
session = await mongoose.startSession();
session.startTransaction();
}
try {
const organization = await Organization.findByIdAndDelete(
organizationId,
{
session
}
);
if (!organization) throw ResourceNotFoundError();
await MembershipOrg.deleteMany({
organization: organization._id
}, {
session
});
await BotOrg.deleteMany({
organization: organization._id
}, {
session
});
await SSOConfig.deleteMany({
organization: organization._id
}, {
session
});
await Role.deleteMany({
organization: organization._id
}, {
session
});
await IncidentContactOrg.deleteMany({
organization: organization._id
}, {
session
});
await GitRisks.deleteMany({
organization: organization._id
}, {
session
});
await GitAppInstallationSession.deleteMany({
organization: organization._id
}, {
session
});
await GitAppOrganizationInstallation.deleteMany({
organization: organization._id
}, {
session
});
const workspaceIds = await Workspace.distinct("_id", {
organization: organization._id
});
await Workspace.deleteMany({
organization: organization._id
}, {
session
});
await Membership.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Key.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Bot.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await BotKey.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretBlindIndexData.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Secret.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretVersion.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretSnapshot.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretImport.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Folder.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await FolderVersion.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Webhook.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await TrustedIP.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Tag.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await IntegrationAuth.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Integration.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await ServiceToken.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await ServiceTokenData.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await ServiceTokenDataV3.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await ServiceTokenDataV3Key.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await AuditLog.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Log.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Action.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretApprovalPolicy.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretApprovalRequest.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
if (organization.customerId) {
// delete from stripe here
await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}`
);
}
return organization;
} catch (err) {
if (!existingSession) {
await session.abortTransaction();
}
throw InternalServerError({
message: "Failed to delete organization"
});
} finally {
if (!existingSession) {
await session.commitTransaction();
session.endSession();
}
}
}
/** /**
* Update organization subscription quantity to reflect number of members in * Update organization subscription quantity to reflect number of members in
* the organization. * the organization.

View File

@@ -1,20 +1,25 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import {
CreateSecretBatchParams,
CreateSecretParams, CreateSecretParams,
DeleteSecretBatchParams,
DeleteSecretParams, DeleteSecretParams,
GetSecretParams, GetSecretParams,
GetSecretsParams, GetSecretsParams,
UpdateSecretBatchParams,
UpdateSecretParams UpdateSecretParams
} from "../interfaces/services/SecretService"; } from "../interfaces/services/SecretService";
import { import {
Folder, Folder,
ISecret, ISecret,
IServiceTokenData, IServiceTokenData,
IServiceTokenDataV3,
Secret, Secret,
SecretBlindIndexData, SecretBlindIndexData,
ServiceTokenData, ServiceTokenData,
TFolderRootSchema TFolderRootSchema
} from "../models"; } from "../models";
import { Permission } from "../models/serviceTokenDataV3";
import { EventType, SecretVersion } from "../ee/models"; import { EventType, SecretVersion } from "../ee/models";
import { import {
BadRequestError, BadRequestError,
@@ -48,7 +53,51 @@ import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/aut
import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService"; import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService";
import picomatch from "picomatch"; import picomatch from "picomatch";
import path from "path"; import path from "path";
import { getAnImportedSecret } from "../services/SecretImportService";
/**
* Validate scope for service token v3
* @param authPayload
* @param environment
* @param secretPath
* @returns
*/
export const isValidScopeV3 = ({
authPayload,
environment,
secretPath,
requiredPermissions
}: {
authPayload: IServiceTokenDataV3;
environment: string;
secretPath: string;
requiredPermissions: Permission[];
}) => {
const { scopes } = authPayload;
const validScope = scopes.find(
(scope) =>
picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) &&
scope.environment === environment
);
if (
validScope &&
!requiredPermissions.every((permission) => validScope.permissions.includes(permission))
) {
return false;
}
return Boolean(validScope);
};
/**
* Validate scope for service token v2
* @param authPayload
* @param environment
* @param secretPath
* @returns
*/
export const isValidScope = ( export const isValidScope = (
authPayload: IServiceTokenData, authPayload: IServiceTokenData,
environment: string, environment: string,
@@ -70,6 +119,8 @@ export function containsGlobPatterns(secretPath: string) {
return globChars.some((char) => normalizedPath.includes(char)); 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. * Returns an object containing secret [secret] but with its value, key, comment decrypted.
* *
@@ -329,7 +380,8 @@ export const createSecretHelper = async ({
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
secretPath = "/", secretPath = "/",
metadata metadata,
skipMultilineEncoding
}: CreateSecretParams) => { }: CreateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
@@ -393,6 +445,7 @@ export const createSecretHelper = async ({
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
skipMultilineEncoding,
folder: folderId, folder: folderId,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8,
@@ -415,6 +468,7 @@ export const createSecretHelper = async ({
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
skipMultilineEncoding,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8
}); });
@@ -622,13 +676,14 @@ export const getSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
secretPath = "/" secretPath = "/",
include_imports = true
}: GetSecretParams) => { }: GetSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId) 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 using service token filter towards the folderId by secretpath
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
@@ -655,6 +710,11 @@ export const getSecretHelper = async ({
}).lean(); }).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(); if (!secret) throw SecretNotFoundError();
// (EE) create (audit) log // (EE) create (audit) log
@@ -730,27 +790,70 @@ export const getSecretHelper = async ({
export const updateSecretHelper = async ({ export const updateSecretHelper = async ({
secretName, secretName,
workspaceId, workspaceId,
secretId,
environment, environment,
type, type,
authData, authData,
newSecretName,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath secretPath,
tags,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
skipMultilineEncoding
}: UpdateSecretParams) => { }: UpdateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ // get secret blind index salt
secretName, const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
}); });
let oldSecretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
secretName,
salt
});
if (secretId) {
const secret = await Secret.findOne({
workspace: workspaceId,
environment,
_id: secretId
}).select("secretBlindIndex");
if (secret && secret.secretBlindIndex) oldSecretBlindIndex = secret.secretBlindIndex;
}
let secret: ISecret | null = null; let secret: ISecret | null = null;
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); 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" });
}
}
if (type === SECRET_SHARED) { if (type === SECRET_SHARED) {
// case: update shared secret // case: update shared secret
secret = await Secret.findOneAndUpdate( secret = await Secret.findOneAndUpdate(
{ {
secretBlindIndex, secretBlindIndex: oldSecretBlindIndex,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId, folder: folderId,
@@ -760,6 +863,15 @@ export const updateSecretHelper = async ({
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex: newSecretNameBlindIndex,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
tags,
$inc: { version: 1 } $inc: { version: 1 }
}, },
{ {
@@ -771,7 +883,7 @@ export const updateSecretHelper = async ({
secret = await Secret.findOneAndUpdate( secret = await Secret.findOneAndUpdate(
{ {
secretBlindIndex, secretBlindIndex: oldSecretBlindIndex,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
@@ -782,6 +894,12 @@ export const updateSecretHelper = async ({
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
tags,
skipMultilineEncoding,
secretBlindIndex: newSecretNameBlindIndex,
$inc: { version: 1 } $inc: { version: 1 }
}, },
{ {
@@ -798,16 +916,18 @@ export const updateSecretHelper = async ({
workspace: secret.workspace, workspace: secret.workspace,
folder: folderId, folder: folderId,
type, type,
tags,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment, environment: secret.environment,
isDeleted: false, isDeleted: false,
secretBlindIndex, secretBlindIndex: newSecretName ? newSecretNameBlindIndex : oldSecretBlindIndex,
secretKeyCiphertext: secret.secretKeyCiphertext, secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV, secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag, secretKeyTag: secret.secretKeyTag,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
skipMultilineEncoding,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8
}); });
@@ -896,12 +1016,22 @@ export const deleteSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
secretPath = "/" secretPath = "/",
// used for update corner case and blindIndex goes wrong way
secretId
}: DeleteSecretParams) => { }: DeleteSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ let secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
}); });
if (secretId) {
const secret = await Secret.findOne({
workspace: workspaceId,
environment,
_id: secretId
}).select("secretBlindIndex");
if (secret && secret.secretBlindIndex) secretBlindIndex = secret.secretBlindIndex;
}
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
@@ -1091,6 +1221,7 @@ const recursivelyExpandSecret = async (
let interpolatedValue = interpolatedSec[key]; let interpolatedValue = interpolatedSec[key];
if (!interpolatedValue) { if (!interpolatedValue) {
// eslint-disable-next-line no-console
console.error(`Couldn't find referenced value - ${key}`); console.error(`Couldn't find referenced value - ${key}`);
return ""; return "";
} }
@@ -1140,7 +1271,7 @@ const formatMultiValueEnv = (val?: string) => {
export const expandSecrets = async ( export const expandSecrets = async (
workspaceId: string, workspaceId: string,
rootEncKey: 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 expandedSec: Record<string, string> = {};
const interpolatedSec: Record<string, string> = {}; const interpolatedSec: Record<string, string> = {};
@@ -1158,7 +1289,10 @@ export const expandSecrets = async (
for (const key of Object.keys(secrets)) { for (const key of Object.keys(secrets)) {
if (expandedSec?.[key]) { 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; continue;
} }
@@ -1173,8 +1307,506 @@ export const expandSecrets = async (
key key
); );
secrets[key].value = formatMultiValueEnv(expandedVal); secrets[key].value = secrets[key].skipMultilineEncoding
? expandedVal
: formatMultiValueEnv(expandedVal);
} }
return secrets; 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,5 +1,27 @@
import { IUser, User } from "../models"; import mongoose, { Types, mongo } from "mongoose";
import {
APIKeyData,
BackupPrivateKey,
IUser,
Key,
Membership,
MembershipOrg,
TokenVersion,
User,
UserAction
} from "../models";
import {
Action,
Log
} from "../ee/models";
import { sendMail } from "./nodemailer"; import { sendMail } from "./nodemailer";
import {
InternalServerError,
ResourceNotFoundError
} from "../utils/errors";
import { ADMIN } from "../variables";
import { deleteOrganization } from "../helpers/organization";
import { deleteWorkspace } from "../helpers/workspace";
/** /**
* Initialize a user under email [email] * Initialize a user under email [email]
@@ -134,3 +156,207 @@ export const checkUserDevice = async ({
}); });
} }
}; };
/**
* Check that if we delete user with id [userId] then
* there won't be any admin-less organizations or projects
* @param {Object} obj
* @param {String} obj.userId - id of user to check deletion conditions for
*/
const checkDeleteUserConditions = async ({
userId
}: {
userId: Types.ObjectId;
}) => {
const memberships = await Membership.find({
user: userId
});
const membershipOrgs = await MembershipOrg.find({
user: userId
});
// delete organizations where user is only member
for await (const membershipOrg of membershipOrgs) {
const orgMemberCount = await MembershipOrg.countDocuments({
organization: membershipOrg.organization,
});
const otherOrgAdminCount = await MembershipOrg.countDocuments({
organization: membershipOrg.organization,
user: { $ne: userId },
role: ADMIN
});
if (orgMemberCount > 1 && otherOrgAdminCount === 0) {
throw InternalServerError({
message: "Failed to delete account because an org would be admin-less"
});
}
}
// delete workspaces where user is only member
for await (const membership of memberships) {
const workspaceMemberCount = await Membership.countDocuments({
workspace: membership.workspace
});
const otherWorkspaceAdminCount = await Membership.countDocuments({
workspace: membership.workspace,
user: { $ne: userId },
role: ADMIN
});
if (workspaceMemberCount > 1 && otherWorkspaceAdminCount === 0) {
throw InternalServerError({
message: "Failed to delete account because a workspace would be admin-less"
});
}
}
}
/**
* Delete account with id [userId]
* @param {Object} obj
* @param {Types.ObjectId} obj.userId - id of user to delete
* @returns {User} user - deleted user
*/
export const deleteUser = async ({
userId,
existingSession
}: {
userId: Types.ObjectId;
existingSession?: mongo.ClientSession;
}) => {
let session;
if (existingSession) {
session = existingSession;
} else {
session = await mongoose.startSession();
session.startTransaction();
}
try {
const user = await User.findByIdAndDelete(userId, {
session
});
if (!user) throw ResourceNotFoundError();
await checkDeleteUserConditions({
userId: user._id
});
await UserAction.deleteMany({
user: user._id
}, {
session
});
await BackupPrivateKey.deleteMany({
user: user._id
}, {
session
});
await APIKeyData.deleteMany({
user: user._id
}, {
session
});
await Action.deleteMany({
user: user._id
}, {
session
});
await Log.deleteMany({
user: user._id
}, {
session
});
await TokenVersion.deleteMany({
user: user._id
});
await Key.deleteMany({
receiver: user._id
}, {
session
});
const membershipOrgs = await MembershipOrg.find({
user: userId
}, null, {
session
});
// delete organizations where user is only member
for await (const membershipOrg of membershipOrgs) {
const memberCount = await MembershipOrg.countDocuments({
organization: membershipOrg.organization
});
if (memberCount === 1) {
// organization only has 1 member (the current user)
await deleteOrganization({
organizationId: membershipOrg.organization,
existingSession: session
});
}
}
const memberships = await Membership.find({
user: userId
}, null, {
session
});
// delete workspaces where user is only member
for await (const membership of memberships) {
const memberCount = await Membership.countDocuments({
workspace: membership.workspace
});
if (memberCount === 1) {
// workspace only has 1 member (the current user) -> delete workspace
await deleteWorkspace({
workspaceId: membership.workspace,
existingSession: session
});
}
}
await MembershipOrg.deleteMany({
user: userId
}, {
session
});
await Membership.deleteMany({
user: userId
}, {
session
});
return user;
} catch (err) {
if (!existingSession) {
await session.abortTransaction();
}
throw InternalServerError({
message: "Failed to delete account"
})
} finally {
if (!existingSession) {
await session.commitTransaction();
session.endSession();
}
}
}

View File

@@ -1,18 +1,42 @@
import { Types } from "mongoose"; import mongoose, { Types, mongo } from "mongoose";
import { import {
Bot, Bot,
BotKey,
Folder,
Integration,
IntegrationAuth,
Key, Key,
Membership, Membership,
Secret, Secret,
Workspace, SecretBlindIndexData,
SecretImport,
ServiceToken,
ServiceTokenData,
ServiceTokenDataV3,
ServiceTokenDataV3Key,
Tag,
Webhook,
Workspace
} from "../models"; } from "../models";
import { import {
Action,
AuditLog,
FolderVersion,
IPType, IPType,
Log,
SecretApprovalPolicy,
SecretApprovalRequest,
SecretSnapshot,
SecretVersion,
TrustedIP TrustedIP
} from "../ee/models"; } from "../ee/models";
import { createBot } from "../helpers/bot"; import { createBot } from "../helpers/bot";
import { EELicenseService } from "../ee/services"; import { EELicenseService } from "../ee/services";
import { SecretService } from "../services"; import { SecretService } from "../services";
import {
InternalServerError,
ResourceNotFoundError
} from "../utils/errors";
/** /**
* Create a workspace with name [name] in organization with id [organizationId] * Create a workspace with name [name] in organization with id [organizationId]
@@ -77,18 +101,190 @@ export const createWorkspace = async ({
* @param {Object} obj * @param {Object} obj
* @param {String} obj.id - id of workspace to delete * @param {String} obj.id - id of workspace to delete
*/ */
export const deleteWorkspace = async ({ id }: { id: string }) => { export const deleteWorkspace = async ({
await Workspace.deleteOne({ _id: id }); workspaceId,
await Bot.deleteOne({ existingSession
workspace: id, }: {
}); workspaceId: Types.ObjectId;
existingSession?: mongo.ClientSession;
}) => {
let session;
if (existingSession) {
session = existingSession;
} else {
session = await mongoose.startSession();
session.startTransaction();
}
try {
const workspace = await Workspace.findByIdAndDelete(workspaceId, { session });
if (!workspace) throw ResourceNotFoundError();
await Membership.deleteMany({ await Membership.deleteMany({
workspace: id, workspace: workspace._id
}); }, {
await Secret.deleteMany({ session
workspace: id,
}); });
await Key.deleteMany({ await Key.deleteMany({
workspace: id, workspace: workspace._id
}, {
session
}); });
await Bot.deleteMany({
workspace: workspace._id
}, {
session
});
await BotKey.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretBlindIndexData.deleteMany({
workspace: workspace._id
}, {
session
});
await Secret.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretVersion.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretSnapshot.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretImport.deleteMany({
workspace: workspace._id
}, {
session
});
await Folder.deleteMany({
workspace: workspace._id
}, {
session
});
await FolderVersion.deleteMany({
workspace: workspace._id
}, {
session
});
await Webhook.deleteMany({
workspace: workspace._id
}, {
session
});
await TrustedIP.deleteMany({
workspace: workspace._id
}, {
session
});
await Tag.deleteMany({
workspace: workspace._id
}, {
session
});
await IntegrationAuth.deleteMany({
workspace: workspace._id
}, {
session
});
await Integration.deleteMany({
workspace: workspace._id
}, {
session
});
await ServiceToken.deleteMany({
workspace: workspace._id
}, {
session
});
await ServiceTokenData.deleteMany({
workspace: workspace._id
}, {
session
});
await ServiceTokenDataV3.deleteMany({
workspace: workspace._id
}, {
session
});
await ServiceTokenDataV3Key.deleteMany({
workspace: workspace._id
}, {
session
});
await AuditLog.deleteMany({
workspace: workspace._id
}, {
session
});
await Log.deleteMany({
workspace: workspace._id
}, {
session
});
await Action.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretApprovalPolicy.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretApprovalRequest.deleteMany({
workspace: workspace._id
}, {
session
});
return workspace;
} catch (err) {
if (!existingSession) {
await session.abortTransaction();
}
throw InternalServerError({
message: "Failed to delete organization"
});
} finally {
if (!existingSession) {
await session.commitTransaction();
session.endSession();
}
}
}; };

View File

@@ -25,8 +25,11 @@ import {
users as eeUsersRouter, users as eeUsersRouter,
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter,
roles as v1RoleRouter, roles as v1RoleRouter,
secretApprovalPolicy as v1SecretApprovalPolicy,
secretApprovalRequest as v1SecretApprovalRequest,
secretScanning as v1SecretScanningRouter secretScanning as v1SecretScanningRouter
} from "./ee/routes/v1"; } from "./ee/routes/v1";
import { serviceTokenData as v3ServiceTokenDataRouter } from "./ee/routes/v3";
import { import {
auth as v1AuthRouter, auth as v1AuthRouter,
bot as v1BotRouter, bot as v1BotRouter,
@@ -38,6 +41,7 @@ import {
membership as v1MembershipRouter, membership as v1MembershipRouter,
organization as v1OrganizationRouter, organization as v1OrganizationRouter,
password as v1PasswordRouter, password as v1PasswordRouter,
sso as v1SSORouter,
secretImps as v1SecretImpsRouter, secretImps as v1SecretImpsRouter,
secret as v1SecretRouter, secret as v1SecretRouter,
secretsFolder as v1SecretsFolder, secretsFolder as v1SecretsFolder,
@@ -54,7 +58,6 @@ import {
organizations as v2OrganizationsRouter, organizations as v2OrganizationsRouter,
secret as v2SecretRouter, // begin to phase out secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter, secrets as v2SecretsRouter,
serviceAccounts as v2ServiceAccountsRouter,
serviceTokenData as v2ServiceTokenDataRouter, serviceTokenData as v2ServiceTokenDataRouter,
signup as v2SignupRouter, signup as v2SignupRouter,
tags as v2TagsRouter, tags as v2TagsRouter,
@@ -78,12 +81,15 @@ import {
getSecretScanningPrivateKey, getSecretScanningPrivateKey,
getSecretScanningWebhookProxy, getSecretScanningWebhookProxy,
getSecretScanningWebhookSecret, getSecretScanningWebhookSecret,
getSiteURL getSiteURL,
} from "./config"; } from "./config";
import { setup } from "./utils/setup"; import { setup } from "./utils/setup";
import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices"; import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices";
import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent"; import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent";
const SmeeClient = require("smee-client"); // eslint-disable-line const SmeeClient = require("smee-client"); // eslint-disable-line
import path from "path";
let handler: null | any = null;
const main = async () => { const main = async () => {
await setup(); await setup();
@@ -144,6 +150,27 @@ const main = async () => {
next(); next();
}); });
if ((await getNodeEnv()) === "production" && process.env.STANDALONE_BUILD === "true") {
const nextJsBuildPath = path.join(__dirname, "../frontend-build");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-var-requires
const conf = require("../frontend-build/.next/required-server-files.json").config;
const NextServer =
// eslint-disable-next-line @typescript-eslint/no-var-requires
require("../frontend-build/node_modules/next/dist/server/next-server").default;
const nextApp = new NextServer({
dev: false,
dir: nextJsBuildPath,
port: await getPort(),
conf,
hostname: "local",
customServer: false
});
handler = nextApp.getRequestHandler();
}
// (EE) routes // (EE) routes
app.use("/api/v1/secret", eeSecretRouter); app.use("/api/v1/secret", eeSecretRouter);
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter); app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
@@ -153,6 +180,7 @@ const main = async () => {
app.use("/api/v1/organizations", eeOrganizationsRouter); app.use("/api/v1/organizations", eeOrganizationsRouter);
app.use("/api/v1/sso", eeSSORouter); app.use("/api/v1/sso", eeSSORouter);
app.use("/api/v1/cloud-products", eeCloudProductsRouter); app.use("/api/v1/cloud-products", eeCloudProductsRouter);
app.use("/api/v3/service-token", v3ServiceTokenDataRouter);
// v1 routes // v1 routes
app.use("/api/v1/signup", v1SignupRouter); app.use("/api/v1/signup", v1SignupRouter);
@@ -176,6 +204,9 @@ const main = async () => {
app.use("/api/v1/webhooks", v1WebhooksRouter); app.use("/api/v1/webhooks", v1WebhooksRouter);
app.use("/api/v1/secret-imports", v1SecretImpsRouter); app.use("/api/v1/secret-imports", v1SecretImpsRouter);
app.use("/api/v1/roles", v1RoleRouter); app.use("/api/v1/roles", v1RoleRouter);
app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy);
app.use("/api/v1/sso", v1SSORouter);
app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequest);
// v2 routes (improvements) // v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter); app.use("/api/v2/signup", v2SignupRouter);
@@ -188,7 +219,7 @@ const main = async () => {
app.use("/api/v2/secret", v2SecretRouter); // deprecate app.use("/api/v2/secret", v2SecretRouter); // deprecate
app.use("/api/v2/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets app.use("/api/v2/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets
app.use("/api/v2/service-token", v2ServiceTokenDataRouter); app.use("/api/v2/service-token", v2ServiceTokenDataRouter);
app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new // app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
// v3 routes (experimental) // v3 routes (experimental)
app.use("/api/v3/auth", v3AuthRouter); app.use("/api/v3/auth", v3AuthRouter);
@@ -202,6 +233,12 @@ const main = async () => {
// server status // server status
app.use("/api", healthCheck); app.use("/api", healthCheck);
if (handler) {
app.all("*", (req, res) => {
return handler(req, res);
});
}
//* Handle unrouted requests and respond with proper error message as well as status code //* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next) => { app.use((req, res, next) => {
if (res.headersSent) return next(); if (res.headersSent) return next();
@@ -221,10 +258,24 @@ const main = async () => {
// await createTestUserForDevelopment(); // await createTestUserForDevelopment();
setUpHealthEndpoint(server); setUpHealthEndpoint(server);
server.on("close", async () => { const serverCleanup = async () => {
await DatabaseService.closeDatabase(); await DatabaseService.closeDatabase();
syncSecretsToThirdPartyServices.close(); syncSecretsToThirdPartyServices.close();
githubPushEventSecretScan.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; return server;

View File

@@ -423,6 +423,7 @@ const exchangeCodeGitlab = async ({
accessToken: res.access_token, accessToken: res.access_token,
refreshToken: res.refresh_token, refreshToken: res.refresh_token,
accessExpiresAt, accessExpiresAt,
url
}; };
}; };

View File

@@ -40,6 +40,8 @@ import {
INTEGRATION_NETLIFY_API_URL, INTEGRATION_NETLIFY_API_URL,
INTEGRATION_NORTHFLANK, INTEGRATION_NORTHFLANK,
INTEGRATION_NORTHFLANK_API_URL, INTEGRATION_NORTHFLANK_API_URL,
INTEGRATION_QOVERY,
INTEGRATION_QOVERY_API_URL,
INTEGRATION_RAILWAY, INTEGRATION_RAILWAY,
INTEGRATION_RAILWAY_API_URL, INTEGRATION_RAILWAY_API_URL,
INTEGRATION_RENDER, INTEGRATION_RENDER,
@@ -63,10 +65,10 @@ import sodium from "libsodium-wrappers";
import { standardRequest } from "../config/request"; import { standardRequest } from "../config/request";
const getSecretKeyValuePair = ( 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) => { Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
if (secrets[key]) prev[key] = secrets[key]?.value || ""; prev[key] = secrets?.[key] === null ? null : secrets?.[key]?.value;
return prev; return prev;
}, {}); }, {});
@@ -85,13 +87,15 @@ const syncSecrets = async ({
integrationAuth, integrationAuth,
secrets, secrets,
accessId, accessId,
accessToken accessToken,
appendices
}: { }: {
integration: IIntegration; integration: IIntegration;
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
secrets: Record<string, { value: string; comment?: string }>; secrets: Record<string, { value: string; comment?: string }>;
accessId: string | null; accessId: string | null;
accessToken: string; accessToken: string;
appendices?: { prefix: string, suffix: string };
}) => { }) => {
switch (integration.integration) { switch (integration.integration) {
case INTEGRATION_GCP_SECRET_MANAGER: case INTEGRATION_GCP_SECRET_MANAGER:
@@ -151,7 +155,8 @@ const syncSecrets = async ({
await syncSecretsGitHub({ await syncSecretsGitHub({
integration, integration,
secrets, secrets,
accessToken accessToken,
appendices
}); });
break; break;
case INTEGRATION_GITLAB: case INTEGRATION_GITLAB:
@@ -214,6 +219,14 @@ const syncSecrets = async ({
break; break;
case INTEGRATION_CHECKLY: case INTEGRATION_CHECKLY:
await syncSecretsCheckly({ await syncSecretsCheckly({
integration,
secrets,
accessToken,
appendices
});
break;
case INTEGRATION_QOVERY:
await syncSecretsQovery({
integration, integration,
secrets, secrets,
accessToken accessToken
@@ -339,16 +352,18 @@ const syncSecretsGCPSecretManager = async ({
...(pageToken ? { pageToken } : {}) ...(pageToken ? { pageToken } : {})
}); });
const res: GCPSMListSecretsRes = (await standardRequest.get( const res: GCPSMListSecretsRes = (
await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`, `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`,
{ {
params, params,
headers: { headers: {
"Authorization": `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json" "Accept-Encoding": "application/json"
} }
} }
)).data; )
).data;
if (res.secrets) { if (res.secrets) {
const filteredSecrets = res.secrets?.filter((gcpSecret) => { const filteredSecrets = res.secrets?.filter((gcpSecret) => {
@@ -357,7 +372,10 @@ const syncSecretsGCPSecretManager = async ({
let isValid = true; let isValid = true;
if (integration.metadata.secretPrefix && !key.startsWith(integration.metadata.secretPrefix)) { if (
integration.metadata.secretPrefix &&
!key.startsWith(integration.metadata.secretPrefix)
) {
isValid = false; isValid = false;
} }
@@ -378,20 +396,21 @@ const syncSecretsGCPSecretManager = async ({
pageToken = res.nextPageToken; pageToken = res.nextPageToken;
} }
const res: { [key: string]: string; } = {}; const res: { [key: string]: string } = {};
interface GCPLatestSecretVersionAccess { interface GCPLatestSecretVersionAccess {
name: string; name: string;
payload: { payload: {
data: string; data: string;
} };
} }
for await (const gcpSecret of gcpSecrets) { for await (const gcpSecret of gcpSecrets) {
const arr = gcpSecret.name.split("/"); const arr = gcpSecret.name.split("/");
const key = arr[arr.length - 1]; const key = arr[arr.length - 1];
const secretLatest: GCPLatestSecretVersionAccess = (await standardRequest.get( const secretLatest: GCPLatestSecretVersionAccess = (
await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`, `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
{ {
headers: { headers: {
@@ -399,8 +418,8 @@ const syncSecretsGCPSecretManager = async ({
"Accept-Encoding": "application/json" "Accept-Encoding": "application/json"
} }
} }
)).data; )
).data;
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8"); res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
} }
@@ -414,11 +433,14 @@ const syncSecretsGCPSecretManager = async ({
replication: { replication: {
automatic: {} automatic: {}
}, },
...(integration.metadata.secretGCPLabel ? { ...(integration.metadata.secretGCPLabel
? {
labels: { labels: {
[integration.metadata.secretGCPLabel.labelName]: integration.metadata.secretGCPLabel.labelValue [integration.metadata.secretGCPLabel.labelName]:
integration.metadata.secretGCPLabel.labelValue
} }
} : {}) }
: {})
}, },
{ {
params: { params: {
@@ -480,7 +502,7 @@ const syncSecretsGCPSecretManager = async ({
} }
} }
} }
} };
/** /**
* Sync/push [secrets] to Azure Key Vault with vault URI [integration.app] * Sync/push [secrets] to Azure Key Vault with vault URI [integration.app]
@@ -720,15 +742,12 @@ const syncSecretsAWSParameterStore = async ({
} = {}; } = {};
if (parameterList) { if (parameterList) {
awsParameterStoreSecretsObj = parameterList.reduce( awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => {
(obj: any, secret: any) => { return {
return ({
...obj, ...obj,
[secret.Name.substring(integration.path.length)]: secret [secret.Name.substring(integration.path.length)]: secret
}); };
}, }, {});
{}
);
} }
// Identify secrets to create // Identify secrets to create
@@ -941,7 +960,11 @@ const syncSecretsVercel = async ({
? { ? {
teamId: integrationAuth.teamId teamId: integrationAuth.teamId
} }
: {}) : {}),
...(integration?.path
? {
gitBranch: integration?.path
} : {})
}; };
const vercelSecrets: VercelSecret[] = ( const vercelSecrets: VercelSecret[] = (
@@ -960,7 +983,7 @@ const syncSecretsVercel = async ({
if ( if (
integration.targetEnvironment === "preview" && integration.targetEnvironment === "preview" &&
integration.path && secret.gitBranch &&
integration.path !== secret.gitBranch integration.path !== secret.gitBranch
) { ) {
// case: secret on preview environment does not have same target git branch // case: secret on preview environment does not have same target git branch
@@ -1323,11 +1346,13 @@ const syncSecretsNetlify = async ({
const syncSecretsGitHub = async ({ const syncSecretsGitHub = async ({
integration, integration,
secrets, secrets,
accessToken accessToken,
appendices
}: { }: {
integration: IIntegration; integration: IIntegration;
secrets: Record<string, { value: string; comment?: string }>; secrets: Record<string, { value: string; comment?: string }>;
accessToken: string; accessToken: string;
appendices?: { prefix: string, suffix: string };
}) => { }) => {
interface GitHubRepoKey { interface GitHubRepoKey {
key_id: string; key_id: string;
@@ -1357,7 +1382,7 @@ const syncSecretsGitHub = async ({
).data; ).data;
// Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
const encryptedSecrets: GitHubSecretRes = ( let encryptedSecrets: GitHubSecretRes = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", { await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
owner: integration.owner, owner: integration.owner,
repo: integration.app repo: integration.app
@@ -1370,6 +1395,15 @@ const syncSecretsGitHub = async ({
{} {}
); );
encryptedSecrets = Object.keys(encryptedSecrets).reduce((result: {
[key: string]: GitHubSecret;
}, key) => {
if ((appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)) {
result[key] = encryptedSecrets[key];
}
return result;
}, {});
Object.keys(encryptedSecrets).map(async (key) => { Object.keys(encryptedSecrets).map(async (key) => {
if (!(key in secrets)) { if (!(key in secrets)) {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
@@ -1857,7 +1891,9 @@ const syncSecretsGitLab = async ({
environment_scope: string; environment_scope: string;
} }
const gitLabApiUrl = integrationAuth.url ? `${integrationAuth.url}/api` : INTEGRATION_GITLAB_API_URL; const gitLabApiUrl = integrationAuth.url
? `${integrationAuth.url}/api`
: INTEGRATION_GITLAB_API_URL;
const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => { const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => {
const headers = { const headers = {
@@ -1867,7 +1903,9 @@ const syncSecretsGitLab = async ({
}; };
let allEnvVariables: GitLabSecret[] = []; let allEnvVariables: GitLabSecret[] = [];
let url: string | null = `${gitLabApiUrl}/v4/projects/${integrationAppId}/variables?per_page=100`; let url:
| string
| null = `${gitLabApiUrl}/v4/projects/${integrationAppId}/variables?per_page=100`;
while (url) { while (url) {
const response: any = await standardRequest.get(url, { headers }); const response: any = await standardRequest.get(url, { headers });
@@ -1888,17 +1926,21 @@ const syncSecretsGitLab = async ({
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken); const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables const getSecretsRes: GitLabSecret[] = allEnvVariables
.filter( .filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
(secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment
)
.filter((gitLabSecret) => { .filter((gitLabSecret) => {
let isValid = true; let isValid = true;
if (integration.metadata.secretPrefix && !gitLabSecret.key.startsWith(integration.metadata.secretPrefix)) { if (
integration.metadata.secretPrefix &&
!gitLabSecret.key.startsWith(integration.metadata.secretPrefix)
) {
isValid = false; isValid = false;
} }
if (integration.metadata.secretSuffix && !gitLabSecret.key.endsWith(integration.metadata.secretSuffix)) { if (
integration.metadata.secretSuffix &&
!gitLabSecret.key.endsWith(integration.metadata.secretSuffix)
) {
isValid = false; isValid = false;
} }
@@ -2047,13 +2089,15 @@ const syncSecretsSupabase = async ({
const syncSecretsCheckly = async ({ const syncSecretsCheckly = async ({
integration, integration,
secrets, secrets,
accessToken accessToken,
appendices
}: { }: {
integration: IIntegration; integration: IIntegration;
secrets: Record<string, { value: string; comment?: string }>; secrets: Record<string, { value: string; comment?: string }>;
accessToken: string; accessToken: string;
appendices?: { prefix: string, suffix: string };
}) => { }) => {
const getSecretsRes = ( let getSecretsRes = (
await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
@@ -2069,6 +2113,15 @@ const syncSecretsCheckly = async ({
{} {}
); );
getSecretsRes = Object.keys(getSecretsRes).reduce((result: {
[key: string]: string;
}, key) => {
if ((appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)) {
result[key] = getSecretsRes[key];
}
return result;
}, {});
// add secrets // add secrets
for await (const key of Object.keys(secrets)) { for await (const key of Object.keys(secrets)) {
if (!(key in getSecretsRes)) { if (!(key in getSecretsRes)) {
@@ -2126,6 +2179,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] * Sync/push [secrets] to Terraform Cloud project with id [integration.appId]
* @param {Object} obj * @param {Object} obj
@@ -2267,17 +2411,17 @@ const syncSecretsTeamCity = async ({
if (integration.targetEnvironment && integration.targetEnvironmentId) { if (integration.targetEnvironment && integration.targetEnvironmentId) {
// case: sync to specific build-config in TeamCity project // case: sync to specific build-config in TeamCity project
const res = (await standardRequest.get<GetTeamCityBuildConfigParametersRes>( const res = (
await standardRequest.get<GetTeamCityBuildConfigParametersRes>(
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`, `${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
Accept: "application/json", Accept: "application/json"
},
} }
)) }
.data )
.property ).data.property
.filter((parameter) => !parameter.inherited) .filter((parameter) => !parameter.inherited)
.reduce((obj: any, secret: TeamCitySecret) => { .reduce((obj: any, secret: TeamCitySecret) => {
const secretName = secret.name.replace(/^env\./, ""); const secretName = secret.name.replace(/^env\./, "");
@@ -2291,17 +2435,19 @@ const syncSecretsTeamCity = async ({
if (!(key in res) || (key in res && secrets[key].value !== res[key])) { if (!(key in res) || (key in res && secrets[key].value !== res[key])) {
// case: secret does not exist in TeamCity or secret value has changed // case: secret does not exist in TeamCity or secret value has changed
// -> create/update secret // -> create/update secret
await standardRequest.post(`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`, await standardRequest.post(
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{ {
name:`env.${key}`, name: `env.${key}`,
value: secrets[key].value value: secrets[key].value
}, },
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
Accept: "application/json", Accept: "application/json"
}, }
}); }
);
} }
} }

View File

@@ -1,10 +1,12 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import {
IServiceTokenData, IServiceTokenData,
IServiceTokenDataV3,
IUser, IUser,
} from "../../models"; } from "../../models";
import { import {
ServiceActor, ServiceActor,
ServiceActorV3,
UserActor, UserActor,
UserAgentType UserAgentType
} from "../../ee/models"; } from "../../ee/models";
@@ -21,6 +23,11 @@ export interface UserAuthData extends BaseAuthData {
authPayload: IUser; authPayload: IUser;
} }
export interface ServiceTokenV3AuthData extends BaseAuthData {
actor: ServiceActorV3;
authPayload: IServiceTokenDataV3;
}
export interface ServiceTokenAuthData extends BaseAuthData { export interface ServiceTokenAuthData extends BaseAuthData {
actor: ServiceActor; actor: ServiceActor;
authPayload: IServiceTokenData; authPayload: IServiceTokenData;
@@ -28,4 +35,5 @@ export interface ServiceTokenAuthData extends BaseAuthData {
export type AuthData = export type AuthData =
| UserAuthData | UserAuthData
| ServiceTokenV3AuthData
| ServiceTokenAuthData; | ServiceTokenAuthData;

View File

@@ -16,10 +16,11 @@ export interface CreateSecretParams {
secretCommentCiphertext?: string; secretCommentCiphertext?: string;
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
skipMultilineEncoding?: boolean;
secretPath: string; secretPath: string;
metadata?: { metadata?: {
source?: string; source?: string;
} };
} }
export interface GetSecretsParams { export interface GetSecretsParams {
@@ -37,10 +38,16 @@ export interface GetSecretParams {
environment: string; environment: string;
type?: "shared" | "personal"; type?: "shared" | "personal";
authData: AuthData; authData: AuthData;
include_imports?: boolean;
} }
export interface UpdateSecretParams { export interface UpdateSecretParams {
secretName: string; secretName: string;
newSecretName?: string;
secretId?: string;
secretKeyCiphertext?: string;
secretKeyIV?: string;
secretKeyTag?: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
type: "shared" | "personal"; type: "shared" | "personal";
@@ -49,13 +56,73 @@ export interface UpdateSecretParams {
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretPath: string; secretPath: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
tags?: string[];
} }
export interface DeleteSecretParams { export interface DeleteSecretParams {
secretName: string; secretName: string;
secretId?: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
type: "shared" | "personal"; type: "shared" | "personal";
authData: AuthData; authData: AuthData;
secretPath: string; 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

@@ -3,8 +3,8 @@ import { ErrorRequestHandler } from "express";
import { TokenExpiredError } from "jsonwebtoken"; import { TokenExpiredError } from "jsonwebtoken";
import { InternalServerError, UnauthorizedRequestError } from "../utils/errors"; import { InternalServerError, UnauthorizedRequestError } from "../utils/errors";
import { getLogger } from "../utils/logger"; import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError"; import RequestError from "../utils/requestError";
import { getNodeEnv } from "../config"; import { ForbiddenError } from "@casl/ability";
export const requestErrorHandler: ErrorRequestHandler = async ( export const requestErrorHandler: ErrorRequestHandler = async (
error: RequestError | Error, error: RequestError | Error,
@@ -14,41 +14,37 @@ export const requestErrorHandler: ErrorRequestHandler = async (
) => { ) => {
if (res.headersSent) return next(); if (res.headersSent) return next();
if (await getNodeEnv() !== "production") { const logAndCaptureException = async (error: RequestError) => {
/* eslint-disable no-console */
console.error(error);
}
//TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError
if (error instanceof TokenExpiredError) {
error = UnauthorizedRequestError({ stack: error.stack, message: "Token expired" });
} else if (!(error instanceof RequestError)) {
error = InternalServerError({
context: { exception: error.message },
stack: error.stack,
});
(await getLogger("backend-main")).log( (await getLogger("backend-main")).log(
(<RequestError>error).levelName.toLowerCase(), (<RequestError>error).levelName.toLowerCase(),
(<RequestError>error).message `${error.stack}\n${error.message}`
); );
}
//* Set Sentry user identification if req.user is populated //* Set Sentry user identification if req.user is populated
if (req.user !== undefined && req.user !== null) { if (req.user !== undefined && req.user !== null) {
Sentry.setUser({ email: (req.user as any).email }); Sentry.setUser({ email: (req.user as any).email });
} }
//* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL'
//* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on
if (
[LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes(
(<RequestError>error).level
)
) {
Sentry.captureException(error); Sentry.captureException(error);
};
if (error instanceof RequestError) {
if (error instanceof TokenExpiredError) {
error = UnauthorizedRequestError({ stack: error.stack, message: "Token expired" });
}
await logAndCaptureException((<RequestError>error));
} else {
if (error instanceof ForbiddenError) {
error = UnauthorizedRequestError({ context: { exception: error.message }, stack: error.stack })
} else {
error = InternalServerError({ context: { exception: error.message }, stack: error.stack });
} }
res await logAndCaptureException((<RequestError>error));
.status((<RequestError>error).statusCode) }
.json((<RequestError>error).format(req));
delete (<any>error).stacktrace // remove stack trace from being sent to client
res.status((<RequestError>error).statusCode).json(error);
next(); next();
}; };

View File

@@ -3,6 +3,7 @@ import { NextFunction, Request, Response } from "express";
import { import {
getAuthAPIKeyPayload, getAuthAPIKeyPayload,
getAuthSTDPayload, getAuthSTDPayload,
getAuthSTDV3Payload,
getAuthUserPayload, getAuthUserPayload,
validateAuthMode, validateAuthMode,
} from "../helpers/auth"; } from "../helpers/auth";
@@ -49,6 +50,12 @@ const requireAuth = ({
}); });
req.serviceTokenData = authData.authPayload; req.serviceTokenData = authData.authPayload;
break; break;
case AuthMode.SERVICE_TOKEN_V3:
authData = await getAuthSTDV3Payload({
req,
authTokenValue
});
break;
case AuthMode.API_KEY: case AuthMode.API_KEY:
authData = await getAuthAPIKeyPayload({ authData = await getAuthAPIKeyPayload({
req, req,
@@ -61,9 +68,7 @@ const requireAuth = ({
req, req,
authTokenValue authTokenValue
}); });
// authPayload = authUserPayload.user;
req.user = authData.authPayload; req.user = authData.authPayload;
// req.tokenVersionId = authUserPayload.tokenVersionId; // TODO
break; break;
} }

View File

@@ -2,7 +2,8 @@ import jwt from "jsonwebtoken";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { User } from "../models"; import { User } from "../models";
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
import { getJwtMfaSecret } from "../config"; import { getAuthSecret } from "../config";
import { AuthTokenType } from "../variables";
declare module "jsonwebtoken" { declare module "jsonwebtoken" {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -26,9 +27,11 @@ const requireMfaAuth = async (
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: "Missing Authorization Body in the request header"})) if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: "Missing Authorization Body in the request header"}))
const decodedToken = <jwt.UserIDJwtPayload>( const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, await getJwtMfaSecret()) jwt.verify(AUTH_TOKEN_VALUE, await getAuthSecret())
); );
if (decodedToken.authTokenType !== AuthTokenType.MFA_TOKEN) throw UnauthorizedRequestError();
const user = await User.findOne({ const user = await User.findOne({
_id: decodedToken.userId, _id: decodedToken.userId,
}).select("+publicKey"); }).select("+publicKey");

View File

@@ -2,7 +2,8 @@ import jwt from "jsonwebtoken";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { User } from "../models"; import { User } from "../models";
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
import { getJwtSignupSecret } from "../config"; import { getAuthSecret } from "../config";
import { AuthTokenType } from "../variables";
declare module "jsonwebtoken" { declare module "jsonwebtoken" {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -27,9 +28,11 @@ const requireSignupAuth = async (
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: "Missing Authorization Body in the request header"})) if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: "Missing Authorization Body in the request header"}))
const decodedToken = <jwt.UserIDJwtPayload>( const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret()) jwt.verify(AUTH_TOKEN_VALUE, await getAuthSecret())
); );
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw UnauthorizedRequestError();
const user = await User.findOne({ const user = await User.findOne({
_id: decodedToken.userId, _id: decodedToken.userId,
}).select("+publicKey"); }).select("+publicKey");

View File

@@ -14,17 +14,19 @@ export * from "./tag";
export * from "./folder"; export * from "./folder";
export * from "./secretImports"; export * from "./secretImports";
export * from "./secretBlindIndexData"; export * from "./secretBlindIndexData";
export * from "./serviceToken"; export * from "./serviceToken"; // TODO: deprecate
export * from "./serviceAccount"; export * from "./serviceAccount"; // TODO: deprecate
export * from "./serviceAccountKey"; export * from "./serviceAccountKey"; // TODO: deprecate
export * from "./serviceAccountOrganizationPermission"; export * from "./serviceAccountOrganizationPermission"; // TODO: deprecate
export * from "./serviceAccountWorkspacePermission"; export * from "./serviceAccountWorkspacePermission"; // TODO: deprecate
export * from "./tokenData"; export * from "./tokenData";
export * from "./user"; export * from "./user";
export * from "./userAction"; export * from "./userAction";
export * from "./workspace"; export * from "./workspace";
export * from "./serviceTokenData"; export * from "./serviceTokenData"; // TODO: deprecate
export * from "./apiKeyData"; export * from "./apiKeyData";
export * from "./loginSRPDetail"; export * from "./loginSRPDetail";
export * from "./tokenVersion"; export * from "./tokenVersion";
export * from "./webhooks"; export * from "./webhooks";
export * from "./serviceTokenDataV3";
export * from "./serviceTokenDataV3Key";

View File

@@ -18,6 +18,7 @@ import {
INTEGRATION_LARAVELFORGE, INTEGRATION_LARAVELFORGE,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_NORTHFLANK, INTEGRATION_NORTHFLANK,
INTEGRATION_QOVERY,
INTEGRATION_RAILWAY, INTEGRATION_RAILWAY,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
@@ -45,6 +46,7 @@ export interface IIntegration {
targetServiceId: string; targetServiceId: string;
path: string; path: string;
region: string; region: string;
scope: string;
secretPath: string; secretPath: string;
integration: integration:
| "azure-key-vault" | "azure-key-vault"
@@ -63,6 +65,7 @@ export interface IIntegration {
| "travisci" | "travisci"
| "supabase" | "supabase"
| "checkly" | "checkly"
| "qovery"
| "terraform-cloud" | "terraform-cloud"
| "teamcity" | "teamcity"
| "hashicorp-vault" | "hashicorp-vault"
@@ -119,11 +122,13 @@ const integrationSchema = new Schema<IIntegration>(
}, },
targetService: { targetService: {
// railway-specific service // railway-specific service
// qovery-specific project
type: String, type: String,
default: null, default: null,
}, },
targetServiceId: { targetServiceId: {
// railway-specific service // railway-specific service
// qovery specific project
type: String, type: String,
default: null, default: null,
}, },
@@ -143,6 +148,11 @@ const integrationSchema = new Schema<IIntegration>(
type: String, type: String,
default: null, default: null,
}, },
scope: {
// qovery-specific scope
type: String,
default: null
},
integration: { integration: {
type: String, type: String,
enum: [ enum: [
@@ -162,6 +172,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_QOVERY,
INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TEAMCITY, INTEGRATION_TEAMCITY,
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,

View File

@@ -1,6 +1,3 @@
// TODO: in the future separate metadata
// into distinct types by integration
export type Metadata = { export type Metadata = {
secretPrefix?: string; secretPrefix?: string;
secretSuffix?: string; secretSuffix?: string;

View File

@@ -52,6 +52,7 @@ import {
| "aws-parameter-store" | "aws-parameter-store"
| "aws-secret-manager" | "aws-secret-manager"
| "checkly" | "checkly"
| "qovery"
| "cloudflare-pages" | "cloudflare-pages"
| "codefresh" | "codefresh"
| "digital-ocean-app-platform" | "digital-ocean-app-platform"

View File

@@ -4,7 +4,7 @@ import {
ENCODING_SCHEME_BASE64, ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED
} from "../variables"; } from "../variables";
export interface ISecret { export interface ISecret {
@@ -12,7 +12,7 @@ export interface ISecret {
version: number; version: number;
workspace: Types.ObjectId; workspace: Types.ObjectId;
type: string; type: string;
user: Types.ObjectId; user?: Types.ObjectId;
environment: string; environment: string;
secretBlindIndex?: string; secretBlindIndex?: string;
secretKeyCiphertext: string; secretKeyCiphertext: string;
@@ -27,13 +27,14 @@ export interface ISecret {
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
secretCommentHash?: string; secretCommentHash?: string;
skipMultilineEncoding?: boolean;
algorithm: "aes-256-gcm"; algorithm: "aes-256-gcm";
keyEncoding: "utf8" | "base64"; keyEncoding: "utf8" | "base64";
tags?: string[]; tags?: string[];
folder?: string; folder?: string;
metadata?: { metadata?: {
[key: string]: string; [key: string]: string;
} };
} }
const secretSchema = new Schema<ISecret>( const secretSchema = new Schema<ISecret>(
@@ -41,105 +42,109 @@ const secretSchema = new Schema<ISecret>(
version: { version: {
type: Number, type: Number,
required: true, required: true,
default: 1, default: 1
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "Workspace", ref: "Workspace",
required: true, required: true
}, },
type: { type: {
type: String, type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL], enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true, required: true
}, },
user: { user: {
// user associated with the personal secret // user associated with the personal secret
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "User", ref: "User"
}, },
tags: { tags: {
ref: "Tag", ref: "Tag",
type: [Schema.Types.ObjectId], type: [Schema.Types.ObjectId],
default: [], default: []
}, },
environment: { environment: {
type: String, type: String,
required: true, required: true
}, },
secretBlindIndex: { secretBlindIndex: {
type: String, type: String,
select: false, select: false
}, },
secretKeyCiphertext: { secretKeyCiphertext: {
type: String, type: String,
required: true, required: true
}, },
secretKeyIV: { secretKeyIV: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretKeyTag: { secretKeyTag: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretKeyHash: { secretKeyHash: {
type: String, type: String
}, },
secretValueCiphertext: { secretValueCiphertext: {
type: String, type: String,
required: true, required: true
}, },
secretValueIV: { secretValueIV: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretValueTag: { secretValueTag: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretValueHash: { secretValueHash: {
type: String, type: String
}, },
secretCommentCiphertext: { secretCommentCiphertext: {
type: String, type: String,
required: false, required: false
}, },
secretCommentIV: { secretCommentIV: {
type: String, // symmetric type: String, // symmetric
required: false, required: false
}, },
secretCommentTag: { secretCommentTag: {
type: String, // symmetric type: String, // symmetric
required: false, required: false
}, },
secretCommentHash: { secretCommentHash: {
type: String, type: String,
required: false, required: false
},
skipMultilineEncoding: {
type: Boolean,
required: false
}, },
algorithm: { algorithm: {
// the encryption algorithm used // the encryption algorithm used
type: String, type: String,
enum: [ALGORITHM_AES_256_GCM], enum: [ALGORITHM_AES_256_GCM],
required: true, required: true,
default: ALGORITHM_AES_256_GCM, default: ALGORITHM_AES_256_GCM
}, },
keyEncoding: { keyEncoding: {
type: String, type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true, required: true,
default: ENCODING_SCHEME_UTF8, default: ENCODING_SCHEME_UTF8
}, },
folder: { folder: {
type: String, type: String,
default: "root", default: "root"
}, },
metadata: { metadata: {
type: Schema.Types.Mixed type: Schema.Types.Mixed
} }
}, },
{ {
timestamps: true, timestamps: true
} }
); );

View File

@@ -1,81 +0,0 @@
import mongoose, { Schema, model } from "mongoose";
import { ISecret, Secret } from "./secret";
interface ISecretApprovalRequest {
secret: mongoose.Types.ObjectId;
requestedChanges: ISecret;
requestedBy: mongoose.Types.ObjectId;
approvers: IApprover[];
status: ApprovalStatus;
timestamp: Date;
requestType: RequestType;
requestId: string;
}
interface IApprover {
userId: mongoose.Types.ObjectId;
status: ApprovalStatus;
}
export enum ApprovalStatus {
PENDING = "pending",
APPROVED = "approved",
REJECTED = "rejected"
}
export enum RequestType {
UPDATE = "update",
DELETE = "delete",
CREATE = "create"
}
const approverSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
status: {
type: String,
enum: [ApprovalStatus],
default: ApprovalStatus.PENDING,
},
});
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
{
secret: {
type: mongoose.Schema.Types.ObjectId,
ref: "Secret",
},
requestedChanges: Secret,
requestedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
approvers: [approverSchema],
status: {
type: String,
enum: ApprovalStatus,
default: ApprovalStatus.PENDING,
},
timestamp: {
type: Date,
default: Date.now,
},
requestType: {
type: String,
enum: RequestType,
required: true,
},
requestId: {
type: String,
required: false,
},
},
{
timestamps: true,
}
);
export const SecretApprovalRequest = model<ISecretApprovalRequest>("SecretApprovalRequest", secretApprovalRequestSchema);

View File

@@ -1,3 +1,4 @@
// TODO: deprecate
import { Schema, Types, model } from "mongoose"; import { Schema, Types, model } from "mongoose";
export interface IServiceToken { export interface IServiceToken {
_id: Types.ObjectId; _id: Types.ObjectId;

View File

@@ -1,3 +1,4 @@
// TODO: deprecate
import { Document, Schema, Types, model } from "mongoose"; import { Document, Schema, Types, model } from "mongoose";
export interface IServiceTokenData extends Document { export interface IServiceTokenData extends Document {

View File

@@ -0,0 +1,129 @@
import { Document, Schema, Types, model } from "mongoose";
import { IPType } from "../ee/models";
export enum Permission {
READ = "read",
WRITE = "write"
}
export interface IServiceTokenV3Scope {
environment: string;
secretPath: string;
permissions: Permission[];
}
export interface IServiceTokenV3TrustedIp {
ipAddress: string;
type: IPType;
prefix: number;
}
export interface IServiceTokenDataV3 extends Document {
_id: Types.ObjectId;
name: string;
workspace: Types.ObjectId;
user: Types.ObjectId;
publicKey: string;
isActive: boolean;
lastUsed?: Date;
usageCount: number;
expiresAt?: Date;
scopes: Array<IServiceTokenV3Scope>;
trustedIps: Array<IServiceTokenV3TrustedIp>;
}
const serviceTokenDataV3Schema = new Schema(
{
name: {
type: String,
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: "User",
required: true
},
publicKey: {
type: String,
required: true
},
isActive: {
type: Boolean,
required: true
},
lastUsed: {
type: Date,
required: false
},
usageCount: {
type: Number,
default: 0,
required: true
},
expiresAt: {
type: Date,
required: false,
expires: 0
},
scopes: {
type: [
{
environment: {
type: String,
required: true
},
secretPath: {
type: String,
default: "/",
required: true
},
permissions: {
type: [String],
enum: [Permission.READ, Permission.WRITE],
default: [Permission.READ],
required: true
}
}
],
required: true
},
trustedIps: {
type: [
{
ipAddress: {
type: String,
required: true
},
type: {
type: String,
enum: [
IPType.IPV4,
IPType.IPV6
],
required: true
},
prefix: {
type: Number,
required: false
}
}
],
default: [{
ipAddress: "0.0.0.0",
type: IPType.IPV4.toString(),
prefix: 0
}],
required: true
}
},
{
timestamps: true
}
);
export const ServiceTokenDataV3 = model<IServiceTokenDataV3>("ServiceTokenDataV3", serviceTokenDataV3Schema);

View File

@@ -0,0 +1,43 @@
import { Document, Schema, Types, model } from "mongoose";
export interface IServiceTokenDataV3Key extends Document {
_id: Types.ObjectId;
encryptedKey: string;
nonce: string;
sender: Types.ObjectId;
serviceTokenData: Types.ObjectId;
workspace: Types.ObjectId;
}
const serviceTokenDataV3KeySchema = new Schema(
{
encryptedKey: {
type: String,
required: true
},
nonce: {
type: String,
required: true
},
sender: {
type: Schema.Types.ObjectId,
ref: "User",
required: true
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: "ServiceTokenDataV3",
required: true,
},
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
}
},
{
timestamps: true
}
);
export const ServiceTokenDataV3Key = model<IServiceTokenDataV3Key>("ServiceTokenDataV3Key", serviceTokenDataV3KeySchema);

View File

@@ -4,6 +4,7 @@ export enum AuthMethod {
EMAIL = "email", EMAIL = "email",
GOOGLE = "google", GOOGLE = "google",
GITHUB = "github", GITHUB = "github",
GITLAB = "gitlab",
OKTA_SAML = "okta-saml", OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml", AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml", JUMPCLOUD_SAML = "jumpcloud-saml",

View File

@@ -60,13 +60,14 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => {
integrationAuth, integrationAuth,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
accessId: access.accessId === undefined ? null : access.accessId, accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken accessToken: access.accessToken,
appendices: { prefix: integration.metadata?.secretPrefix || "", suffix: integration.metadata?.secretSuffix || "" }
}); });
} }
}) })
syncSecretsToThirdPartyServices.on("error", (error) => { syncSecretsToThirdPartyServices.on("error", (error) => {
console.log("QUEUE ERROR:", error) // eslint-disable-line // console.log("QUEUE ERROR:", error) // eslint-disable-line
}) })
export const syncSecretsToActiveIntegrationsQueue = (jobDetails: TSyncSecretsToThirdPartyServices) => { export const syncSecretsToActiveIntegrationsQueue = (jobDetails: TSyncSecretsToThirdPartyServices) => {

View File

@@ -2,7 +2,7 @@ import Queue, { Job } from "bull";
import { ProbotOctokit } from "probot" import { ProbotOctokit } from "probot"
import TelemetryService from "../../services/TelemetryService"; import TelemetryService from "../../services/TelemetryService";
import { sendMail } from "../../helpers"; import { sendMail } from "../../helpers";
import GitRisks from "../../ee/models/gitRisks"; import { GitRisks } from "../../ee/models";
import { MembershipOrg, User } from "../../models"; import { MembershipOrg, User } from "../../models";
import { ADMIN } from "../../variables"; import { ADMIN } from "../../variables";
import { convertKeysToLowercase, scanFullRepoContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper"; import { convertKeysToLowercase, scanFullRepoContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper";
@@ -13,7 +13,7 @@ export const githubFullRepositorySecretScan = new Queue("github-full-repository-
type TScanPushEventQueueDetails = { type TScanPushEventQueueDetails = {
organizationId: string, organizationId: string,
installationId: number, installationId: string,
repository: { repository: {
id: number, id: number,
fullName: string, fullName: string,
@@ -30,7 +30,8 @@ githubFullRepositorySecretScan.process(async (job: Job, done: Queue.DoneCallback
installationId: installationId installationId: installationId
}, },
}); });
const findings: SecretMatch[] = await scanFullRepoContentAndGetFindings(octokit, installationId, repository.fullName)
const findings: SecretMatch[] = await scanFullRepoContentAndGetFindings(octokit, installationId as any, repository.fullName)
for (const finding of findings) { for (const finding of findings) {
await GitRisks.findOneAndUpdate({ fingerprint: finding.Fingerprint }, await GitRisks.findOneAndUpdate({ fingerprint: finding.Fingerprint },
{ {

View File

@@ -3,7 +3,7 @@ import { ProbotOctokit } from "probot"
import { Commit } from "@octokit/webhooks-types"; import { Commit } from "@octokit/webhooks-types";
import TelemetryService from "../../services/TelemetryService"; import TelemetryService from "../../services/TelemetryService";
import { sendMail } from "../../helpers"; import { sendMail } from "../../helpers";
import GitRisks from "../../ee/models/gitRisks"; import { GitRisks } from "../../ee/models";
import { MembershipOrg, User } from "../../models"; import { MembershipOrg, User } from "../../models";
import { ADMIN } from "../../variables"; import { ADMIN } from "../../variables";
import { convertKeysToLowercase, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper"; import { convertKeysToLowercase, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper";

View File

@@ -11,6 +11,7 @@ import key from "./key";
import inviteOrg from "./inviteOrg"; import inviteOrg from "./inviteOrg";
import secret from "./secret"; import secret from "./secret";
import serviceToken from "./serviceToken"; import serviceToken from "./serviceToken";
import sso from "./sso";
import password from "./password"; import password from "./password";
import integration from "./integration"; import integration from "./integration";
import integrationAuth from "./integrationAuth"; import integrationAuth from "./integrationAuth";
@@ -37,5 +38,6 @@ export {
integrationAuth, integrationAuth,
secretsFolder, secretsFolder,
webhooks, webhooks,
secretImps secretImps,
sso
}; };

View File

@@ -60,6 +60,54 @@ router.get(
integrationAuthController.getIntegrationAuthVercelBranches integrationAuthController.getIntegrationAuthVercelBranches
); );
router.get(
"/:integrationAuthId/qovery/orgs",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
integrationAuthController.getIntegrationAuthQoveryOrgs
);
router.get(
"/:integrationAuthId/qovery/projects",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
integrationAuthController.getIntegrationAuthQoveryProjects
);
router.get(
"/:integrationAuthId/qovery/environments",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
integrationAuthController.getIntegrationAuthQoveryEnvironments
);
router.get(
"/:integrationAuthId/qovery/apps",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
integrationAuthController.getIntegrationAuthQoveryApps
);
router.get(
"/:integrationAuthId/qovery/containers",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
integrationAuthController.getIntegrationAuthQoveryContainers
);
router.get(
"/:integrationAuthId/qovery/jobs",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
integrationAuthController.getIntegrationAuthQoveryJobs
);
router.get( router.get(
"/:integrationAuthId/railway/environments", "/:integrationAuthId/railway/environments",
requireAuth({ requireAuth({

View File

@@ -13,15 +13,6 @@ router.get(
organizationController.getOrganizations organizationController.getOrganizations
); );
router.post(
// not used on frontend
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationController.createOrganization
);
router.get( router.get(
"/:organizationId", "/:organizationId",
requireAuth({ requireAuth({

View File

@@ -7,7 +7,7 @@ import { AuthMode } from "../../variables";
router.post( router.post(
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
}), }),
secretImpsController.createSecretImp secretImpsController.createSecretImp
); );
@@ -15,7 +15,7 @@ router.post(
router.put( router.put(
"/:id", "/:id",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
}), }),
secretImpsController.updateSecretImport secretImpsController.updateSecretImport
); );
@@ -23,7 +23,7 @@ router.put(
router.delete( router.delete(
"/:id", "/:id",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
}), }),
secretImpsController.deleteSecretImport secretImpsController.deleteSecretImport
); );
@@ -31,7 +31,7 @@ router.delete(
router.get( router.get(
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
}), }),
secretImpsController.getSecretImports secretImpsController.getSecretImports
); );
@@ -39,7 +39,7 @@ router.get(
router.get( router.get(
"/secrets", "/secrets",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
}), }),
secretImpsController.getAllSecretsFromImport secretImpsController.getAllSecretsFromImport
); );

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