1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-21 17:02:49 +00:00

Compare commits

..

204 Commits

Author SHA1 Message Date
7487b373fe adding cypress test 2023-10-24 10:38:08 -04:00
9a500504a4 adding cypress tests 2023-10-21 09:49:31 -07:00
1510c39631 Update Chart.yaml 2023-10-19 19:19:46 +01:00
d0579b383f update readiness probe 2023-10-19 19:19:32 +01:00
b4f4c1064d Update values.yaml 2023-10-19 19:10:22 +01:00
d72d3940e6 update img name in prod img gha 2023-10-19 17:45:29 +01:00
7217bcb3d8 Merge pull request from Infisical/migrate-to-standalone-infisical
Migrate to standalone infisical
2023-10-19 16:07:55 +01:00
2faa9222d8 update gamma gh action 2023-10-19 16:05:41 +01:00
589f0bc134 update staging test for img 2023-10-19 13:23:07 +01:00
bd6dc3d4c0 update gamma helm values 2023-10-19 13:10:36 +01:00
9338babda6 update infisical helm chart read me 2023-10-19 13:03:18 +01:00
6f9e8644d7 update infisical helm chart to use standalone img 2023-10-19 12:58:16 +01:00
2fdb10277e update docs to use standalone infisical 2023-10-19 12:53:20 +01:00
15d2c536ed update prod docker compose 2023-10-19 12:23:20 +01:00
a304228961 Merge pull request 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
c865b78b41 feat: added api key support for folder and secret import 2023-10-18 16:38:19 +05:30
be80ac999e Merge pull request from Infisical/fix-azure-saml-flow
Patch Azure SAML Flow
2023-10-18 11:56:45 +01:00
076fe58325 Fix azure-saml flow 2023-10-18 11:49:57 +01:00
66bfab1994 update platform version env 2023-10-18 10:41:15 +01:00
b92c50addd properly add pre baked values 2023-10-18 10:40:34 +01:00
8fbca05052 remove extract_version 2023-10-17 23:13:04 +01:00
d99830067e bake posthog key in single docker img 2023-10-17 23:08:43 +01:00
cdc8ef95ab add platform version to UI 2023-10-17 23:08:43 +01:00
072e97b956 Fix azure samlConfig 2023-10-17 16:51:46 +01:00
4f26a7cad3 Revert audience change for azure saml 2023-10-17 15:50:31 +01:00
7bb6ff3d0c Merge branch 'main' of https://github.com/Infisical/infisical 2023-10-17 15:32:42 +01:00
ecccec8e35 Attempt fix azure samlConfig 2023-10-17 15:32:34 +01:00
7fd15a06e5 conditionally build standalone infisical 2023-10-17 14:32:56 +01:00
5d4a37004e Merge pull request from G3root/serve-frontend-from-backend
feat: serve frontend from backend
2023-10-17 14:15:41 +01:00
aa61fd091d remove redundant file from docker ignore 2023-10-17 14:11:16 +01:00
04ac54bcfa run cmd as non root user and update port to non privileged 2023-10-17 14:08:22 +01:00
38dbf1e738 Add missing / to samlConfig callbackURL 2023-10-17 14:03:37 +01:00
ddf9d7848c Update protocol in samlConfig 2023-10-17 12:57:05 +01:00
0b40de49ec remove redis error logs 2023-10-17 12:24:54 +01:00
b1d16cab39 remove promise.all from closeDatabaseHelper 2023-10-17 12:24:18 +01:00
fb7c7045e9 set telemetry post frontend build in standalone docker img 2023-10-17 12:22:08 +01:00
d570828e47 Update path and callbackURL for samlConfig 2023-10-17 12:13:54 +01:00
2a92b6c787 Merge pull request from akhilmhdh/feat/secret-update-id
feat: changed secret update to use id
2023-10-17 11:14:30 +01:00
ee54fdabe1 feat: changed secret update to use id 2023-10-17 15:38:30 +05:30
136308f299 revert: temp workaround: remove use of get static prop 2023-10-16 20:37:35 +05:30
ba41244877 chore: remove next.js 2023-10-16 20:35:47 +05:30
c4dcf334f0 fix: remove copy config 2023-10-16 20:35:29 +05:30
66bac3ef48 fix: static props not rendered 2023-10-16 20:35:16 +05:30
e5347719c3 temp workaround: remove use of get static prop 2023-10-16 15:03:55 +01:00
275416a08f Remove 1-on-1 pairing link from README 2023-10-16 10:23:24 +01:00
abe1f54aab remove nginx and pm2 2023-10-15 23:58:04 +01:00
13c1e2b349 fix: docker file 2023-10-15 12:09:45 +05:30
f5a9afec61 fix: pm2 config 2023-10-15 11:52:55 +05:30
d0a85c98b2 chore: add docker ignore and git ignore 2023-10-15 11:47:35 +05:30
e0669cae7c chore: update path 2023-10-15 11:47:06 +05:30
2c011b7d53 feat: add custom server 2023-10-14 13:20:27 +05:30
1b24a9b6e9 chore: add next to gitignore 2023-10-14 13:20:14 +05:30
00c173aead chore: add next for backend 2023-10-14 13:19:54 +05:30
2e15ad0625 fix: type 2023-10-14 12:54:26 +05:30
3f0b6dc6c1 Merge pull request 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
f766a1eb29 Merge pull request from akhilmhdh/feat/secret-bug
feat: added support for recursive file creation
2023-10-13 15:57:50 +01:00
543c55b5a6 Add FAQ to self-hosted SSO docs for it not working due to misconfiguration 2023-10-13 15:56:10 +01:00
cdb1d38f99 Merge pull request 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
0a53b72cce Patch delete user, org, project session impl and account for orgId in localStorage 2023-10-13 11:22:40 +01:00
b921c376b2 Merge pull request from ragnarbull/improve-logging
Improve-logging
2023-10-12 10:08:39 -04:00
b1ec59eb67 polish error handling 2023-10-12 15:06:37 +01:00
4e6e12932a Merge pull request from Tchoupinax/main
feat(helm-chart): allow to provide affinity values for the pods
2023-10-12 06:03:56 -04:00
792c382743 update chart version 2023-10-12 11:03:21 +01:00
f5c8e537c9 generate documentation 2023-10-12 11:01:21 +01:00
4bf09a8efc Merge pull request from Salman2301/patch-1
docs: fix missing export format `yaml`
2023-10-12 05:02:06 -04:00
001265cf2a feat(helm-chart): allow to provide affinity values for the pods 2023-10-11 21:15:21 +02:00
a56a135396 Merge pull request from Infisical/delete-org
Delete user, organization, project capabilities feature/update
2023-10-11 19:26:19 +01:00
9838c29867 docs: fix missing export format yaml 2023-10-11 18:48:56 +05:30
4f5946b252 feat: added support for recursive file creation 2023-10-11 17:19:34 +05:30
dc23517133 Merge remote-tracking branch 'origin' into delete-org 2023-10-10 14:54:19 +01:00
5e4d4f56e3 Merge pull request from Infisical/github-checkly-suffixes
allow multiple simultaneous integrations with checkly and github
2023-10-10 14:48:30 +01:00
a855a2cee6 Merge pull request from Infisical/ui-updates
fix styling issues
2023-10-10 13:55:40 +01:00
e86258949c Refactor learning note rendering to use react-query 2023-10-10 12:18:59 +01:00
f119c921d0 remove comment 2023-10-09 13:38:21 +00:00
b6ef55783e Fix: add stack trace errors in logging for prod 2023-10-09 13:37:16 +00:00
feade5d029 Clear react-query cache upon user logout, delete account 2023-10-09 09:00:31 +01:00
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
0eb7896b59 Merge remote-tracking branch 'origin' into delete-org 2023-10-09 07:47:41 +01:00
9fcecc9c92 Finish preliminary delete user, organization, refactor delete project, create org page 2023-10-09 07:47:20 +01:00
ee6afa8983 allowed creating multiple github integrations to different apps at once 2023-10-08 17:05:26 -07:00
6f4ac02558 fix ui for env overview empty screen 2023-10-08 14:02:22 -07:00
5971480ca9 allow multiple simultaneous integrations with checkly and github 2023-10-08 13:53:54 -07:00
d222b09ba5 changed scroll to auto 2023-10-07 20:25:04 -07:00
a9fd0374bd fixed signup screen scroll 2023-10-07 20:22:49 -07:00
ca008c809a added update-blog reference and fixed login 2023-10-07 20:19:10 -07:00
6df7590051 fix styling issues 2023-10-07 17:13:44 -07:00
60bd5e57fc Update README.md 2023-10-06 21:56:27 -07:00
703a7a316a Update values.yaml for staging 2023-10-06 16:19:10 -04:00
f4de7a2c56 Revert "disable mongo for staging"
This reverts commit 383825672bb536b759d22407d716f1ddabc292b7.
2023-10-06 16:18:28 -04:00
383825672b disable mongo for staging 2023-10-06 16:08:58 -04:00
c6124d7444 update memory value for backend 2023-10-06 15:56:44 -04:00
ee80f4a89b add / after cli callback host 2023-10-05 15:42:21 -04:00
0b3b014bf5 Merge pull request from akhilmhdh/feat/secret-approval-part-2
Policy based secret review system
2023-10-05 15:06:41 -04:00
7f463cabce feat(secret-approval): moved approval code to ee 2023-10-05 21:10:18 +05:30
b1962129a3 Merge pull request 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
28ad403665 change broswer based login from localhost to 127.0.0.1 2023-10-05 11:19:29 -04:00
cb893f71ee feat(secret-approval): conflict ui, request count and secret path in request detail 2023-10-05 20:45:16 +05:30
80a3ea42ac minor typo 2023-10-05 20:44:16 +05:30
aafd7f0884 add secretApproval to globalFeatureSet 2023-10-05 20:44:16 +05:30
faacb75034 feat(secret-approval): added new audit log and subscription on policy creation 2023-10-05 20:44:16 +05:30
7caac2e64c minor style updates to approvals 2023-10-05 20:34:50 +05:30
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
9dc97f7208 feat(secret-approval): added auto naming policy and minor ux enhancements 2023-10-05 20:34:50 +05:30
4fd227c85f feat(secret-approval): added loading and empty states for request list 2023-10-05 20:34:50 +05:30
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
63588b4e44 feat(secret-approval): implemented the new policy based approval system bare version 2023-10-05 20:34:50 +05:30
fc43511f5d feat(secret-approval): implemented the base 2023-10-05 20:33:48 +05:30
a995627815 Merge pull request from Infisical/service-token-v3
Service Token V3
2023-10-05 15:42:41 +01:00
c2f7923c1d Hide ST V3 from UI, switch docs to ST V2 2023-10-05 15:23:32 +01:00
abf6034aca Adjust ST V3 2023-10-05 14:53:59 +01:00
5fce85ca41 Merge pull request from scomans/main
fix: renaming environments not updated in `secretimports` model
2023-10-04 23:12:30 -04:00
702d28faca Add URL_GITLAB_LOGIN envar to docs 2023-10-04 22:09:09 +01:00
dbeafe1f5d Attempt fix gitlab sso docs images 2023-10-04 22:03:18 +01:00
46f700023b Update README 2023-10-04 21:49:06 +01:00
25b988ca9a Merge pull request from atimapreandrew/gitlab-sso
Gitlab sso
2023-10-04 21:39:52 +01:00
41af5cea93 Move google, github, gitlab auth out of /ee 2023-10-04 21:31:50 +01:00
e21daf6771 Add docs for gitlab sso, add support for self-hosted gitlab instance sso 2023-10-04 20:48:42 +01:00
c0f81ec84e Merge pull request from RobinTail:RobinTail-patch-1
Upgrading `zod`
2023-10-04 15:22:04 -04:00
c85e2c71ca add NEXT_INFISICAL_PLATFORM_VERSION to staging 2023-10-04 15:06:59 -04:00
9ae6e5ea1c Merge pull request from ragnarbull/display-curr-version
Feat: add version tags to Docker image & display on frontend
2023-10-04 15:01:11 -04:00
d3026a98d8 add back infisical/backend:latest 2023-10-04 14:57:30 -04:00
14b8c2c12a remove unrelated changes 2023-10-04 14:29:36 -04:00
d9a69441c4 Updating the lock file accordingly. 2023-10-04 14:57:43 +00:00
f46a23dabf Upgrading zod in package.json 2023-10-04 16:54:39 +02:00
9e2d6daeba Merge remote-tracking branch 'origin' into gitlab-sso 2023-10-04 11:11:05 +01:00
a2bebb5afa Merge remote-tracking branch 'origin' into service-token-v3 2023-10-04 11:02:55 +01:00
9fc5303a97 Merge pull request from Infisical/update-docs
Update Platform Documentation
2023-10-04 11:01:47 +01:00
97a5b509b7 Point getting started SDK to SDK repos to avoid docs sprawl 2023-10-04 10:57:11 +01:00
7660119584 Merge remote-tracking branch 'origin' into update-docs 2023-10-04 10:44:38 +01:00
a51d202d51 Fix merge conflicts 2023-10-04 10:39:50 +01:00
34273b30f2 Finish update for platform docs 2023-10-04 10:25:54 +01:00
726f38c15f Resolve review comments 2023-10-04 04:50:16 +00:00
390c2cc4d9 Changed to pass build arg & create NEXT envar 2023-10-04 02:03:17 +00:00
49098b7693 update folder name of build tool integrations 2023-10-03 20:12:03 -04:00
501d940558 add Gradle docs 2023-10-03 20:00:50 -04:00
7234c014c8 Merge pull request from akhilmhdh:fix/remove-import-permission-token
fix: resolved permission check on imported secrets when using service token
2023-10-03 12:38:12 -07:00
f3908e6b2a fix: resolved permission check on imported secrets when using service token 2023-10-03 20:14:43 +05:30
cf4eb629f2 Start revising project docs 2023-10-03 08:58:13 +01:00
95af82963f Add version tags to Docker image & display on frontend 2023-10-03 05:55:36 +00:00
bd8c17d720 resolve merge conflict 2023-10-02 18:58:38 -04:00
d3bc95560c Merge remote-tracking branch 'origin' into update-docs 2023-10-02 20:57:03 +01:00
4a838b788f Start comb docs, integrations, intro, organization 2023-10-02 20:26:16 +01:00
01c655699c Merge pull request from Infisical/proper-server-cleanup
handle siginit and sigterm
2023-10-02 11:44:36 -07:00
3456dfbd86 handle siginit and sigterm 2023-10-02 11:40:00 -07:00
560bde297c Merge pull request from akhilmhdh/fix/path-wrong-name
fix: resolved dashboard showing text folderName in nesting folders
2023-10-02 07:45:01 -07:00
c2ca4d77fc Add ST V3 docs, update ST-handling recommendation docs 2023-10-01 20:06:36 +01:00
3b3f78ee3c Start revising docs 2023-10-01 16:42:40 +01:00
d6a5c50fd9 Fix lint issue 2023-09-30 22:04:06 +01:00
839fcc2775 Add paywall to ST V3 ip allowlisting 2023-09-30 21:55:18 +01:00
eb2f433f43 Add trusted IP rules to ST V3 2023-09-30 20:52:04 +01:00
04c74293ed fix: resolved dashboard showing text folderName in nesting folders 2023-09-30 23:47:38 +05:30
cdf4440848 Fix lint issues 2023-09-30 13:45:37 +01:00
20b584b7b8 Fix merge conflicts 2023-09-30 12:54:43 +01:00
3779209ed5 Update permission implementation for ST V3 2023-09-30 12:52:35 +01:00
105c2e51ee fix: renaming environments not updated in secretimports model 2023-09-30 08:54:51 +02:00
66aa218ad9 Merge pull request from Infisical/snyk-fix-f1bf0685a5c66fa5416b87a6ef8520e1
[Snyk] Security upgrade sharp from 0.32.1 to 0.32.6
2023-09-29 15:05:46 -07:00
fb3a386aa3 Merge pull request from akhilmhdh/feat/patch-dashboardv3
feat(dashboard-v3): patched dashboard copy sec bug
2023-09-29 08:21:12 -07:00
d723d26d2e Fix merge conflicts 2023-09-29 11:42:47 +01:00
8a576196a3 Return workspaceId in response for service token key copy 2023-09-29 11:16:12 +01:00
2cf5fd80ca feat(dashboard-v3): removed a line at top on empty state 2023-09-29 14:31:31 +05:30
74534cfbaa feat(dashboard-v3): patched dashboard copy sec bug and add secret in empty state 2023-09-29 13:34:44 +05:30
66787b1f93 fix secret scanning zod error for installationId 2023-09-28 23:09:12 -07:00
890082acbc Update service-token.mdx 2023-09-28 22:11:24 -07:00
a364b174e0 add expire time to service token create 2023-09-28 19:31:33 -07:00
2bb2ccc19e patch crypto in create service token in cli 2023-09-28 19:27:38 -07:00
3bbf770027 bug fixes for v3 secret apis 2023-09-28 12:11:26 -07:00
2610356d45 Merge pull request from akhilmhdh/feat/dashboard-v3
Feat/dashboard v3
2023-09-28 10:35:35 -07:00
67e164e2bb feat(dashboard-v3): z-index change in tooltip for drawer 2023-09-28 22:29:50 +05:30
84fcb82116 fix: frontend/package.json & frontend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-SHARP-5922108
2023-09-28 16:52:33 +00:00
4502d12e46 feat(dashboard-v3): typo fix 2023-09-28 21:28:43 +05:30
ef6ee6b2e6 feat(dashboard-v3): resolved create secret issue and allow empty secret values in create secret 2023-09-28 21:28:43 +05:30
e902a54af0 remove deprecated EELogService 2023-09-28 21:28:43 +05:30
50efb8b8bd feat: resolved minor issues with dashboard v3 on feedback 2023-09-28 21:28:43 +05:30
5450c1126a minor style updates to the dashboard 2023-09-28 21:28:43 +05:30
4929022523 fix: resolved trimming keys but keeping last line break for ssh keys and added skip encoding on integration sync 2023-09-28 21:28:43 +05:30
85378e25aa feat: updated input create secret style and some more updates on style 2023-09-28 21:28:43 +05:30
b54c29fc48 feat(dashboard-v3): implemented new the dashboard with v3 support 2023-09-28 21:28:43 +05:30
fcf3f2837e feat(dashboard-v3): updated ui components and hooks for new migrated apis and v3 apis 2023-09-28 21:28:43 +05:30
0ada343b6f feat(dashboard-v3): migrated folder, imports and snapshots to use only secret path and not folder id 2023-09-28 21:28:06 +05:30
d0b8aba990 Merge pull request from G3root/update-other
fix: renaming environments not updated in some models
2023-09-28 07:58:05 -07:00
4365be9b75 Merge pull request from akhilmhdh/feat/secret-approval
Secret approval policies feature
2023-09-27 23:56:16 -07:00
b0c398688b feat(secret-approval): updated names to secret policy and fixed approval number bug 2023-09-28 12:23:01 +05:30
1141408d5b add exit codes for errors 2023-09-27 21:42:34 -07:00
b24bff5af6 Update service-token.mdx 2023-09-27 21:17:28 -07:00
c67432a56f feat(secret-approval): implemented frontend ui for secret policies 2023-09-27 23:10:45 +05:30
edeb6bbc66 feat(secret-approval): implemented backend api for secret policies 2023-09-27 23:10:28 +05:30
f72e240ce5 Merge branch 'main' into gitlab-sso 2023-09-27 12:57:53 +01:00
77ec17ccd4 fix: update many query 2023-09-27 17:01:02 +05:30
6e992858aa fix: add renamed fields to other models 2023-09-27 15:12:32 +05:30
9cda85f03e checkpoint 2023-09-27 11:50:52 +05:30
c65a53f1f7 Add endpoint to get project key for ST V3 2023-09-26 14:24:55 +01:00
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
4683dc7869 Scope switch cases into blocks for secrets v3 2023-09-26 10:56:08 +01:00
bc489e65ca Integrated login with Gitlab 2023-09-26 02:21:27 +01:00
45b85ab962 Fix merge conflicts 2023-09-25 13:26:09 +01:00
698a268b5f Add permissions and audit logging to service tokens v3 2023-09-25 13:24:28 +01:00
0d74752169 Integrated login with Gitlab 2023-09-24 22:33:00 +01:00
255705501f Integrated login with Gitlab 2023-09-24 15:21:23 +01:00
f59b3b3305 Add separate ServiceTokenV3 auth type 2023-09-22 14:43:42 +01:00
53856ff868 Make more progress on service token v3 2023-09-22 11:19:14 +01:00
84d094b4d8 Finish preliminary CRUD ops for service token v3, ServiceTokenV3Key structure 2023-09-21 15:15:22 +01:00
efb14ca267 Merge remote-tracking branch 'origin' into service-token-v3 2023-09-20 17:47:55 +01:00
1896442168 Finish basic scaffolding for service token v3 2023-09-20 17:32:33 +01:00
1cdd840485 Begin service token v3 2023-09-18 22:15:48 +01:00
607 changed files with 19805 additions and 7748 deletions
.dockerignore.env.example
.github
.gitignoreDockerfile.standalone-infisicalREADME.md
backend
.eslintrcpackage-lock.jsonpackage.json
src
config
controllers
ee
helpers
index.ts
integrations
interfaces
middleware
services/SecretService
middleware
models
queues
routes
services
utils
validation
variables
cli/packages/cmd
cypress.config.js
cypress
docker-compose.yml
docs
cli/commands
documentation
images
activity-logs.pngauthentication-google-redirect.pngdashboard-add-folder.pngdashboard-folder-overview.pngdashboard-folders.pngdashboard-name-modal-organization.pngdashboard-name-selected.pngdashboard-secrets-overview.pngdashboard.pngexample-secret-referencing.pngintegrations-teamcity-auth.pngintegrations-teamcity-create.pngintegrations-teamcity-dashboard.pngintegrations-teamcity-projects.pngintegrations-teamcity-serverurl.pngintegrations-teamcity-tokens.pngintegrations-teamcity.png
integrations
aws
bitbucket
checkly
circleci
cloud-66
cloudflare
codefresh
digital-ocean
flyio
hashicorp-vault
laravel-forge
northflank
railway
render
supabase
terraform
travis-ci
windmill
organization-ic-add.pngorganization-ic.pngorganization-members-add.pngorganization-members.pngorganization-overview.pngorganization-service-accounts.pngorganization.pngpit-commits.pngpit-snapshot.pngpit-snapshots.png
platform
project-download.pngproject-drag-drop.pngproject-envar-override.pngproject-envar-toggle-moved.pngproject-envar-toggle.pngproject-environment.pngproject-folder-token.pngproject-hide.pngproject-integrations.pngproject-members.pngproject-search-typed.pngproject-search.pngproject-token-add.pngproject-token-added.pngproject-token-name.pngproject-token-old-add.pngproject-token-old-permissions.pngproject-token-permissions.pngproject_settings_page.pngsecret-import-add.pngsecret-import-change-order.pngsecret-versioning.png
self-hosting/configuration/email
service-token-permissions.png
sso
integrations
internals
mint.json
security
self-hosting
ecosystem.config.js
frontend
.eslintrc.jsDockerfilecypress.config.js
cypress
next.config.jspackage-lock.jsonpackage.json
public
scripts
src
components
context/ProjectPermissionContext
helpers
hooks/api
layouts/AppLayout
pages
api/secret-scanning
integrations/github
login
org
[id]
members
overview
none
project/[id]
signup
views
DashboardPage
IntegrationsPage/components/IntegrationsSection
Login
Org/NonePage
Project
AuditLogsPage/components
MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection
SecretApprovalPage
SecretMainPage
SecretOverviewPage
SecretScanning/components
Settings
BillingSettingsPage/components/BillingCloudTab
OrgSettingsPage/components
OrgAuthTab
OrgDeleteSection
OrgGeneralTab
OrgIncidentContactsSection
OrgNameChangeSection
PersonalSettingsPage
ProjectSettingsPage
tailwind.config.js
helm-charts/infisical
img
nginx
standalone-entrypoint.sh

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

@ -9,6 +9,7 @@ JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
JWT_SERVICE_TOKEN_SECRET=f32f716d70a42c5703f4656015e76200
JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201
# JWT lifetime

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

41
.github/values.yaml vendored

@ -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:
enabled: true
name: backend
@ -32,7 +6,7 @@ backend:
secrets.infisical.com/auto-reload: "true"
replicaCount: 2
image:
repository: infisical/staging_deployment_backend
repository: infisical/staging_infisical
tag: "latest"
pullPolicy: Always
kubeSecretRef: managed-backend-secret
@ -40,12 +14,15 @@ backend:
annotations: {}
type: ClusterIP
nodePort: ""
resources:
limits:
memory: 300Mi
backendEnvironmentVariables: null
## Mongo DB persistence
mongodb:
enabled: true
enabled: false
persistence:
enabled: false
@ -60,14 +37,8 @@ ingress:
enabled: true
# annotations:
# 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
frontend:
path: /
pathType: Prefix
backend:
path: /api
pathType: Prefix
tls:
[]
# - secretName: letsencrypt-nginx

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

@ -2,7 +2,7 @@ name: Build, Publish and Deploy to Gamma
on: [workflow_dispatch]
jobs:
backend-image:
infisical-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
@ -32,8 +32,9 @@ jobs:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true
context: backend
tags: infisical/backend:test
context: .
file: Dockerfile.standalone-infisical
tags: infisical/infisical:test
- name: ⏻ Spawn backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
@ -49,66 +50,20 @@ jobs:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: backend
context: .
file: Dockerfile.standalone-infisical
tags: |
infisical/staging_deployment_backend:${{ steps.commit.outputs.short }}
infisical/staging_deployment_backend:latest
infisical/staging_infisical:${{ steps.commit.outputs.short }}
infisical/staging_infisical:latest
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:
name: Deploy to gamma
runs-on: ubuntu-latest
needs: [frontend-image, backend-image]
needs: [infisical-image]
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3

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

6
.gitignore vendored

@ -33,7 +33,7 @@ reports
junit.xml
# next.js
/.next/
.next/
/out/
# production
@ -59,4 +59,6 @@ yarn-error.log*
.infisical.json
# Editor specific
.vscode/*
.vscode/*
frontend-build

@ -1,7 +1,13 @@
ARG POSTHOG_HOST=https://app.posthog.com
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
@ -11,7 +17,7 @@ COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
RUN npm ci --only-production --ignore-scripts
# Rebuild the source code only when needed
FROM node:16-alpine AS frontend-builder
FROM base AS frontend-builder
WORKDIR /app
# Copy dependencies
@ -27,41 +33,38 @@ ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
# Build
RUN npm run build
# Production image
FROM node:16-alpine AS frontend-runner
FROM base AS frontend-runner
WORKDIR /app
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
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
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown nextjs:nodejs ./public/data
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/static ./.next/static
RUN chown non-root-user:nodejs ./public/data
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
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
##
## 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
@ -69,10 +72,11 @@ COPY backend/package*.json ./
RUN npm ci --only-production
COPY /backend .
COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh
RUN npm run build
# Production stage
FROM node:16-alpine AS backend-runner
FROM base AS backend-runner
WORKDIR /app
@ -81,27 +85,44 @@ RUN npm ci --only-production
COPY --from=backend-build /app .
RUN mkdir frontend-build
# 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 /
# 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=frontend-runner /app/ /app/
COPY --from=frontend-runner /app ./backend/frontend-build
EXPOSE 80
ENV PORT 8080
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"]

@ -1,9 +1,8 @@
<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">
</h1>
<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>
<h4 align="center">
@ -34,7 +33,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a>
<a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-1.38M-orange" alt="Cloudsmith downloads" />
<img src="https://img.shields.io/badge/Downloads-2.58M-orange" alt="Cloudsmith downloads" />
</a>
<a href="https://infisical.com/slack">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
@ -44,11 +43,11 @@
</a>
</h4>
<img src="/img/infisical_github_repo.png" width="100%" alt="Dashboard" />
<img src="/img/infisical_github_repo2.png" width="100%" alt="Dashboard" />
## 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.
@ -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:
- [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 [community calls](https://us06web.zoom.us/j/82623506356) every Wednesday at 11am EST to ask any questions, provide feedback, hangout and more.

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

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

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

@ -1,3 +1,5 @@
import { GITLAB_URL } from "../variables";
import InfisicalClient from "infisical-node";
export const client = new InfisicalClient({
@ -26,6 +28,7 @@ export const getJwtSignupLifetime = async () => (await client.getSecret("JWT_SIG
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 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 getNodeEnv = async () => (await client.getSecret("NODE_ENV")).secretValue || "production";
export const getVerboseErrorOutput = async () => (await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true;
@ -52,6 +55,9 @@ export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIEN
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 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 getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";

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

@ -1,7 +1,7 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
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 { sendMail } from "../../helpers/nodemailer";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
@ -15,7 +15,6 @@ import {
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import Role from "../../ee/models/role";
import { BadRequestError } from "../../utils/errors";
import { InviteUserToWorkspaceV1 } from "../../validation/workspace";

@ -6,13 +6,11 @@ import {
Organization,
Workspace
} 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 { licenseServerKeyRequest } from "../../config/request";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/organization";
import { ACCEPTED } from "../../variables";
import {
OrgPermissionActions,
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]
* @param req

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

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

@ -9,12 +9,9 @@ import { Secret, ServiceTokenData } from "../../models";
import { Folder } from "../../models/folder";
import {
appendFolder,
deleteFolderById,
generateFolderId,
getAllFolderIds,
getFolderByPath,
getFolderWithPathFromId,
getParentFromFolderId,
validateFolderName
} from "../../services/FolderService";
import {
@ -25,13 +22,9 @@ import {
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/folders";
/**
* Create folder with name [folderName] for workspace with id [workspaceId]
* and environment [environment]
* @param req
* @param res
* @returns
*/
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "The folder doesn't exist" });
// verify workspace id/environment
export const createFolder = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Create a folder'
@ -107,7 +100,7 @@ export const createFolder = async (req: Request, res: Response) => {
}
*/
const {
body: { workspaceId, environment, folderName, parentFolderId }
body: { workspaceId, environment, folderName, directory }
} = await validateRequest(reqValidator.CreateFolderV1, req);
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({
workspace: workspaceId,
environment
}).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
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({
workspace: workspaceId,
environment,
@ -152,14 +138,15 @@ export const createFolder = async (req: Request, res: Response) => {
id: "root",
name: "root",
version: 1,
children: [{ id, name: folderName, children: [], version: 1 }]
children: []
}
});
const { parent, child } = appendFolder(folder.nodes, { folderName, directory });
await folder.save();
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: folder.nodes
nodes: parent
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
@ -173,9 +160,9 @@ export const createFolder = async (req: Request, res: Response) => {
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: id,
folderId: child.id,
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);
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" });
}
}
if (!hasCreated) return res.json({ folder: child });
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder
nodes: parent
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolderId
folderId: child.id
});
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: folder.id,
folderId: child.id,
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 {
body: { workspaceId, environment, name },
params: { folderId }
body: { workspaceId, environment, name, directory },
params: { folderName }
} = await validateRequest(reqValidator.UpdateFolderV1, req);
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 });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const parentFolder = getParentFromFolderId(folders.nodes, folderId);
const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
if (req.user) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
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 folder = parentFolder.children.find(({ name }) => name === folderName);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
const oldFolderName = folder.name;
parentFolder.version += 1;
@ -505,24 +466,12 @@ export const deleteFolder = async (req: Request, res: Response) => {
}
*/
const {
params: { folderId },
body: { environment, workspaceId }
params: { folderName },
body: { environment, workspaceId, directory }
} = 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) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
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);
ForbiddenError.from(permission).throwUnlessCan(
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;
const delFolderIds = getAllFolderIds(delFolder);
const delFolderIds = getAllFolderIds(deletedFolder);
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
@ -565,9 +525,9 @@ export const deleteFolder = async (req: Request, res: Response) => {
type: EventType.DELETE_FOLDER,
metadata: {
environment,
folderId,
folderName: delFolder.name,
folderPath: secretPath
folderId: deletedFolder.id,
folderName: deletedFolder.name,
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 {
query: { workspaceId, environment, parentFolderId, parentFolderPath }
query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetFoldersV1, req);
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (req.user) await getUserProjectPermissions(req.user._id, workspaceId);
if (!folders) {
res.send({ folders: [], dir: [] });
return;
}
// if instead of parentFolderId given a path like /folder1/folder2
if (parentFolderPath) {
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
environment,
parentFolderPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folder = getFolderByPath(folders.nodes, parentFolderPath);
if (!folder) {
res.send({ folders: [], dir: [] });
return;
}
// dir is not needed at present as this is only used in overview section of secrets
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir: [{ name: folder.name, id: folder.id }]
});
}
if (!parentFolderId) {
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/");
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
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);
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// check that user is a member of the workspace
await getUserProjectPermissions(req.user._id, workspaceId);
}
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
return res.send({ folders: [], dir: [] });
}
const folder = getFolderByPath(folders.nodes, directory);
return res.send({
folders: folder?.children?.map(({ id, name }) => ({ id, name })) || []
});
};

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

@ -1,6 +1,7 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
Folder,
Integration,
Membership,
Secret,
@ -21,19 +22,22 @@ import {
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { SecretImport } from "../../models";
import { ServiceAccountWorkspacePermission } from "../../models";
import { Webhook } from "../../models";
/**
* Create new workspace environment named [environmentName]
* Create new workspace environment named [environmentName]
* with slug [environmentSlug] under workspace with id
* @param req
* @param res
* @returns
*/
export const createWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Create environment'
#swagger.description = 'Create environment'
#swagger.security = [{
"apiKeyAuth": []
}]
@ -42,12 +46,12 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
"description": "ID of project",
"required": true,
"type": "string"
}
}
/*
/*
#swagger.summary = 'Create environment'
#swagger.description = 'Create environment'
#swagger.security = [{
"apiKeyAuth": []
}]
@ -56,7 +60,7 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
"description": "ID of project",
"required": true,
"type": "string"
}
}
#swagger.requestBody = {
content: {
@ -84,7 +88,7 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"schema": {
"type": "object",
"properties": {
"message": {
@ -111,7 +115,7 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
},
"description": "Response after creating a new environment"
}
}
}
}
}
*/
@ -242,7 +246,7 @@ export const reorderWorkspaceEnvironments = async (req: Request, res: Response)
* @returns
*/
export const renameWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Rename workspace environment'
#swagger.description = 'Rename a specific environment within a workspace'
@ -313,7 +317,7 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
}
}
}
}
}
*/
const {
params: { workspaceId },
@ -369,13 +373,43 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
{ environment: environmentSlug }
);
await ServiceTokenData.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
{
workspace: workspaceId,
"scopes.environment": oldEnvironmentSlug
},
{ $set: { "scopes.$[element].environment": environmentSlug } },
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] }
);
await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Folder.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretImport.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await 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(
{
workspace: workspaceId,
@ -418,10 +452,10 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
* @returns
*/
export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Delete workspace environment'
#swagger.description = 'Delete a specific environment from a workspace'
#swagger.security = [{
"apiKeyAuth": []
}]
@ -454,7 +488,7 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) =>
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"schema": {
"type": "object",
"properties": {
"message": {
@ -472,9 +506,9 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) =>
},
"description": "Response after deleting an environment from a workspace"
}
}
}
}
}
}
*/
const {
params: { workspaceId },
@ -561,10 +595,10 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) =>
// TODO(akhilmhdh) after rbac this can be completely removed
export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Get all accessible environments of a workspace'
#swagger.description = 'Fetch all environments that the user has access to in a specified workspace'
#swagger.security = [{
"apiKeyAuth": []
}]
@ -611,7 +645,7 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res:
}
}
}
}
}
*/
const {
params: { workspaceId }

@ -1,11 +1,25 @@
import { Request, Response } from "express";
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 { updateSubscriptionOrgQuantity } from "../../helpers/organization";
import Role from "../../ee/models/role";
import { BadRequestError } from "../../utils/errors";
import { CUSTOM } from "../../variables";
import {
createOrganization as create,
deleteOrganization,
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 { validateRequest } from "../../helpers/validation";
import {
@ -332,3 +346,60 @@ export const getOrganizationServiceAccounts = async (req: Request, res: Response
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
});
}

@ -5,49 +5,9 @@ import bcrypt from "bcrypt";
import { APIKeyData, AuthMethod, MembershipOrg, TokenVersion, User } from "../../models";
import { getSaltRounds } from "../../config";
import { validateRequest } from "../../helpers/validation";
import { deleteUser } from "../../helpers/user";
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].
* 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"
});
};
/**
* 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
});
}

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

@ -3,10 +3,11 @@ import { Types } from "mongoose";
import { EventService, SecretService } from "../../services";
import { eventPushSecrets } from "../../events";
import { BotService } from "../../services";
import { containsGlobPatterns, isValidScope, repackageSecretToRaw } from "../../helpers/secrets";
import { containsGlobPatterns, isValidScopeV3, repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
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 { BadRequestError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation";
@ -17,8 +18,115 @@ import {
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
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 { 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
@ -58,32 +166,13 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
if (!environment || !workspaceId)
throw BadRequestError({ message: "Missing environment or workspace id" });
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
}
const { authVerifier: permissionCheckFn } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Read
});
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
@ -150,21 +239,13 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
}
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Read
});
const secret = await SecretService.getSecret({
secretName,
@ -196,24 +277,24 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
export const createSecretRaw = async (req: Request, res: Response) => {
const {
params: { secretName },
body: { secretPath, environment, workspaceId, type, secretValue, secretComment }
body: {
secretPath,
environment,
workspaceId,
type,
secretValue,
secretComment,
skipMultilineEncoding
}
} = await validateRequest(reqValidator.CreateSecretRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Create
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
@ -249,7 +330,8 @@ export const createSecretRaw = async (req: Request, res: Response) => {
secretPath,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag
secretCommentTag: secretCommentEncrypted.tag,
skipMultilineEncoding
});
await EventService.handleEvent({
@ -279,24 +361,16 @@ export const createSecretRaw = async (req: Request, res: Response) => {
export const updateSecretByNameRaw = async (req: Request, res: Response) => {
const {
params: { secretName },
body: { secretValue, environment, secretPath, type, workspaceId }
body: { secretValue, environment, secretPath, type, workspaceId, skipMultilineEncoding }
} = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Edit
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
@ -316,7 +390,8 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretPath
secretPath,
skipMultilineEncoding
});
await EventService.handleEvent({
@ -346,21 +421,13 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
body: { environment, secretPath, type, workspaceId }
} = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Delete
});
const { secret } = await SecretService.deleteSecret({
secretName,
@ -409,37 +476,18 @@ export const getSecrets = async (req: Request, res: Response) => {
if (folderId && folderId !== "root") {
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;
}
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
}
const { authVerifier: permissionCheckFn } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Read
});
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
@ -488,21 +536,13 @@ export const getSecretByName = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
}
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Read
});
const secret = await SecretService.getSecret({
secretName,
@ -540,25 +580,50 @@ export const createSecret = async (req: Request, res: Response) => {
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext
secretCommentCiphertext,
skipMultilineEncoding
},
params: { secretName }
} = await validateRequest(reqValidator.CreateSecretV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
const { membership } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
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({
@ -577,7 +642,8 @@ export const createSecret = async (req: Request, res: Response) => {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
metadata
metadata,
skipMultilineEncoding
});
await EventService.handleEvent({
@ -607,28 +673,68 @@ export const updateSecretByName = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueTag,
secretValueIV,
secretId,
type,
environment,
secretPath,
workspaceId
workspaceId,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
secretName: newSecretName,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
skipMultilineEncoding
},
params: { secretName }
} = await validateRequest(reqValidator.UpdateSecretByNameV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
if (newSecretName && (!secretKeyIV || !secretKeyTag || !secretKeyCiphertext)) {
throw BadRequestError({ message: "Missing encrypted key" });
}
const { membership } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
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({
@ -636,11 +742,21 @@ export const updateSecretByName = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
secretId,
authData: req.authData,
newSecretName,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath
secretPath,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
});
await EventService.handleEvent({
@ -663,28 +779,43 @@ export const updateSecretByName = async (req: Request, res: Response) => {
*/
export const deleteSecretByName = async (req: Request, res: Response) => {
const {
body: { type, environment, secretPath, workspaceId },
body: { type, environment, secretPath, workspaceId, secretId },
params: { secretName }
} = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
const { membership } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
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({
secretName,
secretId,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
@ -704,3 +835,135 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
secret
});
};
export const createSecretByNameBatch = async (req: Request, res: Response) => {
const {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req);
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
});
};

@ -1,7 +1,7 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { validateRequest } from "../../helpers/validation";
import { Secret } from "../../models";
import { Secret, ServiceTokenDataV3 } from "../../models";
import { SecretService } from "../../services";
import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService";
import { UnauthorizedRequestError } from "../../utils/errors";
@ -101,3 +101,17 @@ export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
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
});
}

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

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

@ -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 });
};

@ -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 });
};

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

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

@ -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
});
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,12 +1,12 @@
import { Types } from "mongoose";
import * as Sentry from "@sentry/node";
import NodeCache from "node-cache";
import {
import {
getLicenseKey,
getLicenseServerKey,
getLicenseServerUrl,
} from "../../config";
import {
import {
licenseKeyRequest,
licenseServerKeyRequest,
refreshLicenseKeyToken,
@ -37,6 +37,7 @@ interface FeatureSet {
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
trial_end: number | null;
has_used_trial: boolean;
secretApproval: boolean;
}
/**
@ -46,7 +47,7 @@ interface FeatureSet {
* - Self-hosted enterprise: Fetch and update global feature set
*/
class EELicenseService {
private readonly _isLicenseValid: boolean; // TODO: deprecate
public instanceType: "self-hosted" | "enterprise-self-hosted" | "cloud" = "self-hosted";
@ -64,7 +65,7 @@ class EELicenseService {
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,
rbac: true,
rbac: false,
customRateLimits: false,
customAlerts: false,
auditLogs: false,
@ -72,18 +73,19 @@ class EELicenseService {
samlSSO: false,
status: null,
trial_end: null,
has_used_trial: true
has_used_trial: true,
secretApproval: false
}
public localFeatureSet: NodeCache;
constructor() {
this._isLicenseValid = true;
this.localFeatureSet = new NodeCache({
stdTTL: 60,
});
}
public async getPlan(organizationId: Types.ObjectId, workspaceId?: Types.ObjectId): Promise<FeatureSet> {
try {
if (this.instanceType === "cloud") {
@ -96,7 +98,7 @@ class EELicenseService {
if (!organization) throw OrganizationNotFoundError();
let url = `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`;
if (workspaceId) {
url += `?workspaceId=${workspaceId}`;
}
@ -114,14 +116,14 @@ class EELicenseService {
return this.globalFeatureSet;
}
public async refreshPlan(organizationId: Types.ObjectId, workspaceId?: Types.ObjectId) {
if (this.instanceType === "cloud") {
this.localFeatureSet.del(`${organizationId.toString()}-${workspaceId?.toString() ?? ""}`);
await this.getPlan(organizationId, workspaceId);
}
}
public async delPlan(organizationId: Types.ObjectId) {
if (this.instanceType === "cloud") {
this.localFeatureSet.del(`${organizationId.toString()}-`);
@ -136,23 +138,23 @@ class EELicenseService {
if (licenseServerKey) {
// license server key is present -> validate it
const token = await refreshLicenseServerKeyToken()
if (token) {
this.instanceType = "cloud";
}
return;
}
if (licenseKey) {
// license key is present -> validate it
const token = await refreshLicenseKeyToken();
if (token) {
const { data: { currentPlan } } = await licenseKeyRequest.get(
`${await getLicenseServerUrl()}/api/license/v1/plan`
);
this.globalFeatureSet = currentPlan;
this.instanceType = "enterprise-self-hosted";
}

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

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

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

@ -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()
})
});

@ -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"])
})
});

@ -7,6 +7,7 @@ import {
ITokenVersion,
IUser,
ServiceTokenData,
ServiceTokenDataV3,
TokenVersion,
User,
} from "../models";
@ -23,12 +24,14 @@ import {
getJwtProviderAuthSecret,
getJwtRefreshLifetime,
getJwtRefreshSecret,
getJwtServiceTokenSecret
} from "../config";
import {
AuthMode
} from "../variables";
import {
ServiceTokenAuthData,
ServiceTokenV3AuthData,
UserAuthData
} from "../interfaces/middleware";
@ -47,6 +50,9 @@ export const validateAuthMode = ({
headers: { [key: string]: string | string[] | undefined },
acceptedAuthModes: AuthMode[]
}) => {
// TODO: update this to accept service token v3
const apiKey = headers["x-api-key"];
const authHeader = headers["authorization"];
@ -65,6 +71,7 @@ export const validateAuthMode = ({
if (typeof authHeader === "string") {
// 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]
if (tokenType === null)
throw BadRequestError({ message: "Missing Authorization Header in the request header." });
if (tokenType.toLowerCase() !== "bearer")
@ -72,15 +79,21 @@ export const validateAuthMode = ({
if (tokenValue === null)
throw BadRequestError({ message: "Missing Authorization Body in the request header." });
switch (tokenValue.split(".", 1)[0]) {
const parts = tokenValue.split(".");
switch (parts[0]) {
case "st":
authMode = AuthMode.SERVICE_TOKEN;
authTokenValue = tokenValue;
break;
case "stv3":
authMode = AuthMode.SERVICE_TOKEN_V3;
authTokenValue = parts.slice(1).join(".");
break;
default:
authMode = AuthMode.JWT;
authTokenValue = tokenValue;
}
authTokenValue = tokenValue;
}
if (!authMode || !authTokenValue) throw BadRequestError({ message: "Missing valid Authorization or X-API-KEY in request header." });
@ -211,8 +224,73 @@ export const getAuthSTDPayload = async ({
userAgent: 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"])
}
}
/**
@ -382,11 +460,15 @@ export const createToken = ({
secret,
}: {
payload: any;
expiresIn: string | number;
expiresIn?: string | number;
secret: string;
}) => {
return jwt.sign(payload, secret, {
expiresIn,
...(
(expiresIn !== undefined && expiresIn !== null)
? { expiresIn }
: {}
)
});
};

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

@ -14,7 +14,7 @@ export const initDatabaseHelper = async ({
}) => {
try {
await mongoose.connect(mongoURL);
// allow empty strings to pass the required validator
mongoose.Schema.Types.String.checkRequired(v => typeof v === "string");
@ -31,14 +31,10 @@ export const initDatabaseHelper = async ({
* Close database conection
*/
export const closeDatabaseHelper = async () => {
return Promise.all([
new Promise((resolve) => {
if (mongoose.connection && mongoose.connection.readyState == 1) {
mongoose.connection.close()
.then(() => resolve("Database connection closed"));
} else {
resolve("Database connection already closed");
}
}),
]);
}
if (mongoose.connection && mongoose.connection.readyState === 1) {
await mongoose.connection.close();
return "Database connection closed";
} else {
return "Database connection already closed";
}
};

@ -1,5 +1,43 @@
import { Types } from "mongoose";
import { MembershipOrg, Organization } from "../models";
import mongoose, { Types, mongo } from "mongoose";
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 {
ACCEPTED,
} from "../variables";
@ -17,6 +55,7 @@ import {
import {
createBotOrg
} from "./botOrg";
import { InternalServerError, ResourceNotFoundError } from "../utils/errors";
/**
* Create an organization with name [name]
@ -65,6 +104,320 @@ export const createOrganization = async ({
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
* the organization.

@ -1,20 +1,25 @@
import { Types } from "mongoose";
import {
CreateSecretBatchParams,
CreateSecretParams,
DeleteSecretBatchParams,
DeleteSecretParams,
GetSecretParams,
GetSecretsParams,
UpdateSecretBatchParams,
UpdateSecretParams
} from "../interfaces/services/SecretService";
import {
Folder,
ISecret,
IServiceTokenData,
IServiceTokenDataV3,
Secret,
SecretBlindIndexData,
ServiceTokenData,
TFolderRootSchema
} from "../models";
import { Permission } from "../models/serviceTokenDataV3";
import { EventType, SecretVersion } from "../ee/models";
import {
BadRequestError,
@ -50,6 +55,49 @@ import picomatch from "picomatch";
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 = (
authPayload: IServiceTokenData,
environment: string,
@ -71,6 +119,8 @@ export function containsGlobPatterns(secretPath: string) {
return globChars.some((char) => normalizedPath.includes(char));
}
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "Folder not found" });
/**
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
*
@ -330,7 +380,8 @@ export const createSecretHelper = async ({
secretCommentIV,
secretCommentTag,
secretPath = "/",
metadata
metadata,
skipMultilineEncoding
}: CreateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
@ -394,6 +445,7 @@ export const createSecretHelper = async ({
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
skipMultilineEncoding,
folder: folderId,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
@ -416,6 +468,7 @@ export const createSecretHelper = async ({
secretValueCiphertext,
secretValueIV,
secretValueTag,
skipMultilineEncoding,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
});
@ -737,27 +790,70 @@ export const getSecretHelper = async ({
export const updateSecretHelper = async ({
secretName,
workspaceId,
secretId,
environment,
type,
authData,
newSecretName,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath
secretPath,
tags,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
skipMultilineEncoding
}: UpdateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
// get secret blind index salt
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
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;
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) {
// case: update shared secret
secret = await Secret.findOneAndUpdate(
{
secretBlindIndex,
secretBlindIndex: oldSecretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
@ -767,6 +863,15 @@ export const updateSecretHelper = async ({
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex: newSecretNameBlindIndex,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
tags,
$inc: { version: 1 }
},
{
@ -778,7 +883,7 @@ export const updateSecretHelper = async ({
secret = await Secret.findOneAndUpdate(
{
secretBlindIndex,
secretBlindIndex: oldSecretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
type,
@ -789,6 +894,12 @@ export const updateSecretHelper = async ({
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
tags,
skipMultilineEncoding,
secretBlindIndex: newSecretNameBlindIndex,
$inc: { version: 1 }
},
{
@ -805,16 +916,18 @@ export const updateSecretHelper = async ({
workspace: secret.workspace,
folder: folderId,
type,
tags,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment,
isDeleted: false,
secretBlindIndex,
secretBlindIndex: newSecretName ? newSecretNameBlindIndex : oldSecretBlindIndex,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
skipMultilineEncoding,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
});
@ -903,12 +1016,22 @@ export const deleteSecretHelper = async ({
environment,
type,
authData,
secretPath = "/"
secretPath = "/",
// used for update corner case and blindIndex goes wrong way
secretId
}: DeleteSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
let secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
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);
@ -1098,6 +1221,7 @@ const recursivelyExpandSecret = async (
let interpolatedValue = interpolatedSec[key];
if (!interpolatedValue) {
// eslint-disable-next-line no-console
console.error(`Couldn't find referenced value - ${key}`);
return "";
}
@ -1147,7 +1271,7 @@ const formatMultiValueEnv = (val?: string) => {
export const expandSecrets = async (
workspaceId: string,
rootEncKey: string,
secrets: Record<string, { value: string; comment?: string }>
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>
) => {
const expandedSec: Record<string, string> = {};
const interpolatedSec: Record<string, string> = {};
@ -1165,7 +1289,10 @@ export const expandSecrets = async (
for (const key of Object.keys(secrets)) {
if (expandedSec?.[key]) {
secrets[key].value = formatMultiValueEnv(expandedSec[key]);
// should not do multi line encoding if user has set it to skip
secrets[key].value = secrets[key].skipMultilineEncoding
? expandedSec[key]
: formatMultiValueEnv(expandedSec[key]);
continue;
}
@ -1180,8 +1307,506 @@ export const expandSecrets = async (
key
);
secrets[key].value = formatMultiValueEnv(expandedVal);
secrets[key].value = secrets[key].skipMultilineEncoding
? expandedVal
: formatMultiValueEnv(expandedVal);
}
return secrets;
};
export const createSecretBatchHelper = async ({
secrets,
workspaceId,
authData,
secretPath,
environment
}: CreateSecretBatchParams) => {
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment
});
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
folderId = folder.id;
}
// get secret blind index salt
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
const secretBlindIndexes = await Promise.all(
secrets.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[secrets[i].secretName] = curr;
secretBlindIndexToKey[curr] = secrets[i].secretName;
return prev;
}, {})
);
const exists = await Secret.exists({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
secrets.map(({ secretName, type }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}))
)
.exec();
if (exists)
throw BadRequestError({
message: "Failed to create secret that already exists"
});
// create secret
const newlyCreatedSecrets: ISecret[] = await Secret.insertMany(
secrets.map(
({
type,
secretName,
secretKeyIV,
metadata,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding
}) => ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
environment,
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
folder: folderId,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
metadata,
skipMultilineEncoding,
secretBlindIndex: secretBlindIndexes[secretName],
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
})
)
);
await EESecretService.addSecretVersions({
secretVersions: newlyCreatedSecrets.map(
(secret) =>
new SecretVersion({
secret: secret._id,
version: secret.version,
workspace: secret.workspace,
type: secret.type,
folder: folderId,
skipMultilineEncoding: secret?.skipMultilineEncoding,
...(secret.type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment,
isDeleted: false,
secretBlindIndex: secret.secretBlindIndex,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
})
)
});
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.CREATE_SECRETS,
metadata: {
environment,
secretPath,
secrets: newlyCreatedSecrets.map(({ secretBlindIndex, version, _id }) => ({
secretId: _id.toString(),
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
secretVersion: version
}))
}
},
{
workspaceId
}
);
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "secrets added",
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: 1,
environment,
workspaceId,
folderId,
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
return newlyCreatedSecrets;
};
export const updateSecretBatchHelper = async ({
workspaceId,
environment,
authData,
secretPath,
secrets
}: UpdateSecretBatchParams) => {
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment
});
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
folderId = folder.id;
}
// get secret blind index salt
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
const secretBlindIndexes = await Promise.all(
secrets.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[secrets[i].secretName] = curr;
secretBlindIndexToKey[curr] = secrets[i].secretName;
return prev;
}, {})
);
const secretsToBeUpdated = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.select("+secretBlindIndex")
.or(
secrets.map(({ secretName, type }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}))
)
.lean();
if (secretsToBeUpdated.length !== secrets.length)
throw BadRequestError({ message: "Some secrets not found" });
await Secret.bulkWrite(
secrets.map(
({
type,
secretName,
tags,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding
}) => ({
updateOne: {
filter: {
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
secretBlindIndex: secretBlindIndexes[secretName],
type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
},
update: {
$inc: {
version: 1
},
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags,
skipMultilineEncoding
}
}
})
)
);
const secretsGroupedByBlindIndex = secretsToBeUpdated.reduce<Record<string, ISecret>>(
(prev, curr) => {
if (curr.secretBlindIndex) prev[curr.secretBlindIndex] = curr;
return prev;
},
{}
);
await EESecretService.addSecretVersions({
secretVersions: secrets.map((secret) => {
const {
_id,
version,
workspace,
type,
secretBlindIndex,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
skipMultilineEncoding
} = secretsGroupedByBlindIndex[secretBlindIndexes[secret.secretName]];
return new SecretVersion({
secret: _id,
version: version + 1,
workspace: workspace,
type,
folder: folderId,
...(secret.type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment,
isDeleted: false,
secretBlindIndex: secretBlindIndex,
secretKeyCiphertext: secretKeyCiphertext,
secretKeyIV: secretKeyIV,
secretKeyTag: secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
skipMultilineEncoding
});
})
});
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.UPDATE_SECRETS,
metadata: {
environment,
secretPath,
secrets: secretsToBeUpdated.map(({ _id, version, secretBlindIndex }) => ({
secretId: _id.toString(),
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
secretVersion: version + 1
}))
}
},
{
workspaceId
}
);
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "secrets modified",
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: 1,
environment,
workspaceId,
folderId,
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
return;
};
export const deleteSecretBatchHelper = async ({
workspaceId,
environment,
authData,
secretPath = "/",
secrets
}: DeleteSecretBatchParams) => {
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment
});
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
folderId = folder.id;
}
// get secret blind index salt
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
const secretBlindIndexes = await Promise.all(
secrets.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[secrets[i].secretName] = curr;
secretBlindIndexToKey[curr] = secrets[i].secretName;
return prev;
}, {})
);
const deletedSecrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
secrets.map(({ secretName, type }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: type === "shared" ? { $in: ["shared", "personal"] } : type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}))
)
.select({ secretBlindIndexes: 1 })
.lean()
.exec();
await Secret.deleteMany({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
secrets.map(({ secretName, type }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: type === "shared" ? { $in: ["shared", "personal"] } : type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}))
)
.exec();
await EESecretService.markDeletedSecretVersions({
secretIds: deletedSecrets.map((secret) => secret._id)
});
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.DELETE_SECRETS,
metadata: {
environment,
secretPath,
secrets: deletedSecrets.map(({ _id, version, secretBlindIndex }) => ({
secretId: _id.toString(),
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
secretVersion: version
}))
}
},
{
workspaceId
}
);
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "secrets deleted",
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
folderId,
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
return {
secrets: deletedSecrets
};
};

@ -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 {
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]
@ -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();
}
}
}

@ -1,18 +1,42 @@
import { Types } from "mongoose";
import mongoose, { Types, mongo } from "mongoose";
import {
Bot,
BotKey,
Folder,
Integration,
IntegrationAuth,
Key,
Membership,
Secret,
Workspace,
SecretBlindIndexData,
SecretImport,
ServiceToken,
ServiceTokenData,
ServiceTokenDataV3,
ServiceTokenDataV3Key,
Tag,
Webhook,
Workspace
} from "../models";
import {
Action,
AuditLog,
FolderVersion,
IPType,
Log,
SecretApprovalPolicy,
SecretApprovalRequest,
SecretSnapshot,
SecretVersion,
TrustedIP
} from "../ee/models";
import { createBot } from "../helpers/bot";
import { EELicenseService } from "../ee/services";
import { SecretService } from "../services";
import {
InternalServerError,
ResourceNotFoundError
} from "../utils/errors";
/**
* Create a workspace with name [name] in organization with id [organizationId]
@ -77,18 +101,190 @@ export const createWorkspace = async ({
* @param {Object} obj
* @param {String} obj.id - id of workspace to delete
*/
export const deleteWorkspace = async ({ id }: { id: string }) => {
await Workspace.deleteOne({ _id: id });
await Bot.deleteOne({
workspace: id,
});
await Membership.deleteMany({
workspace: id,
});
await Secret.deleteMany({
workspace: id,
});
await Key.deleteMany({
workspace: id,
});
export const deleteWorkspace = async ({
workspaceId,
existingSession
}: {
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({
workspace: workspace._id
}, {
session
});
await Key.deleteMany({
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();
}
}
};

@ -25,8 +25,11 @@ import {
users as eeUsersRouter,
workspace as eeWorkspaceRouter,
roles as v1RoleRouter,
secretApprovalPolicy as v1SecretApprovalPolicy,
secretApprovalRequest as v1SecretApprovalRequest,
secretScanning as v1SecretScanningRouter
} from "./ee/routes/v1";
import { serviceTokenData as v3ServiceTokenDataRouter } from "./ee/routes/v3";
import {
auth as v1AuthRouter,
bot as v1BotRouter,
@ -38,6 +41,7 @@ import {
membership as v1MembershipRouter,
organization as v1OrganizationRouter,
password as v1PasswordRouter,
sso as v1SSORouter,
secretImps as v1SecretImpsRouter,
secret as v1SecretRouter,
secretsFolder as v1SecretsFolder,
@ -54,7 +58,6 @@ import {
organizations as v2OrganizationsRouter,
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceAccounts as v2ServiceAccountsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
signup as v2SignupRouter,
tags as v2TagsRouter,
@ -84,6 +87,9 @@ import { setup } from "./utils/setup";
import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices";
import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent";
const SmeeClient = require("smee-client"); // eslint-disable-line
import path from "path";
let handler: null | any = null;
const main = async () => {
await setup();
@ -144,6 +150,27 @@ const main = async () => {
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
app.use("/api/v1/secret", eeSecretRouter);
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
@ -153,6 +180,7 @@ const main = async () => {
app.use("/api/v1/organizations", eeOrganizationsRouter);
app.use("/api/v1/sso", eeSSORouter);
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
app.use("/api/v3/service-token", v3ServiceTokenDataRouter);
// v1 routes
app.use("/api/v1/signup", v1SignupRouter);
@ -176,6 +204,9 @@ const main = async () => {
app.use("/api/v1/webhooks", v1WebhooksRouter);
app.use("/api/v1/secret-imports", v1SecretImpsRouter);
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)
app.use("/api/v2/signup", v2SignupRouter);
@ -188,7 +219,7 @@ const main = async () => {
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/service-token", v2ServiceTokenDataRouter);
app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
// app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
// v3 routes (experimental)
app.use("/api/v3/auth", v3AuthRouter);
@ -202,6 +233,12 @@ const main = async () => {
// server status
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
app.use((req, res, next) => {
if (res.headersSent) return next();
@ -221,10 +258,24 @@ const main = async () => {
// await createTestUserForDevelopment();
setUpHealthEndpoint(server);
server.on("close", async () => {
const serverCleanup = async () => {
await DatabaseService.closeDatabase();
syncSecretsToThirdPartyServices.close();
githubPushEventSecretScan.close();
process.exit(0);
};
process.on("SIGINT", function () {
server.close(async () => {
await serverCleanup();
});
});
process.on("SIGTERM", function () {
server.close(async () => {
await serverCleanup();
});
});
return server;

@ -65,10 +65,10 @@ import sodium from "libsodium-wrappers";
import { standardRequest } from "../config/request";
const getSecretKeyValuePair = (
secrets: Record<string, { value: string; comment?: string } | null>
secrets: Record<string, { value: string | null; comment?: string } | null>
) =>
Object.keys(secrets).reduce<Record<string, string>>((prev, key) => {
if (secrets[key]) prev[key] = secrets[key]?.value || "";
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
prev[key] = secrets?.[key] === null ? null : secrets?.[key]?.value;
return prev;
}, {});
@ -87,13 +87,15 @@ const syncSecrets = async ({
integrationAuth,
secrets,
accessId,
accessToken
accessToken,
appendices
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: Record<string, { value: string; comment?: string }>;
accessId: string | null;
accessToken: string;
appendices?: { prefix: string, suffix: string };
}) => {
switch (integration.integration) {
case INTEGRATION_GCP_SECRET_MANAGER:
@ -153,7 +155,8 @@ const syncSecrets = async ({
await syncSecretsGitHub({
integration,
secrets,
accessToken
accessToken,
appendices
});
break;
case INTEGRATION_GITLAB:
@ -218,7 +221,8 @@ const syncSecrets = async ({
await syncSecretsCheckly({
integration,
secrets,
accessToken
accessToken,
appendices
});
break;
case INTEGRATION_QOVERY:
@ -325,40 +329,42 @@ const syncSecretsGCPSecretManager = async ({
name: string;
createTime: string;
}
interface GCPSMListSecretsRes {
secrets?: GCPSecret[];
totalSize?: number;
nextPageToken?: string;
}
let gcpSecrets: GCPSecret[] = [];
const pageSize = 100;
let pageToken: string | undefined;
let hasMorePages = true;
const filterParam = integration.metadata.secretGCPLabel
? `?filter=labels.${integration.metadata.secretGCPLabel.labelName}=${integration.metadata.secretGCPLabel.labelValue}`
const filterParam = integration.metadata.secretGCPLabel
? `?filter=labels.${integration.metadata.secretGCPLabel.labelName}=${integration.metadata.secretGCPLabel.labelValue}`
: "";
while (hasMorePages) {
const params = new URLSearchParams({
pageSize: String(pageSize),
...(pageToken ? { pageToken } : {})
});
const res: GCPSMListSecretsRes = (await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`,
{
params,
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
const res: GCPSMListSecretsRes = (
await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
}
)).data;
)
).data;
if (res.secrets) {
const filteredSecrets = res.secrets?.filter((gcpSecret) => {
const arr = gcpSecret.name.split("/");
@ -366,54 +372,58 @@ const syncSecretsGCPSecretManager = async ({
let isValid = true;
if (integration.metadata.secretPrefix && !key.startsWith(integration.metadata.secretPrefix)) {
if (
integration.metadata.secretPrefix &&
!key.startsWith(integration.metadata.secretPrefix)
) {
isValid = false;
}
if (integration.metadata.secretSuffix && !key.endsWith(integration.metadata.secretSuffix)) {
isValid = false;
}
return isValid;
});
gcpSecrets = gcpSecrets.concat(filteredSecrets);
}
if (!res.nextPageToken) {
hasMorePages = false;
}
pageToken = res.nextPageToken;
}
const res: { [key: string]: string; } = {};
const res: { [key: string]: string } = {};
interface GCPLatestSecretVersionAccess {
name: string;
payload: {
data: string;
}
};
}
for await (const gcpSecret of gcpSecrets) {
const arr = gcpSecret.name.split("/");
const key = arr[arr.length - 1];
const secretLatest: GCPLatestSecretVersionAccess = (await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
const secretLatest: GCPLatestSecretVersionAccess = (
await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
}
)).data;
)
).data;
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
}
for await (const key of Object.keys(secrets)) {
if (!(key in res)) {
// case: create secret
@ -423,11 +433,14 @@ const syncSecretsGCPSecretManager = async ({
replication: {
automatic: {}
},
...(integration.metadata.secretGCPLabel ? {
labels: {
[integration.metadata.secretGCPLabel.labelName]: integration.metadata.secretGCPLabel.labelValue
}
} : {})
...(integration.metadata.secretGCPLabel
? {
labels: {
[integration.metadata.secretGCPLabel.labelName]:
integration.metadata.secretGCPLabel.labelValue
}
}
: {})
},
{
params: {
@ -439,7 +452,7 @@ const syncSecretsGCPSecretManager = async ({
}
}
);
await standardRequest.post(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
{
@ -456,7 +469,7 @@ const syncSecretsGCPSecretManager = async ({
);
}
}
for await (const key of Object.keys(res)) {
if (!(key in secrets)) {
// case: delete secret
@ -489,7 +502,7 @@ const syncSecretsGCPSecretManager = async ({
}
}
}
}
};
/**
* Sync/push [secrets] to Azure Key Vault with vault URI [integration.app]
@ -729,15 +742,12 @@ const syncSecretsAWSParameterStore = async ({
} = {};
if (parameterList) {
awsParameterStoreSecretsObj = parameterList.reduce(
(obj: any, secret: any) => {
return ({
...obj,
[secret.Name.substring(integration.path.length)]: secret
});
},
{}
);
awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => {
return {
...obj,
[secret.Name.substring(integration.path.length)]: secret
};
}, {});
}
// Identify secrets to create
@ -1336,11 +1346,13 @@ const syncSecretsNetlify = async ({
const syncSecretsGitHub = async ({
integration,
secrets,
accessToken
accessToken,
appendices
}: {
integration: IIntegration;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
appendices?: { prefix: string, suffix: string };
}) => {
interface GitHubRepoKey {
key_id: string;
@ -1370,7 +1382,7 @@ const syncSecretsGitHub = async ({
).data;
// 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", {
owner: integration.owner,
repo: integration.app
@ -1383,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) => {
if (!(key in secrets)) {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
@ -1869,8 +1890,10 @@ const syncSecretsGitLab = async ({
value: 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 headers = {
@ -1880,7 +1903,9 @@ const syncSecretsGitLab = async ({
};
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) {
const response: any = await standardRequest.get(url, { headers });
@ -1901,23 +1926,27 @@ const syncSecretsGitLab = async ({
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables
.filter(
(secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment
)
.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
.filter((gitLabSecret) => {
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;
}
if (integration.metadata.secretSuffix && !gitLabSecret.key.endsWith(integration.metadata.secretSuffix)) {
if (
integration.metadata.secretSuffix &&
!gitLabSecret.key.endsWith(integration.metadata.secretSuffix)
) {
isValid = false;
}
return isValid;
});
for await (const key of Object.keys(secrets)) {
const existingSecret = getSecretsRes.find((s: any) => s.key == key);
if (!existingSecret) {
@ -2060,13 +2089,15 @@ const syncSecretsSupabase = async ({
const syncSecretsCheckly = async ({
integration,
secrets,
accessToken
accessToken,
appendices
}: {
integration: IIntegration;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
appendices?: { prefix: string, suffix: string };
}) => {
const getSecretsRes = (
let getSecretsRes = (
await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, {
headers: {
Authorization: `Bearer ${accessToken}`,
@ -2082,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
for await (const key of Object.keys(secrets)) {
if (!(key in getSecretsRes)) {
@ -2371,41 +2411,43 @@ const syncSecretsTeamCity = async ({
if (integration.targetEnvironment && integration.targetEnvironmentId) {
// case: sync to specific build-config in TeamCity project
const res = (await standardRequest.get<GetTeamCityBuildConfigParametersRes>(
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
}
))
.data
.property
.filter((parameter) => !parameter.inherited)
.reduce((obj: any, secret: TeamCitySecret) => {
const secretName = secret.name.replace(/^env\./, "");
return {
...obj,
[secretName]: secret.value
};
}, {});
const res = (
await standardRequest.get<GetTeamCityBuildConfigParametersRes>(
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
)
).data.property
.filter((parameter) => !parameter.inherited)
.reduce((obj: any, secret: TeamCitySecret) => {
const secretName = secret.name.replace(/^env\./, "");
return {
...obj,
[secretName]: secret.value
};
}, {});
for await (const key of Object.keys(secrets)) {
if (!(key in res) || (key in res && secrets[key].value !== res[key])) {
// case: secret does not exist in TeamCity or secret value has changed
// -> create/update secret
await standardRequest.post(`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{
name:`env.${key}`,
value: secrets[key].value
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
await standardRequest.post(
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{
name: `env.${key}`,
value: secrets[key].value
},
});
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
}
@ -3034,4 +3076,4 @@ const syncSecretsNorthflank = async ({
);
};
export { syncSecrets };
export { syncSecrets };

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

@ -16,6 +16,7 @@ export interface CreateSecretParams {
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
secretPath: string;
metadata?: {
source?: string;
@ -42,6 +43,11 @@ export interface GetSecretParams {
export interface UpdateSecretParams {
secretName: string;
newSecretName?: string;
secretId?: string;
secretKeyCiphertext?: string;
secretKeyIV?: string;
secretKeyTag?: string;
workspaceId: Types.ObjectId;
environment: string;
type: "shared" | "personal";
@ -50,13 +56,73 @@ export interface UpdateSecretParams {
secretValueIV: string;
secretValueTag: string;
secretPath: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
tags?: string[];
}
export interface DeleteSecretParams {
secretName: string;
secretId?: string;
workspaceId: Types.ObjectId;
environment: string;
type: "shared" | "personal";
authData: AuthData;
secretPath: string;
}
export interface CreateSecretBatchParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
secretPath: string;
secrets: Array<{
secretName: string;
type: "shared" | "personal";
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
metadata?: {
source?: string;
};
}>;
}
export interface UpdateSecretBatchParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
secretPath: string;
secrets: Array<{
secretName: string;
type: "shared" | "personal";
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
tags?: string[];
}>;
}
export interface DeleteSecretBatchParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
secretPath: string;
secrets: Array<{
secretName: string;
type: "shared" | "personal";
}>;
}

@ -3,8 +3,8 @@ import { ErrorRequestHandler } from "express";
import { TokenExpiredError } from "jsonwebtoken";
import { InternalServerError, UnauthorizedRequestError } from "../utils/errors";
import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError";
import { getNodeEnv } from "../config";
import RequestError from "../utils/requestError";
import { ForbiddenError } from "@casl/ability";
export const requestErrorHandler: ErrorRequestHandler = async (
error: RequestError | Error,
@ -14,41 +14,37 @@ export const requestErrorHandler: ErrorRequestHandler = async (
) => {
if (res.headersSent) return next();
if (await getNodeEnv() !== "production") {
/* 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,
});
const logAndCaptureException = async (error: RequestError) => {
(await getLogger("backend-main")).log(
(<RequestError>error).levelName.toLowerCase(),
(<RequestError>error).message
`${error.stack}\n${error.message}`
);
}
//* Set Sentry user identification if req.user is populated
if (req.user !== undefined && req.user !== null) {
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
)
) {
//* Set Sentry user identification if req.user is populated
if (req.user !== undefined && req.user !== null) {
Sentry.setUser({ email: (req.user as any).email });
}
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 });
}
await logAndCaptureException((<RequestError>error));
}
res
.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();
};

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

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

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

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

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

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

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

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

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

@ -40,9 +40,9 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => {
const prefix = (integration.metadata?.secretPrefix || "");
const suffix = (integration.metadata?.secretSuffix || "");
const newKey = prefix + key + suffix;
suffixedSecrets[newKey] = secrets[key];
}
}
}
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
@ -60,13 +60,14 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => {
integrationAuth,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
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) => {
console.log("QUEUE ERROR:", error) // eslint-disable-line
// console.log("QUEUE ERROR:", error) // eslint-disable-line
})
export const syncSecretsToActiveIntegrationsQueue = (jobDetails: TSyncSecretsToThirdPartyServices) => {

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

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

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

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

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

@ -12,23 +12,23 @@ import { AuthMode } from "../../variables";
router.post(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
}),
createFolder
);
router.patch(
"/:folderId",
"/:folderName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
}),
updateFolderById
);
router.delete(
"/:folderId",
"/:folderName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
}),
deleteFolder
);
@ -36,7 +36,7 @@ router.delete(
router.get(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
}),
getFolders
);

@ -0,0 +1,72 @@
import express from "express";
const router = express.Router();
import passport from "passport";
import { authLimiter } from "../../helpers/rateLimiter";
import { ssoController } from "../../ee/controllers/v1";
router.get("/redirect/google", authLimiter, (req, res, next) => {
passport.authenticate("google", {
scope: ["profile", "email"],
session: false,
...(req.query.callback_port
? {
state: req.query.callback_port as string
}
: {})
})(req, res, next);
});
router.get(
"/google",
passport.authenticate("google", {
failureRedirect: "/login/provider/error",
session: false
}),
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,
passport.authenticate("github", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get(
"/redirect/gitlab",
authLimiter,
(req, res, next) => {
passport.authenticate("gitlab", {
session: false,
...(req.query.callback_port ? {
state: req.query.callback_port as string
} : {})
})(req, res, next);
}
);
router.get(
"/gitlab",
authLimiter,
passport.authenticate("gitlab", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
export default router;

@ -54,4 +54,20 @@ router.get(
organizationsController.getOrganizationServiceAccounts
);
router.post(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.createOrganization
);
router.delete(
"/:organizationId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
organizationsController.deleteOrganizationById
);
export default router;

@ -4,14 +4,6 @@ import { requireAuth } from "../../middleware";
import { usersController } from "../../controllers/v2";
import { AuthMode } from "../../variables";
router.get(
"/me",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
usersController.getMe
);
router.patch(
"/me/mfa",
requireAuth({
@ -29,11 +21,11 @@ router.patch(
);
router.put(
"/me/auth-methods",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
usersController.updateAuthMethods
"/me/auth-methods",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
usersController.updateAuthMethods,
);
router.get(
@ -84,4 +76,20 @@ router.delete(
usersController.deleteMySessions
);
router.get(
"/me",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
usersController.getMe
);
router.delete(
"/me",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
usersController.deleteMe
);
export default router;

@ -7,5 +7,5 @@ export {
auth,
secrets,
signup,
workspaces,
workspaces
}

@ -1,19 +1,13 @@
import express from "express";
const router = express.Router();
import {
requireAuth,
requireBlindIndicesEnabled,
requireE2EEOff
} from "../../middleware";
import { requireAuth, requireBlindIndicesEnabled, requireE2EEOff } from "../../middleware";
import { secretsController } from "../../controllers/v3";
import {
AuthMode
} from "../../variables";
import { AuthMode } from "../../variables";
router.get(
"/raw",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
secretsController.getSecretsRaw
);
@ -21,7 +15,7 @@ router.get(
router.get(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "query"
@ -35,7 +29,7 @@ router.get(
router.post(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@ -49,7 +43,7 @@ router.post(
router.patch(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@ -63,7 +57,7 @@ router.patch(
router.delete(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@ -77,7 +71,7 @@ router.delete(
router.get(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "query"
@ -85,10 +79,44 @@ router.get(
secretsController.getSecrets
);
// akhilmhdh: dont put batch router below the individual operation as those have arbitory name as params
router.post(
"/batch",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
}),
secretsController.createSecretByNameBatch
);
router.patch(
"/batch",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
}),
secretsController.updateSecretByNameBatch
);
router.delete(
"/batch",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
}),
secretsController.deleteSecretByNameBatch
);
router.post(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@ -99,7 +127,7 @@ router.post(
router.get(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "query"
@ -110,7 +138,7 @@ router.get(
router.patch(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@ -121,7 +149,7 @@ router.patch(
router.delete(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"

@ -34,4 +34,12 @@ router.post(
// --
router.get(
"/:workspaceId/service-token",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
workspacesController.getWorkspaceServiceTokenData
);
export default router;

@ -5,7 +5,7 @@ import path from "path";
type TAppendFolderDTO = {
folderName: string;
parentFolderId?: string;
directory: string;
};
type TRenameFolderDTO = {
@ -50,9 +50,8 @@ export const folderBfsTraversal = async (
// bfs and then append to the folder
const appendChild = (folders: TFolderSchema, folderName: string) => {
const folder = folders.children.find(({ name }) => name === folderName);
if (folder) {
throw new Error("Folder already exists");
}
if (folder) return { folder, hasCreated: false };
const id = generateFolderId();
folders.version += 1;
folders.children.push({
@ -61,24 +60,32 @@ const appendChild = (folders: TFolderSchema, folderName: string) => {
children: [],
version: 1
});
return { id, name: folderName };
// last element that is the new one
return { folder: folders.children[folders.children.length - 1], hasCreated: true };
};
// root of append child wrapper
export const appendFolder = (
folders: TFolderSchema,
{ folderName, parentFolderId }: TAppendFolderDTO
) => {
const isRoot = !parentFolderId;
{ folderName, directory }: TAppendFolderDTO
): { parent: TFolderSchema; child: TFolderSchema; hasCreated?: boolean } => {
if (directory === "/") {
const newFolder = appendChild(folders, folderName);
return { parent: folders, child: newFolder.folder, hasCreated: newFolder.hasCreated };
}
if (isRoot) {
return appendChild(folders, folderName);
const segments = directory.split("/").filter(Boolean);
const segment = segments.shift();
if (segment) {
const nestedFolders = appendChild(folders, segment);
return appendFolder(nestedFolders.folder, {
folderName,
directory: path.join("/", ...segments)
});
}
const folder = searchByFolderId(folders, parentFolderId);
if (!folder) {
throw new Error("Parent Folder not found");
}
return appendChild(folder, folderName);
const newFolder = appendChild(folders, folderName);
return { parent: folders, child: newFolder.folder, hasCreated: newFolder.hasCreated };
};
export const renameFolder = (

@ -108,14 +108,6 @@ export const getAllImportedSecrets = async (
type: "shared"
}
},
{
$lookup: {
from: "tags", // note this is the name of the collection in the database, not the Mongoose model name
localField: "tags",
foreignField: "_id",
as: "tags"
}
},
{
$group: {
_id: {

@ -1,21 +1,27 @@
import { Types } from "mongoose";
import {
CreateSecretParams,
DeleteSecretParams,
GetSecretParams,
GetSecretsParams,
UpdateSecretParams,
CreateSecretBatchParams,
CreateSecretParams,
DeleteSecretBatchParams,
DeleteSecretParams,
GetSecretParams,
GetSecretsParams,
UpdateSecretBatchParams,
UpdateSecretParams
} from "../interfaces/services/SecretService";
import {
createSecretBlindIndexDataHelper,
createSecretHelper,
deleteSecretHelper,
generateSecretBlindIndexHelper,
generateSecretBlindIndexWithSaltHelper,
getSecretBlindIndexSaltHelper,
getSecretHelper,
getSecretsHelper,
updateSecretHelper,
import {
createSecretBatchHelper,
createSecretBlindIndexDataHelper,
createSecretHelper,
deleteSecretBatchHelper,
deleteSecretHelper,
generateSecretBlindIndexHelper,
generateSecretBlindIndexWithSaltHelper,
getSecretBlindIndexSaltHelper,
getSecretHelper,
getSecretsHelper,
updateSecretBatchHelper,
updateSecretHelper
} from "../helpers/secrets";
class SecretService {
@ -26,13 +32,9 @@ class SecretService {
* @param {Buffer} obj.salt - 16-byte random salt
* @param {Types.ObjectId} obj.workspaceId
*/
static async createSecretBlindIndexData({
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) {
static async createSecretBlindIndexData({ workspaceId }: { workspaceId: Types.ObjectId }) {
return await createSecretBlindIndexDataHelper({
workspaceId,
workspaceId
});
}
@ -42,13 +44,9 @@ class SecretService {
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
* @returns
*/
static async getSecretBlindIndexSalt({
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) {
static async getSecretBlindIndexSalt({ workspaceId }: { workspaceId: Types.ObjectId }) {
return await getSecretBlindIndexSaltHelper({
workspaceId,
workspaceId
});
}
@ -61,14 +59,14 @@ class SecretService {
*/
static async generateSecretBlindIndexWithSalt({
secretName,
salt,
salt
}: {
secretName: string;
salt: string;
}) {
return await generateSecretBlindIndexWithSaltHelper({
secretName,
salt,
salt
});
}
@ -81,14 +79,14 @@ class SecretService {
*/
static async generateSecretBlindIndex({
secretName,
workspaceId,
workspaceId
}: {
secretName: string;
workspaceId: Types.ObjectId;
}) {
return await generateSecretBlindIndexHelper({
secretName,
workspaceId,
workspaceId
});
}
@ -163,6 +161,18 @@ class SecretService {
static async deleteSecret(deleteSecretParams: DeleteSecretParams) {
return await deleteSecretHelper(deleteSecretParams);
}
static async createSecretBatch(createSecretParams: CreateSecretBatchParams) {
return await createSecretBatchHelper(createSecretParams);
}
static async updateSecretBatch(updateSecretParams: UpdateSecretBatchParams) {
return await updateSecretBatchHelper(updateSecretParams);
}
static async deleteSecretBatch(deleteSecretParams: DeleteSecretBatchParams) {
return await deleteSecretBatchHelper(deleteSecretParams);
}
}
export default SecretService;

@ -8,21 +8,25 @@ import {
Organization,
ServiceAccount,
ServiceTokenData,
ServiceTokenDataV3,
User
} from "../models";
import { createToken } from "../helpers/auth";
import {
getClientIdGitHubLogin,
getClientIdGitLabLogin,
getClientIdGoogleLogin,
getClientSecretGitHubLogin,
getClientSecretGitLabLogin,
getClientSecretGoogleLogin,
getJwtProviderAuthLifetime,
getJwtProviderAuthSecret,
getSiteURL,
getUrlGitLabLogin
} from "../config";
import { getSSOConfigHelper } from "../ee/helpers/organizations";
import { InternalServerError, OrganizationNotFoundError } from "./errors";
import { ACCEPTED, INTEGRATION_GITHUB_API_URL, INVITED, MEMBER } from "../variables";
import { getSiteURL } from "../config";
import { standardRequest } from "../config/request";
// eslint-disable-next-line @typescript-eslint/no-var-requires
@ -30,6 +34,8 @@ const GoogleStrategy = require("passport-google-oauth20").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const GitHubStrategy = require("passport-github").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const GitLabStrategy = require("passport-gitlab2").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { MultiSamlStrategy } = require("@node-saml/passport-saml");
/**
@ -49,6 +55,10 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
if (authData.authPayload instanceof ServiceTokenData) {
return { serviceTokenDataId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceTokenDataV3) {
return { serviceTokenDataId: authData.authPayload._id };
}
};
/**
@ -57,7 +67,6 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
* @returns
*/
const getAuthDataPayloadUserObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { user: authData.authPayload._id };
}
@ -67,7 +76,11 @@ const getAuthDataPayloadUserObj = (authData: AuthData) => {
}
if (authData.authPayload instanceof ServiceTokenData) {
return { user: authData.authPayload.user };0
return { user: authData.authPayload.user };
}
if (authData.authPayload instanceof ServiceTokenDataV3) {
return { user: authData.authPayload.user };
}
}
@ -76,6 +89,9 @@ const initializePassport = async () => {
const clientSecretGoogleLogin = await getClientSecretGoogleLogin();
const clientIdGitHubLogin = await getClientIdGitHubLogin();
const clientSecretGitHubLogin = await getClientSecretGitHubLogin();
const urlGitLab = await getUrlGitLabLogin();
const clientIdGitLabLogin = await getClientIdGitLabLogin();
const clientSecretGitLabLogin = await getClientSecretGitLabLogin();
if (clientIdGoogleLogin && clientSecretGoogleLogin) {
passport.use(new GoogleStrategy({
@ -209,6 +225,60 @@ const initializePassport = async () => {
}
));
}
if (urlGitLab && clientIdGitLabLogin && clientSecretGitLabLogin) {
passport.use(new GitLabStrategy({
passReqToCallback: true,
clientID: clientIdGitLabLogin,
clientSecret: clientSecretGitLabLogin,
callbackURL: "/api/v1/sso/gitlab",
baseURL: urlGitLab
},
async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => {
const email = profile.emails[0].value;
let user = await User.findOne({
email
}).select("+publicKey");
if (!user) {
user = await new User({
email: email,
authMethods: [AuthMethod.GITLAB],
firstName: profile.displayName,
lastName: ""
}).save();
}
let isLinkingRequired = false;
if (!user.authMethods.includes(AuthMethod.GITLAB)) {
isLinkingRequired = true;
}
const isUserCompleted = !!user.publicKey;
const providerAuthToken = createToken({
payload: {
userId: user._id.toString(),
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
authMethod: AuthMethod.GITLAB,
isUserCompleted,
isLinkingRequired,
...(req.query.state ? {
callbackPort: req.query.state as string
} : {})
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(),
});
req.isUserCompleted = isUserCompleted;
req.providerAuthToken = providerAuthToken;
return done(null, profile);
}
));
}
passport.use("saml", new MultiSamlStrategy(
{
@ -221,8 +291,7 @@ const initializePassport = async () => {
});
interface ISAMLConfig {
path: string;
callbackURL: string;
callbackUrl: string;
entryPoint: string;
issuer: string;
cert: string;
@ -231,8 +300,7 @@ const initializePassport = async () => {
}
const samlConfig: ISAMLConfig = ({
path: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`,
callbackURL: `${await getSiteURL()}/api/v1/sso/saml2${ssoIdentifier}`,
callbackUrl: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`,
entryPoint: ssoConfig.entryPoint,
issuer: ssoConfig.issuer,
cert: ssoConfig.cert,
@ -243,6 +311,12 @@ const initializePassport = async () => {
samlConfig.wantAuthnResponseSigned = false;
}
if (ssoConfig.authProvider.toString() === AuthMethod.AZURE_SAML.toString()) {
if (req.body.RelayState && JSON.parse(req.body.RelayState).spInitiated) {
samlConfig.audience = `spn:${ssoConfig.issuer}`;
}
}
req.ssoConfig = ssoConfig;
done(null, samlConfig);
@ -335,7 +409,7 @@ const initializePassport = async () => {
authMethod: req.ssoConfig.authProvider,
isUserCompleted,
...(req.body.RelayState ? {
callbackPort: req.body.RelayState as string
callbackPort: JSON.parse(req.body.RelayState).callbackPort as string
} : {})
},
expiresIn: await getJwtProviderAuthLifetime(),

@ -1,6 +1,6 @@
import net from "net";
import { IPType } from "../../ee/models";
import { InternalServerError } from "../errors";
import { InternalServerError, UnauthorizedRequestError } from "../errors";
/**
* Return details of IP [ip]:
@ -98,4 +98,39 @@ export const isValidIpOrCidr = (ip: string): boolean => {
}
return false;
}
}
/**
* Validates the IP address [ipAddress] against the trusted IPs [trustedIps].
* @param {Object} obj
* @param {String} obj.ipAddress - IP address to check
* @param {Object[]} obj.trustedIps - IPs to trust in blocklist
*/
export const checkIPAgainstBlocklist = ({
ipAddress,
trustedIps
}: {
ipAddress: string;
trustedIps: {
ipAddress: string;
type: IPType;
prefix: number;
}[]
}) => {
const blockList = new net.BlockList();
for (const trustedIp of trustedIps) {
if (trustedIp.prefix !== undefined) {
blockList.addSubnet(trustedIp.ipAddress, trustedIp.prefix, trustedIp.type);
} else {
blockList.addAddress(trustedIp.ipAddress, trustedIp.type);
}
}
const { type } = extractIPDetails(ipAddress);
const check = blockList.check(ipAddress, type);
if (!check) throw UnauthorizedRequestError({
message: "Failed to authenticate"
});
}

@ -12,6 +12,27 @@ export enum LogLevel {
EMERGENCY = 600,
}
export const mapToWinstonLogLevel = (customLogLevel: LogLevel): string => {
switch (customLogLevel) {
case LogLevel.DEBUG:
return "debug";
case LogLevel.INFO:
return "info";
case LogLevel.NOTICE:
return "notice";
case LogLevel.WARNING:
return "warn";
case LogLevel.ERROR:
return "error";
case LogLevel.CRITICAL:
return "crit";
case LogLevel.ALERT:
return "alert";
case LogLevel.EMERGENCY:
return "emerg";
}
}
export type RequestErrorContext = {
logLevel?: LogLevel,
statusCode: number,
@ -87,7 +108,8 @@ export default class RequestError extends Error{
}, this.context)
//* Omit sensitive information from context that can leak internal workings of this program if user is not developer
if(!(await getVerboseErrorOutput())){
const verboseErrorOutput = await getVerboseErrorOutput();
if (verboseErrorOutput !== undefined) {
_context = this._omit(_context, [
"stacktrace",
"exception",
@ -110,4 +132,4 @@ export default class RequestError extends Error{
return formatObject
}
}
}

@ -4,7 +4,14 @@ import { Types } from "mongoose";
import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto";
import { EESecretService } from "../../ee/services";
import { redisClient } from "../../services/RedisService"
import { IPType, ISecretVersion, SecretSnapshot, SecretVersion, TrustedIP } from "../../ee/models";
import {
IPType,
ISecretVersion,
Role,
SecretSnapshot,
SecretVersion,
TrustedIP
} from "../../ee/models";
import {
AuthMethod,
BackupPrivateKey,
@ -34,14 +41,12 @@ import {
MEMBER,
OWNER
} from "../../variables";
import { InternalServerError } from "../errors";
import {
ProjectPermissionActions,
ProjectPermissionSub,
memberProjectPermissions
} from "../../ee/services/ProjectRoleService";
import Role from "../../ee/models/role";
/**
* Backfill secrets to ensure that they're all versioned and have

@ -55,9 +55,6 @@ export const setup = async () => {
// initializing global feature set
await EELicenseService.initGlobalFeatureSet();
// initializing the database connection
await DatabaseService.initDatabase(await getMongoURL());
await initializePassport();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY

@ -5,28 +5,30 @@ export const CreateFolderV1 = z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
folderName: z.string().trim(),
parentFolderId: z.string().trim().optional()
directory: z.string().trim().default("/")
})
});
export const UpdateFolderV1 = z.object({
params: z.object({
folderId: z.string().trim()
folderName: z.string().trim()
}),
body: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
name: z.string().trim()
name: z.string().trim(),
directory: z.string().trim().default("/")
})
});
export const DeleteFolderV1 = z.object({
params: z.object({
folderId: z.string().trim()
folderName: z.string().trim()
}),
body: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim()
environment: z.string().trim(),
directory: z.string().trim().default("/")
})
});
@ -34,7 +36,6 @@ export const GetFoldersV1 = z.object({
query: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
parentFolderId: z.string().trim().optional(),
parentFolderPath: z.string().trim().optional()
directory: z.string().trim().default("/")
})
});

@ -9,3 +9,4 @@ export * from "./organization";
export * from "./secrets";
export * from "./serviceAccount";
export * from "./serviceTokenData";
export * from "./serviceTokenDataV3";

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