Compare commits

..

217 Commits

Author SHA1 Message Date
285a6d633a return instance of ServiceTokenData instead of object 2023-08-01 14:40:08 -04:00
12b71bcf67 Merge pull request #816 from jamesoyanna/fix-blog-404-issue
fix: Blog Section in Doc API Reference Redirects to 404
2023-08-01 13:33:33 -04:00
6c0be52ffa upgrade major version of mongoose from v6 to v7 2023-08-01 13:27:01 -04:00
9df51424a2 upgrade major version of mongoose from v6 to v7 2023-08-01 13:24:38 -04:00
531938a3f1 fix: Blog Section in Doc API Reference Redirects to 404 2023-08-01 16:28:32 +01:00
941a8699b5 Merge pull request #806 from atimapreandrew/teamcity-integration
Teamcity integration
2023-08-01 19:04:20 +07:00
6e42da9063 Optimize TeamCity integration 2023-08-01 18:59:27 +07:00
69903c0d5c Merge pull request #799 from afrieirham/fix/digital-ocean-sync
fix: digital ocean reset settings on sync
2023-08-01 15:24:57 +07:00
8ff33a4e63 Fix sentence in security mechanics 2023-08-01 14:46:50 +07:00
1d71864092 hide sign up with inviteOnlySignup=true 2023-07-31 23:54:35 -04:00
4b1a27b301 Update changelog 2023-08-01 10:11:22 +07:00
b78150e78d Updated membership logic for SAML auth 2023-07-31 17:36:42 +07:00
a0f08c73af Added images for TeamCity docs 2023-07-30 13:13:52 +01:00
59ebe0c22e Merge pull request #805 from Infisical/jumpcloud-saml
Add JumpCloud SAML Support
2023-07-30 14:36:10 +07:00
6729caeb75 Add JumpCloud SAML 2023-07-30 14:29:47 +07:00
3543a15c09 TeamCity integration 2023-07-30 01:57:39 +01:00
33e0f13eea Added TeamCity integration docs 2023-07-30 00:26:06 +01:00
e9cff4fe69 TeamCity integration 2023-07-30 00:00:24 +01:00
26867f7328 Update overview.mdx 2023-07-29 15:16:11 -07:00
233459d063 Merge pull request #804 from Infisical/jumpcloud-saml
Optimize SAML SSO configuration flow and add documentation for Azure AD SAML
2023-07-29 15:01:41 +07:00
ba6355e4d2 Fix lint errors 2023-07-29 14:58:13 +07:00
e961a30937 Optimize SAML SSO configuration flow, add docs for Azure AD SAML 2023-07-29 14:39:06 +07:00
53ff420304 Merge pull request #802 from akhilmhdh/feat/org-overview-loading
feat: added loading state for org overview page
2023-07-28 15:13:51 -04:00
196a613f16 Merge pull request #790 from subh-cs/subh-cs/better-logs-k8-operator
Better log for k8-operator
2023-07-28 15:11:40 -04:00
cc4b749ce8 Revise SAML flow, update Okta SAML docs 2023-07-29 01:57:07 +07:00
8cc5f2ef43 typo in cli usage.mdx 2023-07-28 13:26:35 -04:00
06bc02c392 feat: added loading state and show empty state only when loading for org overview page 2023-07-28 21:33:31 +05:30
3682c4d044 Merge pull request #800 from akhilmhdh/fix/style-overview-fixes
feat: fixed padding, added progress bar for routing, added sticky hea…
2023-07-28 09:51:33 -04:00
52892c26e5 feat: fixed padding, added progress bar for routing, added sticky header for overview 2023-07-28 16:55:22 +05:30
5ce67bf750 fix: send current app settings with env sync update 2023-07-28 18:00:46 +08:00
ed2cf68935 Merge remote-tracking branch 'origin' into jumpcloud-saml 2023-07-28 14:52:28 +07:00
386bc09d49 Update Okta SSO image convention 2023-07-28 14:52:07 +07:00
353c6e9166 Merge pull request #798 from Infisical/windmill-docs
Add docs for Windmill integration
2023-07-28 13:26:20 +07:00
1f69467207 Add docs for Windmill integration 2023-07-28 13:24:33 +07:00
5ab218f1f8 fixed parsing .env with : 2023-07-27 19:40:39 -07:00
e1b25aaa54 fixed the padding issue for the secret raw 2023-07-27 19:03:02 -07:00
9193e7ef58 fix styling issues with secret rows 2023-07-27 15:50:42 -07:00
3f998296fe ip table fix 2023-07-27 13:33:31 -07:00
6f7601f2c4 Merge pull request #793 from akhilmhdh/feat/new-overview-page
Feat/new overview page
2023-07-27 15:42:34 -04:00
b7c7544baf minor style changes 2023-07-27 12:37:18 -07:00
4b7ae2477a Merge pull request #765 from sunilk4u/feat/windmill-integration
Feature: Windmill.dev cloud Integeration
2023-07-28 02:11:27 +07:00
e548883bba Fix lint errors, merge conflicts 2023-07-28 02:02:26 +07:00
a7ece1830e Revise Windmill integration 2023-07-28 01:30:28 +07:00
6502d232c9 Start Azure AD SAML docs 2023-07-27 23:48:53 +07:00
f31e8ddfe9 feat: added width for expandable table and secret missing count ui fix 2023-07-27 20:57:38 +05:30
7bbbdcc58b feat: implemented new overview page with improvement in dashboard 2023-07-27 16:36:34 +05:30
bca14dd5c4 feat: added new secret input component and updated toolbar key special prop to innerKey 2023-07-27 16:36:34 +05:30
b6b3c8a736 fix: resolved v2 secret update bug and object returning in import secret empty 2023-07-27 16:29:43 +05:30
d458bd7948 Merge branch 'feat/northflank-integration' 2023-07-27 15:18:28 +07:00
239989ceab Update contributors README 2023-07-27 15:17:15 +07:00
7ff13242c0 Add docs for Northflank 2023-07-27 15:16:11 +07:00
7db8555b65 Merge pull request #788 from ChukwunonsoFrank/feat/northflank-integration
Feature: Northflank integration
2023-07-27 15:15:36 +07:00
980a578bd5 Revise Northflank integration 2023-07-27 14:52:52 +07:00
adb27bb729 fix: allow apps which have write access 2023-07-27 13:11:48 +05:30
d89d360880 Merge pull request #792 from Infisical/fix-ip-whitelisting
Update IP allowlist implementation
2023-07-27 11:47:56 +07:00
8ed5dbb26a Add default IPV6 CIDR for creating workspace 2023-07-27 11:23:57 +07:00
221a43e8a4 Update IP allowlist implementation 2023-07-27 11:18:36 +07:00
e8a2575f7e logging workspaceId, tokenName from k8-operator 2023-07-27 09:10:08 +05:30
41c1828324 roll forward: disable IP white listing 2023-07-26 20:50:53 -04:00
c2c8cf90b7 Merge branch 'main' of https://github.com/Infisical/infisical 2023-07-26 14:03:47 -07:00
00b4d6bd45 changed the icon 2023-07-26 14:03:37 -07:00
f5a6270d2a add workspace auth for multi env/glob request 2023-07-26 16:50:35 -04:00
bc9d6253be change isDisabled criteria for Create Integration button 2023-07-26 21:19:02 +01:00
a5b37c80ad chore: resolve merge conflicts 2023-07-26 20:39:51 +01:00
7b1a4fa8e4 change regexp to accept deeper level paths 2023-07-27 00:48:17 +05:30
7457f573e9 add dash and underscores for secret pattern test 2023-07-26 23:43:44 +05:30
d67e96507a fix:unauthorized response for app name 2023-07-26 23:14:42 +05:30
46545c1462 add secretGroup to integrationController.ts 2023-07-26 18:19:54 +01:00
8331cd4de8 Merge pull request #761 from atimapreandrew/terraform-cloud-integration
Terraform cloud integration
2023-07-26 23:16:51 +07:00
3447074eb5 Fix merge conflicts 2023-07-26 23:13:33 +07:00
5a708ee931 Optimize Terraform Cloud sync function 2023-07-26 23:10:38 +07:00
9913b2fb6c Initialize TrustedIP upon creating a new workspace 2023-07-26 22:20:51 +07:00
2c021f852f Update filter for trusted IPs backfill 2023-07-26 21:15:07 +07:00
8dbc894ce9 Replace insertMany operation with upsert for backfilling trusted ips 2023-07-26 20:45:58 +07:00
511904605f Merge pull request #786 from Infisical/debug-integrations
Fix PATCH IP whitelist behavior and breaking integrations due to incorrect project id in local storage
2023-07-26 17:55:24 +07:00
7ae6d1610f Fix IP whitelist PATCH endpoint, update localStorage project id to reflect navigated to project 2023-07-26 17:46:33 +07:00
7da6d72f13 Remove save call from backfilling trusted ips 2023-07-26 16:18:13 +07:00
ad33356994 Remove required comment for trusted IP schema 2023-07-26 15:29:43 +07:00
cfa2461479 Merge pull request #785 from Infisical/network-access
Add support for IP allowlisting / trusted IPs
2023-07-26 15:11:04 +07:00
bf08bfacb5 Fix lint errors 2023-07-26 15:06:18 +07:00
cf77820059 Merge remote-tracking branch 'origin' into network-access 2023-07-26 14:55:12 +07:00
1ca90f56b8 Add docs for IP allowlisting 2023-07-26 14:51:25 +07:00
5899d7aee9 Complete trusted IPs feature 2023-07-26 13:34:56 +07:00
b565194c43 create versions for brew releases 2023-07-25 15:39:29 -04:00
86e04577c9 print exec error messages as is 2023-07-25 14:17:31 -04:00
f4b3cafc5b Added Terraform Cloud integration docs 2023-07-25 16:51:53 +01:00
18aad7d520 Terraform Cloud integration 2023-07-25 15:25:11 +01:00
54c79012db fix the org-members link 2023-07-25 07:01:27 -07:00
4b720bf940 Update kubernetes.mdx 2023-07-24 18:13:35 -04:00
993866bb8b Update secret-reference.mdx 2023-07-24 17:18:07 -04:00
8c39fa2438 add conditional imports to raw api 2023-07-24 15:01:31 -04:00
7bccfaefac Merge pull request #784 from akhilmhdh/fix/import-delete
fix: resolved secret import delete and include_import response control
2023-07-24 11:47:02 -04:00
e2b666345b fix: resolved secret import delete and include_import response control 2023-07-24 20:53:59 +05:30
90910819a3 Merge pull request #778 from afrieirham/docs/running-docs-locally
docs: add running infisical docs locally guide
2023-07-24 09:00:46 -04:00
8b070484dd docs: add running infisical docs locally 2023-07-24 20:36:42 +08:00
a764087c83 Merge pull request #782 from Infisical/improve-security-docs
Add section on service token best practices
2023-07-24 18:14:26 +07:00
27d5fa5aa0 Add section on service token best practices 2023-07-24 18:10:37 +07:00
2e7705999c Updated changelog and contributors in README 2023-07-24 12:56:02 +07:00
428bf8e252 Merge branch 'main' of https://github.com/Infisical/infisical 2023-07-23 13:59:42 -07:00
264740d84d style updates 2023-07-23 13:59:25 -07:00
723bcd4d83 Update react.mdx 2023-07-23 15:44:54 -04:00
9ed516ccb6 Uncomment Google SSO for signup 2023-07-24 02:35:51 +07:00
067ade94c8 Merge branch 'main' of https://github.com/Infisical/infisical 2023-07-24 01:54:09 +07:00
446edb6ed9 Add CLI support for SAML SSO 2023-07-24 01:53:56 +07:00
896529b7c6 auto scope raw secrets GET with service token 2023-07-23 12:31:03 -04:00
5c836d1c10 Merge pull request #779 from afrieirham/integration/digital-ocean-app-platform
Digital Ocean App Platform Integration
2023-07-23 23:01:35 +07:00
409d46aa10 Fix merge conflicts 2023-07-23 22:55:18 +07:00
682c63bc2a Fix DigitalOcean getApps case where there are no apps 2023-07-23 22:42:41 +07:00
1419371588 Merge pull request #776 from afrieirham/integration/cloud66
Cloud 66 integration
2023-07-23 22:19:23 +07:00
77fdb6307c Optimize Cloud66 integration sync function 2023-07-23 22:16:27 +07:00
c61bba2b6b docs: add digital ocean app platform integration guide 2023-07-23 22:39:08 +08:00
2dc0563042 view: fix integration name only show 3 words 2023-07-23 22:02:59 +08:00
b5fb2ef354 feat: DO app platform integration 2023-07-23 22:01:48 +08:00
dc01758946 Update SAML Okta docs screenshots 2023-07-23 16:59:08 +07:00
1f8683f59e Merge pull request #777 from Infisical/saml-docs
Add docs for Okta SAML 2.0 SSO
2023-07-23 16:49:47 +07:00
a5273cb86f Add docs for Okta SAML 2.0 SSO 2023-07-23 16:47:43 +07:00
d48b5157d4 docs: add cloud 66 integration guide 2023-07-23 17:39:29 +08:00
94a23bfa23 feat: add cloud 66 integration 2023-07-23 16:36:26 +08:00
fcdfa424bc Restrict changing user auth methods if SAML SSO is enforced 2023-07-23 15:19:17 +07:00
3fba1b3ff7 Merge pull request #774 from Infisical/saml-sso-edge-cases
Block inviting members to organization if SAML SSO is configured
2023-07-23 13:27:30 +07:00
953eed70b2 Add back attribution source for non-SAML SSO case 2023-07-23 13:24:12 +07:00
39ba795604 Block inviting members to organization if SAML SSO is configured 2023-07-23 13:05:37 +07:00
5b36227321 Merge pull request #773 from Infisical/debug-google-sso
Initialize organization bot upon creating organization
2023-07-23 12:07:45 +07:00
70d04be978 Initialize organization bot upon creating organization 2023-07-23 12:03:39 +07:00
c2be6674b1 chore: resolve merge conflicts 2023-07-22 11:29:40 +01:00
565f234921 Merge pull request #772 from Infisical/switch-to-google-sso
Add user support for changing authentication methods
2023-07-22 12:38:22 +07:00
ab43e32982 Add user support for changing auth methods 2023-07-22 12:33:57 +07:00
be677fd6c2 disable token error 2023-07-21 18:41:32 -04:00
3d93c6a995 add sentry error to integ 2023-07-21 17:45:27 -04:00
edb201e11f comment out unused import 2023-07-21 17:33:46 -04:00
1807b3e029 add logs for integration and comment out google sso 2023-07-21 17:29:56 -04:00
c02c8e67d3 Merge branch 'main' of https://github.com/Infisical/infisical 2023-07-21 23:54:03 +07:00
d4c5be5f48 Update file casing 2023-07-21 23:53:50 +07:00
5f33c9a389 Update file casing 2023-07-21 23:53:16 +07:00
c9acb22261 Merge pull request #770 from Infisical/docs
Add/revise docs for Codefresh and Bitbucket integrations
2023-07-21 23:44:57 +07:00
33f0510995 Add docs for Codefresh integration, revise docs for Bitbucket integration 2023-07-21 23:41:04 +07:00
25b239a18b Merge pull request #755 from zwkee/integration/bitbucket
BitBucket Integration
2023-07-21 20:55:42 +07:00
504e0f6dc3 Fix lint issues backend 2023-07-21 20:52:35 +07:00
f450be3a00 Fix merge conflicts 2023-07-21 20:49:41 +07:00
d9f6c27e4d Update Bitbucket sync function 2023-07-21 20:16:39 +07:00
9cef35e9e6 Merge pull request #769 from Infisical/saml
Add Google SSO and SAML SSO (Okta)
2023-07-21 18:02:10 +07:00
2621ccdcf1 Add descriptions for SSO endpoints 2023-07-21 17:59:15 +07:00
75e90201c0 Lint and move redirectSSO into controller 2023-07-21 17:54:09 +07:00
fd3cf70e13 Add Google SSO 2023-07-21 17:48:36 +07:00
44108621b4 Run linter, fix import error 2023-07-21 15:01:28 +07:00
5ee65359bf Fix merge conflicts 2023-07-21 14:37:13 +07:00
241dceb845 Remove bodyparser and audit fix deps 2023-07-21 13:39:07 +07:00
af650ef4c7 patch env delete bug 2023-07-20 20:03:01 -04:00
817ddd228c Update overview.mdx 2023-07-20 19:15:58 -04:00
15d81233b4 update docs overvew 2023-07-20 18:25:54 -04:00
705b1833d0 update CLI usage and docs for pinning docker 2023-07-20 18:18:43 -04:00
beb8d2634a add docs to pin cli 2023-07-20 17:59:59 -04:00
fb3ceb4581 Revamp docker docs 2023-07-20 17:28:33 -04:00
2df33dc84f Merge pull request #764 from akhilmhdh/fix/include-optional
made include_imports optional in raw secrets fetch
2023-07-20 09:54:47 -04:00
c62504d658 correct codefresh image file name 2023-07-20 19:21:04 +05:30
ce08512ab5 Merge remote-tracking branch 'upstream/main' into feat/windmill-integration 2023-07-20 19:20:38 +05:30
8abe7c7f99 add secretGroup attribute to model definition 2023-07-20 12:58:07 +01:00
043133444d fix: made include_imports optional in raw secrets fetch 2023-07-20 14:18:35 +05:30
b3baaac5c8 map secret comments to windmill api description 2023-07-20 12:57:16 +05:30
aa019e1501 add pattern match for windmill stored secrets 2023-07-20 02:12:36 +05:30
0f8b505c78 change label for windmill workspace form 2023-07-20 01:45:16 +05:30
5b7e23cdc5 add authorization of user for each app 2023-07-20 01:44:21 +05:30
df25657715 Merge pull request #760 from chisom5/feature-codefresh-integration
Codefresh integration
2023-07-20 00:14:27 +07:00
79c2baba1a Merge branch 'Infisical:main' into feature-codefresh-integration 2023-07-19 17:39:45 +01:00
52a2a782f1 Merge pull request #762 from akhilmhdh/fix/sec-import-fail
fix: resolved empty secrets on fresh env and added empty states
2023-07-19 12:38:01 -04:00
eda095b55f Fix merge conflicts 2023-07-19 23:29:01 +07:00
93761f6487 fix: resolved empty secrets on fresh env and added empty states 2023-07-19 21:58:03 +05:30
c5438fbe6d Fix merge conflicts 2023-07-19 23:25:52 +07:00
e8fdaf571c Make sync function for Codefresh 2023-07-19 23:17:59 +07:00
846e2e037f Update 2023-07-19 22:23:48 +07:00
a0a7ff8715 Codefresh integration
Worked on codefresh integration syncing secrets to infiscial
2023-07-19 16:22:25 +01:00
ec1e842202 change windmill workspace label 2023-07-19 19:04:59 +05:30
83d5291998 add interface for windmill request body 2023-07-19 15:00:42 +05:30
638e011cc0 add windmill logo to integration variable 2023-07-19 14:47:37 +05:30
d2d23a7aba add windmill logo 2023-07-19 14:47:15 +05:30
a52c2f03bf add integration slug name mapping for windmill 2023-07-19 14:12:05 +05:30
51c12e0202 Merge branch 'Infisical:main' into feat/windmill-integration 2023-07-19 13:15:21 +05:30
4db7b0c05e add function for windmill secret sync 2023-07-19 13:13:14 +05:30
284608762b update secret import docs 2023-07-19 00:57:35 -04:00
8960773150 Update overview.mdx 2023-07-18 21:51:18 -07:00
4684c9f8b1 Update secret-reference.mdx 2023-07-19 00:40:32 -04:00
abbf3e80f9 Update secret-reference.mdx 2023-07-19 00:31:45 -04:00
d272f580cf update k8 helm for import feature 2023-07-19 00:25:22 -04:00
da9cb70184 only send risk notif when risks are found 2023-07-19 00:05:19 -04:00
1f3f0375b9 add secret import to k8 operator 2023-07-18 23:59:03 -04:00
8ad851d4b0 added the ability to change user name 2023-07-18 18:36:34 -07:00
edef22d28e Terraform Cloud integration 2023-07-18 23:14:41 +01:00
3b5bc151ba Merge pull request #758 from akhilmhdh/feat/secret-import
Implemented secret link/import feature
2023-07-18 16:58:46 -04:00
678cdd3308 Merge branch 'main' into feat/secret-import 2023-07-18 16:52:25 -04:00
76f43ab6b4 Terraform Cloud integration 2023-07-18 21:08:30 +01:00
33554f4057 patch bug when imports don't show with no secrets 2023-07-18 15:32:14 -04:00
c539d4d243 remove print 2023-07-18 15:31:31 -04:00
124e6dd998 feat(secret-import): added workspace validation for get imports and imported secret api 2023-07-18 19:40:35 +05:30
cef29f5dd7 minor style update 2023-07-17 21:39:05 -07:00
95c914631a patch notify user on risk found 2023-07-17 21:52:24 -04:00
49ae61da08 remove border from risk selection 2023-07-17 21:49:58 -04:00
993abd0921 add secret scanning status to api 2023-07-17 21:28:47 -04:00
6ee7081640 add secret groups field functionality 2023-07-17 22:00:48 +01:00
f681f0a98d fix(secret-import): resolved build failure in frontend 2023-07-18 00:29:42 +05:30
23cd6fd861 doc(secret-imports): updated docs for secret import 2023-07-17 23:10:42 +05:30
cf45c3dc8b feat(secret-import): updated cli to support secret import 2023-07-17 23:10:14 +05:30
45584e0c1a feat(secret-import): implemented ui for secret import 2023-07-17 23:08:57 +05:30
202900a7a3 feat(secret-import): implemented api for secret import 2023-07-17 23:08:42 +05:30
04611d980b create windmill get all workspaces list function 2023-07-17 16:50:27 +05:30
6125246794 add integration authorize redirect url 2023-07-17 16:35:11 +05:30
52e26fc6fa create integration pages for windmill 2023-07-17 16:34:39 +05:30
06bd98bf56 add windmill variables to model schema 2023-07-17 15:12:12 +05:30
7c24e0181a add windmill variables to integration 2023-07-17 15:09:15 +05:30
ceeebc24fa Terraform Cloud integration 2023-07-16 21:12:35 +01:00
df7ad9e645 feat(integration): add integration with BitBucket 2023-07-16 22:04:51 +08:00
112d4ec9c0 refactor: modify Northflank integration sync logic 2023-07-12 12:25:44 +01:00
a3836b970a Terraform Cloud integration 2023-07-10 23:44:55 +01:00
5e2b31cb6c add window redirect for the Northflank integration 2023-07-10 12:57:16 +01:00
3c45941474 chore: resolve merge conflicts 2023-07-09 17:38:45 +01:00
91e172fd79 add Northflank specific create.tsx file 2023-07-09 16:18:58 +01:00
3e975dc4f0 Terraform Cloud integration 2023-07-08 00:07:38 +01:00
d9ab38c590 chore: resolve merge conflicts 2023-07-04 22:52:23 +01:00
413 changed files with 18568 additions and 7599 deletions

View File

@ -47,11 +47,13 @@ CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB=
CLIENT_ID_GITLAB=
CLIENT_ID_BITBUCKET=
CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB=
CLIENT_SECRET_GITLAB=
CLIENT_SECRET_BITBUCKET=
CLIENT_SLUG_VERCEL=
# Sentry (optional) for monitoring errors

View File

@ -108,6 +108,22 @@ brews:
zsh_completion.install "completions/infisical.zsh" => "_infisical"
fish_completion.install "completions/infisical.fish"
man1.install "manpages/infisical.1.gz"
- name: 'infisical@{{.Version}}'
tap:
owner: Infisical
name: homebrew-get-cli
commit_author:
name: "Infisical"
email: ai@infisical.com
folder: Formula
homepage: "https://infisical.com"
description: "The official Infisical CLI"
install: |-
bin.install "infisical"
bash_completion.install "completions/infisical.bash" => "infisical"
zsh_completion.install "completions/infisical.zsh" => "_infisical"
fish_completion.install "completions/infisical.fish"
man1.install "manpages/infisical.1.gz"
nfpms:
- id: infisical

View File

@ -1,4 +1,3 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,8 @@
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-empty-function": "off",
"@typescript-eslint/no-empty-function": "off",
"no-console": 2,
"quotes": [
"error",
@ -24,7 +26,6 @@
],
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"@typescript-eslint/no-empty-function": "off",
"unused-imports/no-unused-vars": [
"warn",
{

4881
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.319.0",
"@godaddy/terminus": "^4.12.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.49.0",
"@sentry/tracing": "^7.48.0",
@ -29,7 +30,8 @@
"jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.10.5",
"mongodb": "^5.7.0",
"mongoose": "^7.4.1",
"nanoid": "^3.3.6",
"node-cache": "^5.1.2",
"nodemailer": "^6.8.0",

View File

@ -37,6 +37,7 @@ export const getClientIdNetlify = async () => (await client.getSecret("CLIENT_ID
export const getClientIdGitHub = async () => (await client.getSecret("CLIENT_ID_GITHUB")).secretValue;
export const getClientIdGitLab = async () => (await client.getSecret("CLIENT_ID_GITLAB")).secretValue;
export const getClientIdGoogle = async () => (await client.getSecret("CLIENT_ID_GOOGLE")).secretValue;
export const getClientIdBitBucket = async () => (await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
export const getClientSecretAzure = async () => (await client.getSecret("CLIENT_SECRET_AZURE")).secretValue;
export const getClientSecretHeroku = async () => (await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue;
export const getClientSecretVercel = async () => (await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue;
@ -44,6 +45,7 @@ export const getClientSecretNetlify = async () => (await client.getSecret("CLIEN
export const getClientSecretGitHub = async () => (await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue;
export const getClientSecretGitLab = async () => (await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue;
export const getClientSecretGoogle = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE")).secretValue;
export const getClientSecretBitBucket = async () => (await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
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";

View File

@ -15,6 +15,7 @@ import * as userController from "./userController";
import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController";
import * as secretImportController from "./secretImportController";
export {
authController,
@ -33,5 +34,6 @@ export {
userController,
workspaceController,
secretScanningController,
webhookController
webhookController,
secretImportController
};

View File

@ -7,6 +7,8 @@ import { IntegrationService } from "../../services";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_NORTHFLANK_API_URL,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_SET,
INTEGRATION_VERCEL_API_URL,
@ -141,12 +143,14 @@ export const saveIntegrationAccessToken = async (req: Request, res: Response) =>
*/
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
const teamId = req.query.teamId as string;
const workspaceSlug = req.query.workspaceSlug as string;
const apps = await getApps({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
accessId: req.accessId,
...(teamId && { teamId })
...(teamId && { teamId }),
...(workspaceSlug && { workspaceSlug })
});
return res.status(200).send({
@ -382,6 +386,139 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo
});
};
/**
* Return list of workspaces allowed for Bitbucket integration
* @param req
* @param res
* @returns
*/
export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: Response) => {
interface WorkspaceResponse {
size: number;
page: number;
pageLen: number;
next: string;
previous: string;
values: Array<Workspace>;
}
interface Workspace {
type: string;
uuid: string;
name: string;
slug: string;
is_private: boolean;
created_on: string;
updated_on: string;
}
const workspaces: Workspace[] = [];
let hasNextPage = true;
let workspaceUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/workspaces`
while (hasNextPage) {
const { data }: { data: WorkspaceResponse } = await standardRequest.get(
workspaceUrl,
{
headers: {
Authorization: `Bearer ${req.accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
if (data?.values.length > 0) {
data.values.forEach((workspace) => {
workspaces.push(workspace)
})
}
if (data.next) {
workspaceUrl = data.next
} else {
hasNextPage = false
}
}
return res.status(200).send({
workspaces
});
};
/**
* Return list of secret groups for Northflank project with id [appId]
* @param req
* @param res
* @returns
*/
export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res: Response) => {
const appId = req.query.appId as string;
interface NorthflankSecretGroup {
id: string;
name: string;
description: string;
priority: number;
projectId: string;
}
interface SecretGroup {
name: string;
groupId: string;
}
const secretGroups: SecretGroup[] = [];
if (appId && appId !== "") {
let page = 1;
const perPage = 10;
let hasMorePages = true;
while(hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage),
filter: "all",
});
const {
data: {
data: {
secrets
}
}
} = await standardRequest.get<{ data: { secrets: NorthflankSecretGroup[] }}>(
`${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${appId}/secrets`,
{
params,
headers: {
Authorization: `Bearer ${req.accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
secrets.forEach((a: any) => {
secretGroups.push({
name: a.name,
groupId: a.id
});
});
if (secrets.length < perPage) {
hasMorePages = false;
}
page++;
}
}
return res.status(200).send({
secretGroups
});
}
/**
* Delete integration authorization with id [integrationAuthId]
* @param req
@ -398,3 +535,4 @@ export const deleteIntegrationAuth = async (req: Request, res: Response) => {
integrationAuth
});
};

View File

@ -2,7 +2,7 @@ import { Request, Response } from "express";
import { Types } from "mongoose";
import { Integration } from "../../models";
import { EventService } from "../../services";
import { eventPushSecrets, eventStartIntegration } from "../../events";
import { eventStartIntegration } from "../../events";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";

View File

@ -1,6 +1,7 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import { MembershipOrg, Organization, User } from "../../models";
import { SSOConfig } from "../../ee/models";
import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg";
import { createToken } from "../../helpers/auth";
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
@ -110,6 +111,18 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
}
const plan = await EELicenseService.getPlan(organizationId);
const ssoConfig = await SSOConfig.findOne({
organization: new Types.ObjectId(organizationId)
});
if (ssoConfig && ssoConfig.isActive) {
// case: SAML SSO is enabled for the organization
return res.status(400).send({
message:
"Failed to invite member due to SAML SSO configured for organization"
});
}
if (plan.memberLimit !== null) {
// case: limit imposed on number of members allowed

View File

@ -9,7 +9,7 @@ import {
import { createOrganization as create } from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { ACCEPTED, OWNER } from "../../variables";
import { getSiteURL, getLicenseServerUrl } from "../../config";
import { getLicenseServerUrl, getSiteURL } from "../../config";
import { licenseServerKeyRequest } from "../../config/request";
export const getOrganizations = async (req: Request, res: Response) => {

View File

@ -0,0 +1,116 @@
import { Request, Response } from "express";
import { validateMembership } from "../../helpers";
import SecretImport from "../../models/secretImports";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { BadRequestError } from "../../utils/errors";
import { ADMIN, MEMBER } from "../../variables";
export const createSecretImport = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId, secretImport } = req.body;
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
const doc = new SecretImport({
workspace: workspaceId,
environment,
folderId,
imports: [{ environment: secretImport.environment, secretPath: secretImport.secretPath }]
});
await doc.save();
return res.status(200).json({ message: "successfully created secret import" });
}
const doesImportExist = importSecDoc.imports.find(
(el) => el.environment === secretImport.environment && el.secretPath === secretImport.secretPath
);
if (doesImportExist) {
throw BadRequestError({ message: "Secret import already exist" });
}
importSecDoc.imports.push({
environment: secretImport.environment,
secretPath: secretImport.secretPath
});
await importSecDoc.save();
return res.status(200).json({ message: "successfully created secret import" });
};
// to keep the ordering, you must pass all the imports in here not the only updated one
// this is because the order decide which import gets overriden
export const updateSecretImport = async (req: Request, res: Response) => {
const { id } = req.params;
const { secretImports } = req.body;
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: importSecDoc.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
importSecDoc.imports = secretImports;
await importSecDoc.save();
return res.status(200).json({ message: "successfully updated secret import" });
};
export const deleteSecretImport = async (req: Request, res: Response) => {
const { id } = req.params;
const { secretImportEnv, secretImportPath } = req.body;
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: importSecDoc.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
importSecDoc.imports = importSecDoc.imports.filter(
({ environment, secretPath }) =>
!(environment === secretImportEnv && secretPath === secretImportPath)
);
await importSecDoc.save();
return res.status(200).json({ message: "successfully delete secret import" });
};
export const getSecretImports = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId } = req.query;
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secretImport: {} });
}
return res.status(200).json({ secretImport: importSecDoc });
};
export const getAllSecretsFromImport = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId } = req.query as {
workspaceId: string;
environment: string;
folderId: string;
};
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secrets: [] });
}
const secrets = await getAllImportedSecrets(workspaceId, environment, folderId);
return res.status(200).json({ secrets });
};

View File

@ -72,7 +72,8 @@ export const deleteWebhook = async (req: Request, res: Response) => {
workspaceId: webhook.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
await webhook.remove();
await webhook.deleteOne();
return res.status(200).send({
message: "successfully removed webhook"

View File

@ -27,16 +27,16 @@ export const createWorkspaceEnvironment = async (
const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body;
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) throw WorkspaceNotFoundError();
const plan = await EELicenseService.getPlan(workspace.organization.toString());
if (plan.environmentLimit !== null) {
// case: limit imposed on number of environments allowed
if (workspace.environments.length >= plan.environmentLimit) {
// case: number of environments used exceeds the number of environments allowed
return res.status(400).send({
message: "Failed to create environment due to environment limit reached. Upgrade plan to create more environments.",
});
@ -191,14 +191,21 @@ export const deleteWorkspaceEnvironment = async (
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceToken.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceTokenData.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
// await ServiceToken.deleteMany({
// workspace: workspaceId,
// environment: environmentSlug,
// });
const result = await ServiceTokenData.updateMany(
{ workspace: workspaceId },
{ $pull: { scopes: { environment: environmentSlug } } }
);
if (result.modifiedCount > 0) {
await ServiceTokenData.deleteMany({ workspace: workspaceId, scopes: { $size: 0 } });
}
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug,

View File

@ -9,8 +9,6 @@ import {
} from "../../types/secret";
const { ValidationError } = mongoose.Error;
import {
BadRequestError,
InternalServerError,
ValidationError as RouteValidationError,
UnauthorizedRequestError
} from "../../utils/errors";
@ -311,16 +309,16 @@ export const updateSecret = async (req: Request, res: Response) => {
{ _id: secretModificationsRequested._id, workspace: workspaceId },
{ $inc: { version: 1 }, $set: sanitizedSecret }
)
.catch((error) => {
if (error instanceof ValidationError) {
throw RouteValidationError({
message: "Unable to apply modifications, please try again",
stack: error.stack
});
}
.catch((error) => {
if (error instanceof ValidationError) {
throw RouteValidationError({
message: "Unable to apply modifications, please try again",
stack: error.stack
});
}
throw error;
});
throw error;
});
if (postHogClient) {
postHogClient.capture({
@ -372,12 +370,12 @@ export const getSecrets = async (req: Request, res: Response) => {
$or: [{ user: userId }, { user: { $exists: false } }],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
})
.catch((err) => {
throw RouteValidationError({
message: "Failed to get secrets, please try again",
stack: err.stack
});
})
.catch((err) => {
throw RouteValidationError({
message: "Failed to get secrets, please try again",
stack: err.stack
});
})
if (postHogClient) {
postHogClient.capture({

View File

@ -35,6 +35,7 @@ import {
} from "../../services/FolderService";
import { isValidScope } from "../../helpers/secrets";
import path from "path";
import { getAllImportedSecrets } from "../../services/SecretImportService";
/**
* Peform a batch of any specified CUD secret operations
@ -690,7 +691,7 @@ export const getSecrets = async (req: Request, res: Response) => {
}
*/
const { tagSlugs, secretPath } = req.query;
const { tagSlugs, secretPath, include_imports } = req.query;
let { folderId } = req.query;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
@ -827,6 +828,12 @@ export const getSecrets = async (req: Request, res: Response) => {
secrets = await Secret.find(secretQuery).populate("tags");
}
// TODO(akhilmhdh) - secret-imp change this to org type
let importedSecrets: any[] = [];
if (include_imports === "true") {
importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string);
}
const channel = getChannelFromUserAgent(req.headers["user-agent"]);
const readAction = await EELogService.createAction({
@ -868,7 +875,8 @@ export const getSecrets = async (req: Request, res: Response) => {
}
return res.status(200).send({
secrets
secrets,
...(include_imports && { imports: importedSecrets })
});
};

View File

@ -2,7 +2,7 @@ import { Request, Response } from "express";
import { Types } from "mongoose";
import crypto from "crypto";
import bcrypt from "bcrypt";
import {
import {
ServiceAccount,
ServiceAccountKey,
ServiceAccountOrganizationPermission,
@ -21,11 +21,11 @@ import { getSaltRounds } from "../../config";
*/
export const getCurrentServiceAccount = async (req: Request, res: Response) => {
const serviceAccount = await ServiceAccount.findById(req.serviceAccount._id);
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: "Failed to find service account" });
}
return res.status(200).send({
serviceAccount,
});
@ -38,13 +38,13 @@ export const getCurrentServiceAccount = async (req: Request, res: Response) => {
*/
export const getServiceAccountById = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: "Failed to find service account" });
}
return res.status(200).send({
serviceAccount,
});
@ -73,7 +73,7 @@ export const createServiceAccount = async (req: Request, res: Response) => {
const secret = crypto.randomBytes(16).toString("base64");
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
// create service account
const serviceAccount = await new ServiceAccount({
name,
@ -83,17 +83,17 @@ export const createServiceAccount = async (req: Request, res: Response) => {
lastUsed: new Date(),
expiresAt,
secretHash,
}).save();
}).save()
const serviceAccountObj = serviceAccount.toObject();
delete serviceAccountObj.secretHash;
delete (serviceAccountObj as any).secretHash;
// provision default org-level permission for service account
await new ServiceAccountOrganizationPermission({
serviceAccount: serviceAccount._id,
}).save();
const secretId = Buffer.from(serviceAccount._id.toString(), "hex").toString("base64");
return res.status(200).send({
@ -111,7 +111,7 @@ export const createServiceAccount = async (req: Request, res: Response) => {
export const changeServiceAccountName = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const { name } = req.body;
const serviceAccount = await ServiceAccount.findOneAndUpdate(
{
_id: new Types.ObjectId(serviceAccountId),
@ -123,7 +123,7 @@ export const changeServiceAccountName = async (req: Request, res: Response) => {
new: true,
}
);
return res.status(200).send({
serviceAccount,
});
@ -142,7 +142,7 @@ export const addServiceAccountKey = async (req: Request, res: Response) => {
encryptedKey,
nonce,
} = req.body;
const serviceAccountKey = await new ServiceAccountKey({
encryptedKey,
nonce,
@ -163,7 +163,7 @@ export const getServiceAccountWorkspacePermissions = async (req: Request, res: R
const serviceAccountWorkspacePermissions = await ServiceAccountWorkspacePermission.find({
serviceAccount: req.serviceAccount._id,
}).populate("workspace");
return res.status(200).send({
serviceAccountWorkspacePermissions,
});
@ -184,19 +184,19 @@ export const addServiceAccountWorkspacePermission = async (req: Request, res: Re
encryptedKey,
nonce,
} = req.body;
if (!req.membership.workspace.environments.some((e: { name: string; slug: string }) => e.slug === environment)) {
return res.status(400).send({
message: "Failed to validate workspace environment",
});
}
const existingPermission = await ServiceAccountWorkspacePermission.findOne({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId),
environment,
});
if (existingPermission) throw BadRequestError({ message: "Failed to add workspace permission to service account due to already-existing " });
const serviceAccountWorkspacePermission = await new ServiceAccountWorkspacePermission({
@ -206,12 +206,12 @@ export const addServiceAccountWorkspacePermission = async (req: Request, res: Re
read,
write,
}).save();
const existingServiceAccountKey = await ServiceAccountKey.findOne({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId),
workspace: new Types.ObjectId(workspaceId),
});
if (!existingServiceAccountKey) {
await new ServiceAccountKey({
encryptedKey,
@ -242,7 +242,7 @@ export const deleteServiceAccountWorkspacePermission = async (req: Request, res:
serviceAccount,
workspace,
});
if (count === 0) {
await ServiceAccountKey.findOneAndDelete({
serviceAccount,
@ -294,12 +294,12 @@ export const deleteServiceAccount = async (req: Request, res: Response) => {
*/
export const getServiceAccountKeys = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const serviceAccountKeys = await ServiceAccountKey.find({
serviceAccount: req.serviceAccount._id,
...(workspaceId ? { workspace: new Types.ObjectId(workspaceId) } : {}),
});
return res.status(200).send({
serviceAccountKeys,
});

View File

@ -5,8 +5,6 @@ import { ServiceAccount, ServiceTokenData, User } from "../../models";
import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT } from "../../variables";
import { getSaltRounds } from "../../config";
import { BadRequestError } from "../../utils/errors";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
/**
* Return service token data associated with service token on request

View File

@ -18,7 +18,7 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
* @returns
*/
export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token, refreshToken;
let user;
const {
email,
firstName,
@ -119,7 +119,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
userAgent: req.headers["user-agent"] ?? "",
});
token = tokens.token;
const token = tokens.token;
// sending a welcome email to new users
if (await getLoopsApiKey()) {
@ -159,7 +159,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
* @returns
*/
export const completeAccountInvite = async (req: Request, res: Response) => {
let user, token, refreshToken;
let user;
const {
email,
firstName,
@ -244,7 +244,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
userAgent: req.headers["user-agent"] ?? "",
});
token = tokens.token;
const token = tokens.token;
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {

View File

@ -3,10 +3,11 @@ import { Types } from "mongoose";
import crypto from "crypto";
import bcrypt from "bcrypt";
import {
MembershipOrg,
User,
APIKeyData,
TokenVersion
AuthProvider,
MembershipOrg,
TokenVersion,
User
} from "../../models";
import { getSaltRounds } from "../../config";
@ -80,6 +81,67 @@ export const updateMyMfaEnabled = async (req: Request, res: Response) => {
});
}
/**
* Update name of the current user to [firstName, lastName].
* @param req
* @param res
* @returns
*/
export const updateName = async (req: Request, res: Response) => {
const {
firstName,
lastName
}: {
firstName: string;
lastName: string;
} = req.body;
const user = await User.findByIdAndUpdate(
req.user._id.toString(),
{
firstName,
lastName: lastName ?? ""
},
{
new: true
}
);
return res.status(200).send({
user,
});
}
/**
* Update auth provider of the current user to [authProvider]
* @param req
* @param res
* @returns
*/
export const updateAuthProvider = async (req: Request, res: Response) => {
const {
authProvider
} = req.body;
if (req.user?.authProvider === AuthProvider.OKTA_SAML) return res.status(400).send({
message: "Failed to update user authentication method because SAML SSO is enforced"
});
const user = await User.findByIdAndUpdate(
req.user._id.toString(),
{
authProvider
},
{
new: true
}
);
return res.status(200).send({
user
});
}
/**
* Return organizations that the current user is part of.
* @param req

View File

@ -179,10 +179,9 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
}
}
*/
let key;
const { workspaceId } = req.params;
key = await Key.findOne({
const key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
}).populate("sender", "+publicKey");

View File

@ -56,7 +56,7 @@ export const login1 = async (req: Request, res: Response) => {
if (!user) throw new Error("Failed to find user");
if (user.authProvider) {
if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) {
await validateProviderAuthToken({
email,
user,
@ -106,7 +106,6 @@ export const login1 = async (req: Request, res: Response) => {
*/
export const login2 = async (req: Request, res: Response) => {
try {
if (!req.headers["user-agent"]) throw InternalServerError({ message: "User-Agent header is required" });
const { email, clientProof, providerAuthToken } = req.body;
@ -117,7 +116,7 @@ export const login2 = async (req: Request, res: Response) => {
if (!user) throw new Error("Failed to find user");
if (user.authProvider) {
if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) {
await validateProviderAuthToken({
email,
user,
@ -189,7 +188,7 @@ export const login2 = async (req: Request, res: Response) => {
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
});
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,

View File

@ -3,8 +3,15 @@ import { Types } from "mongoose";
import { EventService, SecretService } from "../../services";
import { eventPushSecrets } from "../../events";
import { BotService } from "../../services";
import { repackageSecretToRaw } from "../../helpers/secrets";
import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { IServiceTokenData } from "../../models";
import { requireWorkspaceAuth } from "../../middleware";
import { ADMIN, MEMBER, PERMISSION_READ_SECRETS } from "../../variables";
/**
* Return secrets for workspace with id [workspaceId] and environment
@ -13,9 +20,29 @@ import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
* @param res
*/
export const getSecretsRaw = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
let workspaceId = req.query.workspaceId as string;
let environment = req.query.environment as string;
let secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
// if the service token has single scope, it will get all secrets for that scope by default
const serviceTokenDetails: IServiceTokenData = req?.serviceTokenData;
if (serviceTokenDetails && serviceTokenDetails.scopes.length == 1 && !containsGlobPatterns(serviceTokenDetails.scopes[0].secretPath)) {
const scope = serviceTokenDetails.scopes[0];
secretPath = scope.secretPath;
environment = scope.environment;
workspaceId = serviceTokenDetails.workspace.toString();
} else {
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query",
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true
});
}
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
@ -28,13 +55,38 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId)
});
if (includeImports === "true") {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root";
// if folder exist get it and replace folderid with new one
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath as string);
if (!folder) {
throw BadRequestError({ message: "Folder not found" });
}
folderId = folder.id;
}
const importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId);
return res.status(200).send({
secrets: secrets.map((secret) =>
repackageSecretToRaw({
secret,
key
})
),
imports: importedSecrets.map((el) => ({
...el,
secrets: el.secrets.map((secret) => repackageSecretToRaw({ secret, key }))
}))
});
}
return res.status(200).send({
secrets: secrets.map((secret) => {
const rep = repackageSecretToRaw({
secret,
key
});
return rep;
})
});
@ -232,6 +284,7 @@ export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
@ -240,6 +293,24 @@ export const getSecrets = async (req: Request, res: Response) => {
authData: req.authData
});
if (includeImports === "true") {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root";
// if folder exist get it and replace folderid with new one
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath as string);
if (!folder) {
throw BadRequestError({ message: "Folder not found" });
}
folderId = folder.id;
}
const importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId);
return res.status(200).send({
secrets,
imports: importedSecrets
});
}
return res.status(200).send({
secrets
});

View File

@ -12,6 +12,7 @@ import { standardRequest } from "../../config/request";
import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config";
import { BadRequestError } from "../../utils/errors";
import { TelemetryService } from "../../services";
import { AuthProvider } from "../../models";
/**
* Complete setting up user by adding their personal and auth information as part of the
@ -116,11 +117,13 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
if (!user)
throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
// initialize default organization and workspace
await initializeDefaultOrg({
organizationName,
user,
});
if (user.authProvider !== AuthProvider.OKTA_SAML) {
// initialize default organization and workspace
await initializeDefaultOrg({
organizationName,
user,
});
}
// update organization membership statuses that are
// invited to completed with user attached
@ -174,7 +177,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
distinctId: email,
properties: {
email,
attributionSource,
...(attributionSource ? { attributionSource } : {})
},
});
}

View File

@ -1,6 +1,8 @@
import * as secretController from "./secretController";
import * as secretSnapshotController from "./secretSnapshotController";
import * as organizationsController from "./organizationsController";
import * as ssoController from "./ssoController";
import * as usersController from "./usersController";
import * as workspaceController from "./workspaceController";
import * as actionController from "./actionController";
import * as membershipController from "./membershipController";
@ -10,6 +12,8 @@ export {
secretController,
secretSnapshotController,
organizationsController,
ssoController,
usersController,
workspaceController,
actionController,
membershipController,

View File

@ -178,6 +178,12 @@ export const addOrganizationTaxId = async (req: Request, res: Response) => {
return res.status(200).send(data);
}
/**
* Delete tax id with id [taxId] from organization tax ids on file
* @param req
* @param res
* @returns
*/
export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
const { taxId } = req.params;
@ -188,6 +194,12 @@ export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
return res.status(200).send(data);
}
/**
* Return organization's invoices on file
* @param req
* @param res
* @returns
*/
export const getOrganizationInvoices = async (req: Request, res: Response) => {
const { data: { invoices } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices`

View File

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

View File

@ -0,0 +1,13 @@
import { Request, Response } from "express";
/**
* Return the ip address of the current user
* @param req
* @param res
* @returns
*/
export const getMyIp = (req: Request, res: Response) => {
return res.status(200).send({
ip: req.authData.authIP
});
}

View File

@ -3,16 +3,20 @@ import { PipelineStage, Types } from "mongoose";
import { Secret } from "../../../models";
import {
FolderVersion,
IPType,
ISecretVersion,
Log,
SecretSnapshot,
SecretVersion,
TFolderRootVersionSchema,
TrustedIP
} from "../../models";
import { EESecretService } from "../../services";
import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
import Folder, { TFolderSchema } from "../../../models/folder";
import { searchByFolderId } from "../../../services/FolderService";
import { EELicenseService } from "../../services";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
/**
* Return secret snapshots for workspace with id [workspaceId]
@ -588,3 +592,147 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
logs,
});
};
/**
* Return trusted ips for workspace with id [workspaceId]
* @param req
* @param res
*/
export const getWorkspaceTrustedIps = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const trustedIps = await TrustedIP.find({
workspace: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
trustedIps
});
}
/**
* Add a trusted ip to workspace with id [workspaceId]
* @param req
* @param res
*/
export const addWorkspaceTrustedIp = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
ipAddress: ip,
comment,
isActive
} = req.body;
const plan = await EELicenseService.getPlan(req.workspace.organization.toString());
if (!plan.ipAllowlisting) return res.status(400).send({
message: "Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range."
});
const isValidIPOrCidr = isValidIpOrCidr(ip);
if (!isValidIPOrCidr) return res.status(400).send({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
const { ipAddress, type, prefix } = extractIPDetails(ip);
const trustedIp = await new TrustedIP({
workspace: new Types.ObjectId(workspaceId),
ipAddress,
type,
prefix,
isActive,
comment,
}).save();
return res.status(200).send({
trustedIp
});
}
/**
* Update trusted ip with id [trustedIpId] workspace with id [workspaceId]
* @param req
* @param res
*/
export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => {
const { workspaceId, trustedIpId } = req.params;
const {
ipAddress: ip,
comment
} = req.body;
const plan = await EELicenseService.getPlan(req.workspace.organization.toString());
if (!plan.ipAllowlisting) return res.status(400).send({
message: "Failed to update IP access range due to plan restriction. Upgrade plan to update IP access range."
});
const isValidIPOrCidr = isValidIpOrCidr(ip);
if (!isValidIPOrCidr) return res.status(400).send({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
const { ipAddress, type, prefix } = extractIPDetails(ip);
const updateObject: {
ipAddress: string;
type: IPType;
comment: string;
prefix?: number;
$unset?: {
prefix: number;
}
} = {
ipAddress,
type,
comment
};
if (prefix !== undefined) {
updateObject.prefix = prefix;
} else {
updateObject.$unset = { prefix: 1 };
}
const trustedIp = await TrustedIP.findOneAndUpdate(
{
_id: new Types.ObjectId(trustedIpId),
workspace: new Types.ObjectId(workspaceId),
},
updateObject,
{
new: true
}
);
return res.status(200).send({
trustedIp
});
}
/**
* Delete IP access range from workspace with id [workspaceId]
* @param req
* @param res
*/
export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => {
const { workspaceId, trustedIpId } = req.params;
const plan = await EELicenseService.getPlan(req.workspace.organization.toString());
if (!plan.ipAllowlisting) return res.status(400).send({
message: "Failed to delete IP access range due to plan restriction. Upgrade plan to delete IP access range."
});
const trustedIp = await TrustedIP.findOneAndDelete({
_id: new Types.ObjectId(trustedIpId),
workspace: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
trustedIp
});
}

View File

@ -0,0 +1,64 @@
import { Types } from "mongoose";
import {
SSOConfig
} from "../models";
import {
BotOrgService
} from "../../services";
import { client } from "../../config";
import { ValidationError } from "../../utils/errors";
export const getSSOConfigHelper = async ({
organizationId,
ssoConfigId
}: {
organizationId?: Types.ObjectId;
ssoConfigId?: Types.ObjectId;
}) => {
if (!organizationId && !ssoConfigId) throw ValidationError({
message: "Getting SSO data requires either id of organization or SSO data"
});
const ssoConfig = await SSOConfig.findOne({
...(organizationId ? { organization: organizationId } : {}),
...(ssoConfigId ? { _id: ssoConfigId } : {})
});
if (!ssoConfig) throw new Error("Failed to find organization SSO data");
const key = await BotOrgService.getSymmetricKey(
ssoConfig.organization
);
const entryPoint = client.decryptSymmetric(
ssoConfig.encryptedEntryPoint,
key,
ssoConfig.entryPointIV,
ssoConfig.entryPointTag
);
const issuer = client.decryptSymmetric(
ssoConfig.encryptedIssuer,
key,
ssoConfig.issuerIV,
ssoConfig.issuerTag
);
const cert = client.decryptSymmetric(
ssoConfig.encryptedCert,
key,
ssoConfig.certIV,
ssoConfig.certTag
);
return ({
_id: ssoConfig._id,
organization: ssoConfig.organization,
authProvider: ssoConfig.authProvider,
isActive: ssoConfig.isActive,
entryPoint,
issuer,
cert
});
}

View File

@ -1,7 +1,5 @@
import requireLicenseAuth from "./requireLicenseAuth";
import requireSecretSnapshotAuth from "./requireSecretSnapshotAuth";
export {
requireLicenseAuth,
requireSecretSnapshotAuth,
}

View File

@ -1,23 +0,0 @@
import { NextFunction, Request, Response } from "express";
/**
* Validate if organization hosting meets license requirements to
* access a license-specific route.
* @param {Object} obj
* @param {String[]} obj.acceptedTiers
*/
const requireLicenseAuth = ({
acceptedTiers,
}: {
acceptedTiers: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
} catch (err) {
}
}
}
export default requireLicenseAuth;

View File

@ -66,6 +66,4 @@ const actionSchema = new Schema<IAction>(
}
);
const Action = model<IAction>("Action", actionSchema);
export default Action;
export const Action = model<IAction>("Action", actionSchema);

View File

@ -52,9 +52,7 @@ const folderRootVersionSchema = new Schema<TFolderRootVersionSchema>(
}
);
const FolderVersion = model<TFolderRootVersionSchema>(
export const FolderVersion = model<TFolderRootVersionSchema>(
"FolderVersion",
folderRootVersionSchema
);
export default FolderVersion;
);

View File

@ -1,18 +1,7 @@
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
import SecretVersion, { ISecretVersion } from "./secretVersion";
import FolderVersion, { TFolderRootVersionSchema } from "./folderVersion";
import Log, { ILog } from "./log";
import Action, { IAction } from "./action";
export {
SecretSnapshot,
ISecretSnapshot,
SecretVersion,
ISecretVersion,
FolderVersion,
TFolderRootVersionSchema,
Log,
ILog,
Action,
IAction,
};
export * from "./secretSnapshot";
export * from "./secretVersion";
export * from "./folderVersion";
export * from "./log";
export * from "./action";
export * from "./ssoConfig";
export * from "./trustedIp";

View File

@ -63,11 +63,10 @@ const logSchema = new Schema<ILog>(
ipAddress: {
type: String,
},
}, {
timestamps: true,
}
},
{
timestamps: true,
}
);
const Log = model<ILog>("Log", logSchema);
export default Log;
export const Log = model<ILog>("Log", logSchema);

View File

@ -46,9 +46,7 @@ const secretSnapshotSchema = new Schema<ISecretSnapshot>(
}
);
const SecretSnapshot = model<ISecretSnapshot>(
export const SecretSnapshot = model<ISecretSnapshot>(
"SecretSnapshot",
secretSnapshotSchema
);
export default SecretSnapshot;
);

View File

@ -124,9 +124,7 @@ const secretVersionSchema = new Schema<ISecretVersion>(
}
);
const SecretVersion = model<ISecretVersion>(
export const SecretVersion = model<ISecretVersion>(
"SecretVersion",
secretVersionSchema
);
export default SecretVersion;
);

View File

@ -0,0 +1,72 @@
import { Schema, Types, model } from "mongoose";
export enum AuthProvider {
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
}
export interface ISSOConfig {
organization: Types.ObjectId;
authProvider: AuthProvider;
isActive: boolean;
encryptedEntryPoint: string;
entryPointIV: string;
entryPointTag: string;
encryptedIssuer: string;
issuerIV: string;
issuerTag: string;
encryptedCert: string;
certIV: string;
certTag: string;
}
const ssoConfigSchema = new Schema<ISSOConfig>(
{
organization: {
type: Schema.Types.ObjectId,
ref: "Organization"
},
authProvider: {
type: String,
enum: AuthProvider,
required: true
},
isActive: {
type: Boolean,
required: true
},
encryptedEntryPoint: {
type: String
},
entryPointIV: {
type: String
},
entryPointTag: {
type: String
},
encryptedIssuer: {
type: String
},
issuerIV: {
type: String
},
issuerTag: {
type: String
},
encryptedCert: {
type: String
},
certIV: {
type: String
},
certTag: {
type: String
}
},
{
timestamps: true
}
);
export const SSOConfig = model<ISSOConfig>("SSOConfig", ssoConfigSchema);

View File

@ -0,0 +1,54 @@
import { Schema, Types, model } from "mongoose";
export enum IPType {
IPV4 = "ipv4",
IPV6 = "ipv6"
}
export interface ITrustedIP {
_id: Types.ObjectId;
workspace: Types.ObjectId;
ipAddress: string;
type: "ipv4" | "ipv6", // either IPv4/IPv6 address or network IPv4/IPv6 address
isActive: boolean;
comment: string;
prefix?: number; // CIDR
}
const trustedIpSchema = new Schema<ITrustedIP>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
ipAddress: {
type: String,
required: true
},
type: {
type: String,
enum: [
IPType.IPV4,
IPType.IPV6
],
required: true
},
prefix: {
type: Number,
required: false
},
isActive: {
type: Boolean,
required: true
},
comment: {
type: String
}
},
{
timestamps: true
}
);
export const TrustedIP = model<ITrustedIP>("TrustedIP", trustedIpSchema);

View File

@ -1,6 +1,8 @@
import secret from "./secret";
import secretSnapshot from "./secretSnapshot";
import organizations from "./organizations";
import sso from "./sso";
import users from "./users";
import workspace from "./workspace";
import action from "./action";
import cloudProducts from "./cloudProducts";
@ -9,6 +11,8 @@ export {
secret,
secretSnapshot,
organizations,
sso,
users,
workspace,
action,
cloudProducts,

View File

@ -0,0 +1,122 @@
import express from "express";
const router = express.Router();
import passport from "passport";
import {
AuthProvider
} from "../../models";
import {
requireAuth,
requireOrganizationAuth,
validateRequest,
} from "../../../middleware";
import { body, query } from "express-validator";
import { ssoController } from "../../controllers/v1";
import { authLimiter } from "../../../helpers/rateLimiter";
import {
ACCEPTED,
ADMIN,
OWNER
} 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/saml2/:ssoIdentifier",
authLimiter,
(req, res, next) => {
const options = {
failureRedirect: "/",
additionalParams: {
RelayState: req.query.callback_port ?? ""
},
};
passport.authenticate("saml", options)(req, res, next);
}
);
router.post("/saml2/:ssoIdentifier",
passport.authenticate("saml", {
failureRedirect: "/login/provider/error",
failureFlash: true,
session: false
}),
ssoController.redirectSSO
);
router.get(
"/config",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "query"
}),
query("organizationId").exists().trim(),
validateRequest,
ssoController.getSSOConfig
);
router.post(
"/config",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "body"
}),
body("organizationId").exists().trim(),
body("authProvider").exists().isString().isIn([AuthProvider.OKTA_SAML]),
body("isActive").exists().isBoolean(),
body("entryPoint").exists().isString(),
body("issuer").exists().isString(),
body("cert").exists().isString(),
validateRequest,
ssoController.createSSOConfig
);
router.patch(
"/config",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "body"
}),
body("organizationId").exists().trim(),
body("authProvider").optional().isString(),
body("isActive").optional().isBoolean(),
body("entryPoint").optional().isString(),
body("issuer").optional().isString(),
body("cert").optional().isString(),
validateRequest,
ssoController.updateSSOConfig
);
export default router;

View File

@ -0,0 +1,17 @@
import express from "express";
const router = express.Router();
import {
requireAuth
} from "../../../middleware";
import { AUTH_MODE_API_KEY, AUTH_MODE_JWT } from "../../../variables";
import { usersController } from "../../controllers/v1";
router.get(
"/me/ip",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
usersController.getMyIp
);
export default router;

View File

@ -6,13 +6,18 @@ import {
validateRequest,
} from "../../../middleware";
import { body, param, query } from "express-validator";
import { ADMIN, MEMBER } from "../../../variables";
import {
ADMIN,
AUTH_MODE_API_KEY,
AUTH_MODE_JWT,
MEMBER
} from "../../../variables";
import { workspaceController } from "../../controllers/v1";
router.get(
"/:workspaceId/secret-snapshots",
requireAuth({
acceptedAuthModes: ["jwt", "apiKey"],
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -30,7 +35,7 @@ router.get(
router.get(
"/:workspaceId/secret-snapshots/count",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -46,7 +51,7 @@ router.get(
router.post(
"/:workspaceId/secret-snapshots/rollback",
requireAuth({
acceptedAuthModes: ["jwt", "apiKey"],
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -63,7 +68,7 @@ router.post(
router.get(
"/:workspaceId/logs",
requireAuth({
acceptedAuthModes: ["jwt", "apiKey"],
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -79,4 +84,66 @@ router.get(
workspaceController.getWorkspaceLogs
);
router.get(
"/:workspaceId/trusted-ips",
param("workspaceId").exists().isString().trim(),
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
workspaceController.getWorkspaceTrustedIps
);
router.post(
"/:workspaceId/trusted-ips",
param("workspaceId").exists().isString().trim(),
body("ipAddress").exists().isString().trim(),
body("comment").default("").isString().trim(),
body("isActive").exists().isBoolean(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
}),
workspaceController.addWorkspaceTrustedIp
);
router.patch(
"/:workspaceId/trusted-ips/:trustedIpId",
param("workspaceId").exists().isString().trim(),
param("trustedIpId").exists().isString().trim(),
body("ipAddress").isString().trim().default(""),
body("comment").default("").isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
}),
workspaceController.updateWorkspaceTrustedIp
);
router.delete(
"/:workspaceId/trusted-ips/:trustedIpId",
param("workspaceId").exists().isString().trim(),
param("trustedIpId").exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
}),
workspaceController.deleteWorkspaceTrustedIp
);
export default router;

View File

@ -26,11 +26,13 @@ interface FeatureSet {
environmentsUsed: number;
secretVersioning: boolean;
pitRecovery: boolean;
ipAllowlisting: boolean;
rbac: boolean;
customRateLimits: boolean;
customAlerts: boolean;
auditLogs: boolean;
status: 'incomplete' | 'incomplete_expired' | 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | null;
samlSSO: boolean;
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
trial_end: number | null;
has_used_trial: boolean;
}
@ -59,10 +61,12 @@ class EELicenseService {
environmentsUsed: 0,
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,
rbac: true,
customRateLimits: true,
customAlerts: true,
auditLogs: false,
samlSSO: false,
status: null,
trial_end: null,
has_used_trial: true

View File

@ -122,11 +122,11 @@ export const getAuthUserPayload = async ({
}, {
lastUsed: new Date(),
});
if (!tokenVersion) throw UnauthorizedRequestError({
message: "Failed to validate access token",
});
if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError({
message: "Failed to validate access token",
});
@ -150,8 +150,8 @@ export const getAuthSTDPayload = async ({
}) => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3);
let serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt");
const serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt")
if (!serviceTokenData) {
throw ServiceTokenDataNotFoundError({ message: "Failed to find service token data" });
@ -168,7 +168,7 @@ export const getAuthSTDPayload = async ({
message: "Failed to authenticate service token",
});
serviceTokenData = await ServiceTokenData
const serviceTokenDataToReturn = await ServiceTokenData
.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER),
}, {
@ -176,11 +176,11 @@ export const getAuthSTDPayload = async ({
}, {
new: true,
})
.select("+encryptedKey +iv +tag");
.select("+encryptedKey +iv +tag")
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: "Failed to find service token data" });
if (!serviceTokenDataToReturn) throw ServiceTokenDataNotFoundError({ message: "Failed to find service token data" });
return serviceTokenData;
return serviceTokenDataToReturn;
}
/**
@ -275,11 +275,11 @@ export const getAuthAPIKeyPayload = async ({
* @return {String} obj.token - issued JWT token
* @return {String} obj.refreshToken - issued refresh token
*/
export const issueAuthTokens = async ({
export const issueAuthTokens = async ({
userId,
ip,
userAgent,
}: {
}: {
userId: Types.ObjectId;
ip: string;
userAgent: string;
@ -292,7 +292,7 @@ export const issueAuthTokens = async ({
ip,
userAgent,
});
if (!tokenVersion) {
// case: no existing ip and user agent exists
// -> create new (session) token version for ip and user agent
@ -389,7 +389,7 @@ export const validateProviderAuthToken = async ({
const decodedToken = <jwt.ProviderAuthJwtPayload>(
jwt.verify(providerAuthToken, await getJwtProviderAuthSecret())
);
if (
decodedToken.authProvider !== user.authProvider ||
decodedToken.email !== email

View File

@ -275,3 +275,70 @@ export const decryptSymmetricHelper = async ({
return plaintext;
};
/**
* Return decrypted comments for workspace secrets with id [workspaceId]
* and [envionment] using bot
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment
*/
export const getSecretsCommentBotHelper = async ({
workspaceId,
environment,
secretPath
} : {
workspaceId: Types.ObjectId;
environment: string;
secretPath: string;
}) => {
const content = {} as any;
const key = await getKey({ workspaceId: workspaceId });
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
});
if (!folders && secretPath !== "/") {
throw InternalServerError({ message: "Folder not found" });
}
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw InternalServerError({ message: "Folder not found" });
}
folderId = folder.id;
}
const secrets = await Secret.find({
workspace: workspaceId,
environment,
type: SECRET_SHARED,
folder: folderId,
});
secrets.forEach((secret: ISecret) => {
if(secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key,
});
const commentValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key,
});
content[secretKey] = commentValue;
}
});
return content;
}

View File

@ -0,0 +1,134 @@
import { Types } from "mongoose";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { BotOrg } from "../models";
import { decryptSymmetric128BitHexKeyUTF8 } from "../utils/crypto";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8
} from "../variables";
import { InternalServerError } from "../utils/errors";
import { encryptSymmetric128BitHexKeyUTF8, generateKeyPair } from "../utils/crypto";
/**
* Create a bot with name [name] for organization with id [organizationId]
* @param {Object} obj
* @param {String} obj.name - name of bot
* @param {String} obj.organizationId - id of organization that bot belongs to
*/
export const createBotOrg = async ({
name,
organizationId,
}: {
name: string;
organizationId: Types.ObjectId;
}) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const { publicKey, privateKey } = generateKeyPair();
const key = client.createSymmetricKey();
if (rootEncryptionKey) {
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag
} = client.encryptSymmetric(key, rootEncryptionKey);
return await new BotOrg({
name,
organization: organizationId,
publicKey,
encryptedSymmetricKey,
symmetricKeyIV,
symmetricKeyTag,
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
symmetricKeyKeyEncoding: ENCODING_SCHEME_BASE64,
encryptedPrivateKey,
privateKeyIV,
privateKeyTag,
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
privateKeyKeyEncoding: ENCODING_SCHEME_BASE64
}).save();
} else if (encryptionKey) {
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey,
key: encryptionKey
});
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: key,
key: encryptionKey
});
return await new BotOrg({
name,
organization: organizationId,
publicKey,
encryptedSymmetricKey,
symmetricKeyIV,
symmetricKeyTag,
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
symmetricKeyKeyEncoding: ENCODING_SCHEME_UTF8,
encryptedPrivateKey,
privateKeyIV,
privateKeyTag,
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
privateKeyKeyEncoding: ENCODING_SCHEME_UTF8
}).save();
}
throw InternalServerError({
message: "Failed to create new organization bot due to missing encryption key",
});
};
export const getSymmetricKeyHelper = async (organizationId: Types.ObjectId) => {
const rootEncryptionKey = await getRootEncryptionKey();
const encryptionKey = await getEncryptionKey();
const botOrg = await BotOrg.findOne({
organization: organizationId
});
if (!botOrg) throw new Error("Failed to find organization bot");
if (rootEncryptionKey && botOrg.symmetricKeyKeyEncoding == ENCODING_SCHEME_BASE64) {
const key = client.decryptSymmetric(
botOrg.encryptedSymmetricKey,
rootEncryptionKey,
botOrg.symmetricKeyIV,
botOrg.symmetricKeyTag
);
return key;
} else if (encryptionKey && botOrg.symmetricKeyKeyEncoding === ENCODING_SCHEME_UTF8) {
const key = decryptSymmetric128BitHexKeyUTF8({
ciphertext: botOrg.encryptedSymmetricKey,
iv: botOrg.symmetricKeyIV,
tag: botOrg.symmetricKeyTag,
key: encryptionKey
});
return key;
}
throw InternalServerError({
message: "Failed to match encryption key with organization bot symmetric key encoding"
});
}

View File

@ -9,6 +9,7 @@ import {
INTEGRATION_VERCEL,
} from "../variables";
import { UnauthorizedRequestError } from "../utils/errors";
import * as Sentry from "@sentry/node";
interface Update {
workspace: string;
@ -115,46 +116,60 @@ export const syncIntegrationsHelper = async ({
workspaceId: Types.ObjectId;
environment?: string;
}) => {
const integrations = await Integration.find({
workspace: workspaceId,
...(environment
? {
try {
const integrations = await Integration.find({
workspace: workspaceId,
...(environment
? {
environment,
}
: {}),
isActive: true,
app: { $ne: null },
});
// for each workspace integration, sync/push secrets
// to that integration
for await (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({
// issue here?
workspaceId: integration.workspace,
environment: integration.environment,
secretPath: integration.secretPath,
isActive: true,
app: { $ne: null },
});
const integrationAuth = await IntegrationAuth.findById(
integration.integrationAuth
);
if (!integrationAuth) throw new Error("Failed to find integration auth");
// for each workspace integration, sync/push secrets
// to that integration
for await (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({
workspaceId: integration.workspace,
environment: integration.environment,
secretPath: integration.secretPath,
});
// get integration auth access token
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth,
});
// get workspace, environment (shared) secrets comments
const secretComments = await BotService.getSecretComments({
workspaceId: integration.workspace,
environment: integration.environment,
secretPath: integration.secretPath,
})
// sync secrets to integration
await syncSecrets({
integration,
integrationAuth,
secrets,
accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken,
});
const integrationAuth = await IntegrationAuth.findById(
integration.integrationAuth
);
if (!integrationAuth) throw new Error("Failed to find integration auth");
// get integration auth access token
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth,
});
// sync secrets to integration
await syncSecrets({
integration,
integrationAuth,
secrets,
accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken,
secretComments
});
}
} catch (err) {
Sentry.captureException(err);
console.log(`syncIntegrationsHelper: failed with [workspaceId=${workspaceId}] [environment=${environment}]`, err) // eslint-disable-line no-use-before-define
throw err
}
};

View File

@ -14,6 +14,9 @@ import {
licenseKeyRequest,
licenseServerKeyRequest,
} from "../config/request";
import {
createBotOrg
} from "./botOrg";
/**
* Create an organization with name [name]
@ -29,6 +32,7 @@ export const createOrganization = async ({
name: string;
email: string;
}) => {
const licenseServerKey = await getLicenseServerKey();
let organization;
@ -52,6 +56,12 @@ export const createOrganization = async ({
}).save();
}
// initialize bot for organization
await createBotOrg({
name,
organizationId: organization._id
});
return organization;
};

View File

@ -109,9 +109,9 @@ export const v1PushSecrets = async ({
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
if (
s.secretValueHash !==
newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue ||
newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue ||
s.secretCommentHash !==
newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment
newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment
) {
// case: filter secrets where value or comment changed
return true;
@ -371,9 +371,9 @@ export const v2PushSecrets = async ({
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
if (
s.secretValueHash !==
newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash ||
newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash ||
s.secretCommentHash !==
newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash
newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash
) {
// case: filter secrets where value or comment changed
return true;
@ -484,7 +484,7 @@ export const v2PushSecrets = async ({
// (EE) add secret versions for new secrets
EESecretService.addSecretVersions({
secretVersions: newSecrets.map((secretDocument: ISecret) => {
secretVersions: newSecrets.map((secretDocument) => {
return new SecretVersion({
...secretDocument,
secret: secretDocument._id,

View File

@ -44,6 +44,7 @@ import { EELogService, EESecretService } from "../ee/services";
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth";
import { getFolderIdFromServiceToken } from "../services/FolderService";
import picomatch from "picomatch";
import path from "path";
export const isValidScope = (
authPayload: IServiceTokenData,
@ -60,6 +61,13 @@ export const isValidScope = (
return Boolean(validScope);
};
export function containsGlobPatterns(secretPath: string) {
const globChars = ["*", "?", "[", "]", "{", "}", "**"];
const normalizedPath = path.normalize(secretPath);
return globChars.some(char => normalizedPath.includes(char));
}
/**
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
*
@ -338,6 +346,7 @@ export const createSecretHelper = async ({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
type,
environment,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
});
@ -354,6 +363,7 @@ export const createSecretHelper = async ({
secretBlindIndex,
folder: folderId,
workspace: new Types.ObjectId(workspaceId),
environment,
type: SECRET_SHARED
});

View File

@ -5,6 +5,10 @@ import {
Secret,
Workspace,
} from "../models";
import {
IPType,
TrustedIP
} from "../ee/models";
import { createBot } from "../helpers/bot";
import { EELicenseService } from "../ee/services";
import { SecretService } from "../services";
@ -40,6 +44,26 @@ export const createWorkspace = async ({
await SecretService.createSecretBlindIndexData({
workspaceId: workspace._id,
});
// initialize default trusted IPv4 CIDR - 0.0.0.0/0
await new TrustedIP({
workspace: workspace._id,
ipAddress: "0.0.0.0",
type: IPType.IPV4,
prefix: 0,
isActive: true,
comment: ""
}).save()
// initialize default trusted IPv6 CIDR - ::/0
await new TrustedIP({
workspace: workspace._id,
ipAddress: "::",
type: IPType.IPV6,
prefix: 0,
isActive: true,
comment: ""
});
await EELicenseService.refreshPlan(organizationId);

View File

@ -19,9 +19,11 @@ import {
action as eeActionRouter,
cloudProducts as eeCloudProductsRouter,
organizations as eeOrganizationsRouter,
sso as eeSSORouter,
secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
workspace as eeWorkspaceRouter
users as eeUsersRouter,
workspace as eeWorkspaceRouter,
} from "./ee/routes/v1";
import {
auth as v1AuthRouter,
@ -34,6 +36,7 @@ import {
membership as v1MembershipRouter,
organization as v1OrganizationRouter,
password as v1PasswordRouter,
secretImport as v1SecretImportRouter,
secret as v1SecretRouter,
secretScanning as v1SecretScanningRouter,
secretsFolder as v1SecretsFolder,
@ -46,16 +49,16 @@ import {
} from "./routes/v1";
import {
auth as v2AuthRouter,
environment as v2EnvironmentRouter,
organizations as v2OrganizationsRouter,
signup as v2SignupRouter,
users as v2UsersRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
serviceAccounts as v2ServiceAccountsRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter
serviceTokenData as v2ServiceTokenDataRouter,
signup as v2SignupRouter,
tags as v2TagsRouter,
users as v2UsersRouter,
workspace as v2WorkspaceRouter,
} from "./routes/v2";
import {
auth as v3AuthRouter,
@ -80,6 +83,7 @@ const main = async () => {
const app = express();
app.enable("trust proxy");
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(
cors({
@ -88,7 +92,7 @@ const main = async () => {
})
);
if (await getSecretScanningGitAppId()) {
if (await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey()) {
const probot = new Probot({
appId: await getSecretScanningGitAppId(),
privateKey: await getSecretScanningPrivateKey(),
@ -126,9 +130,11 @@ const main = async () => {
// (EE) routes
app.use("/api/v1/secret", eeSecretRouter);
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
app.use("/api/v1/users", eeUsersRouter);
app.use("/api/v1/workspace", eeWorkspaceRouter);
app.use("/api/v1/action", eeActionRouter);
app.use("/api/v1/organizations", eeOrganizationsRouter);
app.use("/api/v1/sso", eeSSORouter);
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
// v1 routes (default)
@ -151,6 +157,7 @@ const main = async () => {
app.use("/api/v1/folders", v1SecretsFolder);
app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
app.use("/api/v1/webhooks", v1WebhooksRouter);
app.use("/api/v1/secret-imports", v1SecretImportRouter);
// v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter);

View File

@ -1,16 +1,21 @@
import { Octokit } from "@octokit/rest";
import { IIntegrationAuth } from "../models";
import { standardRequest } from "../config/request";
import {
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_CHECKLY,
INTEGRATION_CHECKLY_API_URL,
INTEGRATION_CIRCLECI,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUDFLARE_PAGES_API_URL,
INTEGRATION_CLOUD_66,
INTEGRATION_CLOUD_66_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL,
INTEGRATION_DIGITAL_OCEAN_API_URL,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_GITHUB,
@ -22,17 +27,27 @@ import {
INTEGRATION_LARAVELFORGE_API_URL,
INTEGRATION_NETLIFY,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_NORTHFLANK,
INTEGRATION_NORTHFLANK_API_URL,
INTEGRATION_RAILWAY,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_RENDER,
INTEGRATION_RENDER_API_URL,
INTEGRATION_SUPABASE,
INTEGRATION_SUPABASE_API_URL,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TERRAFORM_CLOUD_API_URL,
INTEGRATION_TRAVISCI,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_WINDMILL,
INTEGRATION_WINDMILL_API_URL,
} from "../variables";
import { IIntegrationAuth } from "../models";
import { Octokit } from "@octokit/rest";
import { standardRequest } from "../config/request";
interface App {
name: string;
@ -54,11 +69,13 @@ const getApps = async ({
accessToken,
accessId,
teamId,
workspaceSlug,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
accessId?: string;
teamId?: string;
workspaceSlug?: string;
}) => {
let apps: App[] = [];
switch (integrationAuth.integration) {
@ -124,11 +141,23 @@ const getApps = async ({
serverId: accessId
});
break;
case INTEGRATION_TERRAFORM_CLOUD:
apps = await getAppsTerraformCloud({
accessToken,
workspacesId: accessId,
});
break;
case INTEGRATION_TRAVISCI:
apps = await getAppsTravisCI({
accessToken,
});
break;
case INTEGRATION_TEAMCITY:
apps = await getAppsTeamCity({
integrationAuth,
accessToken,
});
break;
case INTEGRATION_SUPABASE:
apps = await getAppsSupabase({
accessToken,
@ -143,7 +172,38 @@ const getApps = async ({
apps = await getAppsCloudflarePages({
accessToken,
accountId: accessId
})
});
break;
case INTEGRATION_NORTHFLANK:
apps = await getAppsNorthflank({
accessToken,
});
break;
case INTEGRATION_BITBUCKET:
apps = await getAppsBitBucket({
accessToken,
workspaceSlug
});
break;
case INTEGRATION_CODEFRESH:
apps = await getAppsCodefresh({
accessToken,
});
break;
case INTEGRATION_WINDMILL:
apps = await getAppsWindmill({
accessToken
});
break;
case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM:
apps = await getAppsDigitalOceanAppPlatform({
accessToken
});
break;
case INTEGRATION_CLOUD_66:
apps = await getAppsCloud66({
accessToken,
});
break;
}
@ -196,10 +256,10 @@ const getAppsVercel = async ({
},
...(integrationAuth?.teamId
? {
params: {
teamId: integrationAuth.teamId,
},
}
params: {
teamId: integrationAuth.teamId,
},
}
: {}),
})
).data;
@ -532,6 +592,43 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
return apps;
};
/**
* Return list of projects for Terraform Cloud integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Terraform Cloud API
* @param {String} obj.workspacesId - workspace id of Terraform Cloud projects
* @returns {Object[]} apps - names and ids of Terraform Cloud projects
* @returns {String} apps.name - name of Terraform Cloud projects
*/
const getAppsTerraformCloud = async ({
accessToken,
workspacesId
}: {
accessToken: string;
workspacesId?: string;
}) => {
const res = (
await standardRequest.get(`${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${workspacesId}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
})
).data.data;
const apps = []
const appsObj = {
name: res?.attributes.name,
appId: res?.id,
};
apps.push(appsObj)
return apps;
};
/**
* Return list of repositories for GitLab integration
* @param {Object} obj
@ -632,6 +729,39 @@ const getAppsGitlab = async ({
return apps;
};
/**
* Return list of projects for TeamCity integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for TeamCity API
* @returns {Object[]} apps - names and ids of TeamCity projects
* @returns {String} apps.name - name of TeamCity projects
*/
const getAppsTeamCity = async ({
integrationAuth,
accessToken,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
const res = (
await standardRequest.get(`${integrationAuth.url}/app/rest/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
})
).data.project.slice(1);
const apps = res.map((a: any) => {
return {
name: a.name,
appId: a.id,
};
});
return apps;
};
/**
* Return list of projects for Supabase integration
* @param {Object} obj
@ -695,15 +825,76 @@ const getAppsCheckly = async ({ accessToken }: { accessToken: string }) => {
* @returns {Object[]} apps - Cloudflare Pages projects
* @returns {String} apps.name - name of Cloudflare Pages project
*/
const getAppsCloudflarePages = async ({
accessToken,
accountId
const getAppsCloudflarePages = async ({
accessToken,
accountId
}: {
accessToken: string;
accountId?: string;
accessToken: string;
accountId?: string;
}) => {
const { data } = await standardRequest.get(
`${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects`,
const { data } = await standardRequest.get(
`${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
const apps = data.result.map((a: any) => {
return {
name: a.name,
appId: a.id,
};
});
return apps;
}
/**
* Return list of repositories for the BitBucket integration based on provided BitBucket workspace
* @param {Object} obj
* @param {String} obj.accessToken - access token for BitBucket API
* @param {String} obj.workspaceSlug - Workspace identifier for fetching BitBucket repositories
* @returns {Object[]} apps - BitBucket repositories
* @returns {String} apps.name - name of BitBucket repository
*/
const getAppsBitBucket = async ({
accessToken,
workspaceSlug,
}: {
accessToken: string;
workspaceSlug?: string;
}) => {
interface RepositoriesResponse {
size: number;
page: number;
pageLen: number;
next: string;
previous: string;
values: Array<Repository>;
}
interface Repository {
type: string;
uuid: string;
name: string;
is_private: boolean;
created_on: string;
updated_on: string;
}
if (!workspaceSlug) {
return []
}
const repositories: Repository[] = [];
let hasNextPage = true;
let repositoriesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}`
while (hasNextPage) {
const { data }: { data: RepositoriesResponse } = await standardRequest.get(
repositoriesUrl,
{
headers: {
Authorization: `Bearer ${accessToken}`,
@ -712,13 +903,290 @@ const getAppsCloudflarePages = async ({
}
);
const apps = data.result.map((a: any) => {
return {
name: a.name,
appId: a.id,
};
});
return apps;
if (data?.values.length > 0) {
data.values.forEach((repository) => {
repositories.push(repository)
})
}
if (data.next) {
repositoriesUrl = data.next
} else {
hasNextPage = false
}
}
const apps = repositories.map((repository) => {
return {
name: repository.name,
appId: repository.uuid,
};
});
return apps;
}
/** Return list of projects for Northflank integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Northflank API
* @returns {Object[]} apps - names of Northflank apps
* @returns {String} apps.name - name of Northflank app
*/
const getAppsNorthflank = async ({ accessToken }: { accessToken: string }) => {
const {
data: {
data: {
projects
}
}
} = await standardRequest.get(
`${INTEGRATION_NORTHFLANK_API_URL}/v1/projects`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
const apps = projects.map((a: any) => {
return {
name: a.name,
appId: a.id
};
});
return apps;
};
/**
* Return list of projects for Supabase integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Supabase API
* @returns {Object[]} apps - names of Supabase apps
* @returns {String} apps.name - name of Supabase app
*/
const getAppsCodefresh = async ({
accessToken,
}: {
accessToken: string;
}) => {
const res = (
await standardRequest.get(`${INTEGRATION_CODEFRESH_API_URL}/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
})
).data;
const apps = res.projects.map((a: any) => ({
name: a.projectName,
appId: a.id,
}));
return apps;
};
/**
* Return list of projects for Windmill integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Windmill API
* @returns {Object[]} apps - names of Windmill workspaces
* @returns {String} apps.name - name of Windmill workspace
*/
const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
const { data } = await standardRequest.get(
`${INTEGRATION_WINDMILL_API_URL}/workspaces/list`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
// check for write access of secrets in windmill workspaces
const writeAccessCheck = data.map(async (app: any) => {
try {
const userPath = "u/user/variable";
const folderPath = "f/folder/variable";
const { data: writeUser } = await standardRequest.post(
`${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/create`,
{
path: userPath,
value: "variable",
is_secret: true,
description: "variable description"
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
const { data: writeFolder } = await standardRequest.post(
`${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/create`,
{
path: folderPath,
value: "variable",
is_secret: true,
description: "variable description"
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
// is write access is allowed then delete the created secrets from workspace
if (writeUser && writeFolder) {
await standardRequest.delete(
`${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/delete/${userPath}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
await standardRequest.delete(
`${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/delete/${folderPath}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
return app;
} else {
return { error: "cannot write secret" };
}
} catch (err: any) {
return { error: err.message };
}
});
const appsWriteResponses = await Promise.all(writeAccessCheck);
const appsWithWriteAccess = appsWriteResponses.filter((appRes: any) => !appRes.error);
const apps = appsWithWriteAccess.map((a: any) => {
return {
name: a.name,
appId: a.id,
};
});
return apps;
}
/**
* Return list of applications for DigitalOcean App Platform integration
* @param {Object} obj
* @param {String} obj.accessToken - personal access token for DigitalOcean
* @returns {Object[]} apps - names of DigitalOcean apps
* @returns {String} apps.name - name of DigitalOcean app
* @returns {String} apps.appId - id of DigitalOcean app
*/
const getAppsDigitalOceanAppPlatform = async ({ accessToken }: { accessToken: string }) => {
interface DigitalOceanApp {
id: string;
owner_uuid: string;
spec: Spec;
}
interface Spec {
name: string;
region: string;
envs: Env[];
}
interface Env {
key: string;
value: string;
scope: string;
}
const res = (
await standardRequest.get(`${INTEGRATION_DIGITAL_OCEAN_API_URL}/v2/apps`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
})
).data;
return (res.apps ?? []).map((a: DigitalOceanApp) => ({
name: a.spec.name,
appId: a.id
}));
}
/**
* Return list of applications for Cloud66 integration
* @param {Object} obj
* @param {String} obj.accessToken - personal access token for Cloud66 API
* @returns {Object[]} apps - Cloud66 apps
* @returns {String} apps.name - name of Cloud66 app
* @returns {String} apps.appId - uid of Cloud66 app
*/
const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => {
interface Cloud66Apps {
uid: string;
name: string;
account_id: number;
git: string;
git_branch: string;
environment: string;
cloud: string;
fqdn: string;
language: string;
framework: string;
status: number;
health: number;
last_activity: string;
last_activity_iso: string;
maintenance_mode: boolean;
has_loadbalancer: boolean;
created_at: string;
updated_at: string;
deploy_directory: string;
cloud_status: string;
backend: string;
version: string;
revision: string;
is_busy: boolean;
account_name: string;
is_cluster: boolean;
is_inside_cluster: boolean;
cluster_name: any;
application_address: string;
configstore_namespace: string;
}
const stacks = (
await standardRequest.get(`${INTEGRATION_CLOUD_66_API_URL}/3/stacks`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
})
).data.response as Cloud66Apps[]
const apps = stacks.map((app) => ({
name: app.name,
appId: app.uid
}));
return apps;
};
export { getApps };

View File

@ -2,6 +2,8 @@ import { standardRequest } from "../config/request";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GITHUB,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB,
@ -15,11 +17,13 @@ import {
} from "../variables";
import {
getClientIdAzure,
getClientIdBitBucket,
getClientIdGitHub,
getClientIdGitLab,
getClientIdNetlify,
getClientIdVercel,
getClientSecretAzure,
getClientSecretBitBucket,
getClientSecretGitHub,
getClientSecretGitLab,
getClientSecretHeroku,
@ -78,6 +82,15 @@ interface ExchangeCodeGitlabResponse {
created_at: number;
}
interface ExchangeCodeBitBucketResponse {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scopes: string;
state: string;
}
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
* code-token exchange for integration named [integration]
@ -129,6 +142,12 @@ const exchangeCode = async ({
obj = await exchangeCodeGitlab({
code,
});
break;
case INTEGRATION_BITBUCKET:
obj = await exchangeCodeBitBucket({
code,
});
break;
}
return obj;
@ -347,4 +366,43 @@ const exchangeCodeGitlab = async ({ code }: { code: string }) => {
};
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for BitBucket
* code-token exchange
* @param {Object} obj1
* @param {Object} obj1.code - code for code-token exchange
* @returns {Object} obj2
* @returns {String} obj2.accessToken - access token for BitBucket API
* @returns {String} obj2.refreshToken - refresh token for BitBucket API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeBitBucket = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
const res: ExchangeCodeBitBucketResponse = (
await standardRequest.post(
INTEGRATION_BITBUCKET_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code: code,
client_id: await getClientIdBitBucket(),
client_secret: await getClientSecretBitBucket(),
redirect_uri: `${await getSiteURL()}/integrations/bitbucket/oauth2/callback`,
} as any),
{
headers: {
"Accept-Encoding": "application/json",
},
}
)
).data;
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt,
};
};
export { exchangeCode };

View File

@ -2,6 +2,8 @@ import { standardRequest } from "../config/request";
import { IIntegrationAuth } from "../models";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GITLAB,
INTEGRATION_HEROKU,
} from "../variables";
@ -13,8 +15,10 @@ import {
import { IntegrationService } from "../services";
import {
getClientIdAzure,
getClientIdBitBucket,
getClientIdGitLab,
getClientSecretAzure,
getClientSecretBitBucket,
getClientSecretGitLab,
getClientSecretHeroku,
getSiteURL,
@ -46,6 +50,15 @@ interface RefreshTokenGitLabResponse {
created_at: number;
}
interface RefreshTokenBitBucketResponse {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scopes: string;
state: string;
}
/**
* Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration]
@ -83,6 +96,11 @@ const exchangeRefresh = async ({
refreshToken,
});
break;
case INTEGRATION_BITBUCKET:
tokenDetails = await exchangeRefreshBitBucket({
refreshToken,
});
break;
default:
throw new Error("Failed to exchange token for incompatible integration");
}
@ -218,4 +236,46 @@ const exchangeRefreshGitLab = async ({
};
};
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* BitBucket integration
* @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for BitBucket
* @returns
*/
const exchangeRefreshBitBucket = async ({
refreshToken,
}: {
refreshToken: string;
}) => {
const accessExpiresAt = new Date();
const {
data,
}: {
data: RefreshTokenBitBucketResponse;
} = await standardRequest.post(
INTEGRATION_BITBUCKET_TOKEN_URL,
new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: await getClientIdBitBucket(),
client_secret: await getClientSecretBitBucket(),
redirect_uri: `${await getSiteURL()}/integrations/bitbucket/oauth2/callback`,
} as any),
{
headers: {
"Accept-Encoding": "application/json",
},
}
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt,
};
};
export { exchangeRefresh };

View File

@ -18,7 +18,6 @@ const revokeAccess = async ({
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let deletedIntegrationAuth;
// add any integration-specific revocation logic
switch (integrationAuth.integration) {
case INTEGRATION_HEROKU:
@ -33,7 +32,7 @@ const revokeAccess = async ({
break;
}
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
_id: integrationAuth._id,
});

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
import jwt from "jsonwebtoken";
import { Types } from "mongoose";
import { NextFunction, Request, Response } from "express";
import {
getAuthAPIKeyPayload,
@ -51,6 +52,10 @@ const requireAuth = ({
});
let authPayload: IUser | IServiceAccount | IServiceTokenData;
let authUserPayload: {
user: IUser;
tokenVersionId: Types.ObjectId;
};
switch (authMode) {
case AUTH_MODE_SERVICE_ACCOUNT:
authPayload = await getAuthSAAKPayload({
@ -71,12 +76,12 @@ const requireAuth = ({
req.user = authPayload;
break;
default:
const { user, tokenVersionId } = await getAuthUserPayload({
authUserPayload = await getAuthUserPayload({
authTokenValue,
});
authPayload = user;
req.user = user;
req.tokenVersionId = tokenVersionId;
authPayload = authUserPayload.user;
req.user = authUserPayload.user;
req.tokenVersionId = authUserPayload.tokenVersionId;
break;
}

View File

@ -18,6 +18,7 @@ const requireWorkspaceAuth = ({
requiredPermissions = [],
requireBlindIndicesEnabled = false,
requireE2EEOff = false,
checkIPAllowlist = false
}: {
acceptedRoles: Array<"admin" | "member">;
locationWorkspaceId: req;
@ -25,6 +26,7 @@ const requireWorkspaceAuth = ({
requiredPermissions?: string[];
requireBlindIndicesEnabled?: boolean;
requireE2EEOff?: boolean;
checkIPAllowlist?: boolean;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const workspaceId = req[locationWorkspaceId]?.workspaceId;
@ -39,6 +41,7 @@ const requireWorkspaceAuth = ({
requiredPermissions,
requireBlindIndicesEnabled,
requireE2EEOff,
checkIPAllowlist
});
if (membership) {

View File

@ -0,0 +1,98 @@
import { Schema, Types, model } from "mongoose";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8,
} from "../variables";
export interface IBotOrg {
_id: Types.ObjectId;
name: string;
organization: Types.ObjectId;
publicKey: string;
encryptedSymmetricKey: string;
symmetricKeyIV: string;
symmetricKeyTag: string;
symmetricKeyAlgorithm: "aes-256-gcm";
symmetricKeyKeyEncoding: "base64" | "utf8";
encryptedPrivateKey: string;
privateKeyIV: string;
privateKeyTag: string;
privateKeyAlgorithm: "aes-256-gcm";
privateKeyKeyEncoding: "base64" | "utf8";
}
const botOrgSchema = new Schema<IBotOrg>(
{
name: {
type: String,
required: true,
},
organization: {
type: Schema.Types.ObjectId,
ref: "Organization",
required: true,
},
publicKey: {
type: String,
required: true,
},
encryptedSymmetricKey: {
type: String,
required: true
},
symmetricKeyIV: {
type: String,
required: true
},
symmetricKeyTag: {
type: String,
required: true
},
symmetricKeyAlgorithm: {
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
},
symmetricKeyKeyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64,
],
required: true
},
encryptedPrivateKey: {
type: String,
required: true
},
privateKeyIV: {
type: String,
required: true
},
privateKeyTag: {
type: String,
required: true
},
privateKeyAlgorithm: {
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
},
privateKeyKeyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64,
],
required: true
},
},
{
timestamps: true,
}
);
const BotOrg = model<IBotOrg>("BotOrg", botOrgSchema);
export default BotOrg;

View File

@ -1,5 +1,6 @@
import BackupPrivateKey, { IBackupPrivateKey } from "./backupPrivateKey";
import Bot, { IBot } from "./bot";
import BotOrg, { IBotOrg } from "./botOrg";
import BotKey, { IBotKey } from "./botKey";
import IncidentContactOrg, { IIncidentContactOrg } from "./incidentContactOrg";
import Integration, { IIntegration } from "./integration";
@ -31,6 +32,8 @@ export {
IBackupPrivateKey,
Bot,
IBot,
BotOrg,
IBotOrg,
BotKey,
IBotKey,
IncidentContactOrg,

View File

@ -1,11 +1,14 @@
import { Schema, Types, model } from "mongoose";
import {
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CHECKLY,
INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
@ -13,12 +16,17 @@ import {
INTEGRATION_HEROKU,
INTEGRATION_LARAVELFORGE,
INTEGRATION_NETLIFY,
INTEGRATION_NORTHFLANK,
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
INTEGRATION_WINDMILL
} from "../variables";
import { Schema, Types, model } from "mongoose";
export interface IIntegration {
_id: Types.ObjectId;
@ -53,8 +61,16 @@ export interface IIntegration {
| "travisci"
| "supabase"
| "checkly"
| "terraform-cloud"
| "teamcity"
| "hashicorp-vault"
| "cloudflare-pages";
| "cloudflare-pages"
| "bitbucket"
| "codefresh"
| "digital-ocean-app-platform"
| "cloud-66"
| "northflank"
| "windmill";
integrationAuth: Types.ObjectId;
}
@ -142,8 +158,16 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TEAMCITY,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH,
INTEGRATION_WINDMILL,
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CLOUD_66,
INTEGRATION_NORTHFLANK
],
required: true,
},
@ -156,7 +180,7 @@ const integrationSchema = new Schema<IIntegration>(
type: String,
required: true,
default: "/",
},
}
},
{
timestamps: true,

View File

@ -1,4 +1,3 @@
import { Document, Schema, Types, model } from "mongoose";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
@ -6,8 +5,12 @@ import {
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
@ -15,17 +18,47 @@ import {
INTEGRATION_HEROKU,
INTEGRATION_LARAVELFORGE,
INTEGRATION_NETLIFY,
INTEGRATION_NORTHFLANK,
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL
INTEGRATION_VERCEL,
INTEGRATION_WINDMILL
} from "../variables";
import { Document, Schema, Types, model } from "mongoose";
export interface IIntegrationAuth extends Document {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration: "heroku" | "vercel" | "netlify" | "github" | "gitlab" | "render" | "railway" | "flyio" | "azure-key-vault" | "laravel-forge" | "circleci" | "travisci" | "supabase" | "aws-parameter-store" | "aws-secret-manager" | "checkly" | "cloudflare-pages";
integration:
| "heroku"
| "vercel"
| "netlify"
| "github"
| "gitlab"
| "render"
| "railway"
| "flyio"
| "azure-key-vault"
| "laravel-forge"
| "circleci"
| "travisci"
| "supabase"
| "aws-parameter-store"
| "aws-secret-manager"
| "checkly"
| "cloudflare-pages"
| "codefresh"
| "digital-ocean-app-platform"
| "bitbucket"
| "cloud-66"
| "terraform-cloud"
| "teamcity"
| "northflank"
| "windmill";
teamId: string;
accountId: string;
url: string;
@ -68,9 +101,17 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_CIRCLECI,
INTEGRATION_LARAVELFORGE,
INTEGRATION_TRAVISCI,
INTEGRATION_TEAMCITY,
INTEGRATION_SUPABASE,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH,
INTEGRATION_WINDMILL,
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CLOUD_66,
INTEGRATION_NORTHFLANK
],
required: true,
},

View File

@ -0,0 +1,52 @@
import { Schema, Types, model } from "mongoose";
export interface ISecretImports {
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
folderId: string;
imports: Array<{
environment: string;
secretPath: string;
}>;
}
const secretImportSchema = new Schema<ISecretImports>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
environment: {
type: String,
required: true
},
folderId: {
type: String,
required: true,
default: "root"
},
imports: {
type: [
{
environment: {
type: String,
required: true
},
secretPath: {
type: String,
required: true
}
}
],
default: []
}
},
{
timestamps: true
}
);
const SecretImport = model<ISecretImports>("SecretImports", secretImportSchema);
export default SecretImport;

View File

@ -1,7 +1,11 @@
import { Document, Schema, Types, model } from "mongoose";
export enum AuthProvider {
EMAIL = "email",
GOOGLE = "google",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml",
}
export interface IUser extends Document {

View File

@ -1,5 +1,5 @@
import express, { Request, Response } from "express";
import { getSmtpConfigured } from "../../config";
import { getInviteOnlySignup, getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookSecret, getSmtpConfigured } from "../../config";
const router = express.Router();
@ -10,6 +10,8 @@ router.get(
date: new Date(),
message: "Ok",
emailConfigured: await getSmtpConfigured(),
secretScanningConfigured: await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey(),
inviteOnlySignup: await getInviteOnlySignup()
})
}
);

View File

@ -1,7 +1,6 @@
import express from "express";
const router = express.Router();
import { body } from "express-validator";
import passport from "passport";
import { requireAuth, validateRequest } from "../../middleware";
import { authController } from "../../controllers/v1";
import { authLimiter } from "../../helpers/rateLimiter";
@ -44,21 +43,6 @@ router.post(
authController.checkAuth
);
router.get(
"/redirect/google",
authLimiter,
passport.authenticate("google", {
scope: ["profile", "email"],
session: false,
}),
);
router.get(
"/callback/google",
passport.authenticate("google", { failureRedirect: "/login/provider/error", session: false }),
authController.handleAuthProviderCallback,
);
router.get(
"/common-passwords",
authLimiter,

View File

@ -17,6 +17,7 @@ import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder";
import secretScanning from "./secretScanning";
import webhooks from "./webhook";
import secretImport from "./secretImport";
export {
signup,
@ -37,5 +38,6 @@ export {
integrationAuth,
secretsFolder,
secretScanning,
webhooks
webhooks,
secretImport
};

View File

@ -81,6 +81,7 @@ router.get(
}),
param("integrationAuthId"),
query("teamId"),
query("workspaceSlug"),
validateRequest,
integrationAuthController.getIntegrationAuthApps
);
@ -141,6 +142,33 @@ router.get(
integrationAuthController.getIntegrationAuthRailwayServices
);
router.get(
"/:integrationAuthId/bitbucket/workspaces",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param("integrationAuthId").exists().isString(),
validateRequest,
integrationAuthController.getIntegrationAuthBitBucketWorkspaces
);
router.get(
"/:integrationAuthId/northflank/secret-groups",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param("integrationAuthId").exists().isString(),
query("appId").exists().isString(),
validateRequest,
integrationAuthController.getIntegrationAuthNorthflankSecretGroups
);
router.delete(
"/:integrationAuthId",
requireAuth({

View File

@ -0,0 +1,84 @@
import express from "express";
const router = express.Router();
import { body, param, query } from "express-validator";
import { secretImportController } from "../../controllers/v1";
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables";
router.post(
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body"
}),
body("workspaceId").exists().isString().trim().notEmpty(),
body("environment").exists().isString().trim().notEmpty(),
body("folderId").default("root").isString().trim(),
body("secretImport").exists().isObject(),
body("secretImport.environment").isString().exists().trim(),
body("secretImport.secretPath").isString().exists().trim(),
validateRequest,
secretImportController.createSecretImport
);
router.put(
"/:id",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("id").exists().isString().trim(),
body("secretImports").exists().isArray(),
body("secretImports.*.environment").isString().exists().trim(),
body("secretImports.*.secretPath").isString().exists().trim(),
validateRequest,
secretImportController.updateSecretImport
);
router.delete(
"/:id",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("id").exists().isString().trim(),
body("secretImportPath").isString().exists().trim(),
body("secretImportEnv").isString().exists().trim(),
validateRequest,
secretImportController.deleteSecretImport
);
router.get(
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query"
}),
query("workspaceId").exists().isString().trim().notEmpty(),
query("environment").exists().isString().trim().notEmpty(),
query("folderId").default("root").isString().trim(),
validateRequest,
secretImportController.getSecretImports
);
router.get(
"/secrets",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query"
}),
query("workspaceId").exists().isString().trim().notEmpty(),
query("environment").exists().isString().trim().notEmpty(),
query("folderId").default("root").isString().trim(),
validateRequest,
secretImportController.getAllSecretsFromImport
);
export default router;

View File

@ -2,7 +2,7 @@ import express from "express";
const router = express.Router();
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
import { body, param, query } from "express-validator";
import { ADMIN, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, MEMBER } from "../../variables";
import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables";
import { webhookController } from "../../controllers/v1";
router.post(

View File

@ -127,6 +127,7 @@ router.get(
query("tagSlugs"),
query("folderId").default("root").isString().trim(),
query("secretPath").optional().isString().trim(),
query("include_imports").optional().default(false).isBoolean(),
validateRequest,
requireAuth({
acceptedAuthModes: [

View File

@ -10,6 +10,9 @@ import {
AUTH_MODE_API_KEY,
AUTH_MODE_JWT,
} from "../../variables";
import {
AuthProvider
} from "../../models";
router.get(
"/me",
@ -29,6 +32,30 @@ router.patch(
usersController.updateMyMfaEnabled
);
router.patch(
"/me/name",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
body("firstName").exists().isString(),
body("lastName").isString(),
validateRequest,
usersController.updateName
);
router.patch(
"/me/auth-provider",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
body("authProvider").exists().isString().isIn([
AuthProvider.EMAIL,
AuthProvider.GOOGLE
]),
validateRequest,
usersController.updateAuthProvider
);
router.get(
"/me/organizations",
requireAuth({
@ -66,7 +93,7 @@ router.delete(
usersController.deleteAPIKey
);
router.get( // new
router.get(
"/me/sessions",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
@ -74,7 +101,7 @@ router.get( // new
usersController.getMySessions
);
router.delete( // new
router.delete(
"/me/sessions",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],

View File

@ -1,10 +1,6 @@
import express from "express";
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from "../../middleware";
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
import { body, param, query } from "express-validator";
import { secretsController } from "../../controllers/v3";
import {
@ -17,30 +13,23 @@ import {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
SECRET_PERSONAL,
SECRET_SHARED,
SECRET_SHARED
} from "../../variables";
router.get(
"/raw",
query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(),
query("workspaceId").optional().isString().trim(),
query("environment").optional().isString().trim(),
query("secretPath").default("/").isString().trim(),
query("include_imports").optional().isBoolean().default(false),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query",
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
secretsController.getSecretsRaw
);
@ -58,8 +47,8 @@ router.get(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -68,6 +57,7 @@ router.get(
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
checkIPAllowlist: false
}),
secretsController.getSecretByNameRaw
);
@ -86,8 +76,8 @@ router.post(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -96,6 +86,7 @@ router.post(
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
checkIPAllowlist: false
}),
secretsController.createSecretRaw
);
@ -114,8 +105,8 @@ router.patch(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -124,6 +115,7 @@ router.patch(
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
checkIPAllowlist: false
}),
secretsController.updateSecretByNameRaw
);
@ -141,8 +133,8 @@ router.delete(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -151,6 +143,7 @@ router.delete(
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
checkIPAllowlist: false
}),
secretsController.deleteSecretByNameRaw
);
@ -166,8 +159,8 @@ router.get(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -176,6 +169,7 @@ router.get(
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
checkIPAllowlist: false
}),
secretsController.getSecrets
);
@ -201,8 +195,8 @@ router.post(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -211,6 +205,7 @@ router.post(
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
checkIPAllowlist: false
}),
secretsController.createSecret
);
@ -228,8 +223,8 @@ router.get(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -237,6 +232,7 @@ router.get(
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
checkIPAllowlist: false
}),
secretsController.getSecretByName
);
@ -257,8 +253,8 @@ router.patch(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -267,6 +263,7 @@ router.patch(
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
checkIPAllowlist: false
}),
secretsController.updateSecretByName
);
@ -284,8 +281,8 @@ router.delete(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -294,6 +291,7 @@ router.delete(
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
checkIPAllowlist: false
}),
secretsController.deleteSecretByName
);

View File

@ -21,7 +21,8 @@ router.post(
body("salt").exists().isString().trim().notEmpty(),
body("verifier").exists().isString().trim().notEmpty(),
body("organizationName").exists().isString().trim().notEmpty(),
body("providerAuthToken").isString().trim().optional({nullable: true}),
body("providerAuthToken").isString().trim().optional({ nullable: true }),
body("attributionSource").optional().isString().trim(),
validateRequest,
signupController.completeAccountSignup,
);

View File

@ -0,0 +1,12 @@
import { Types } from "mongoose";
import { getSymmetricKeyHelper } from "../helpers/botOrg";
// TODO: DOCstrings
class BotOrgService {
static async getSymmetricKey(organizationId: Types.ObjectId) {
return await getSymmetricKeyHelper(organizationId);
}
}
export default BotOrgService;

View File

@ -5,6 +5,7 @@ import {
getIsWorkspaceE2EEHelper,
getKey,
getSecretsBotHelper,
getSecretsCommentBotHelper,
} from "../helpers/bot";
/**
@ -107,6 +108,30 @@ class BotService {
tag,
});
}
/**
* Return decrypted secret comments for workspace with id [worskpaceId] and
* environment [environment] shared to bot.
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace of secrets
* @param {String} obj.environment - environment for secrets
* @returns {Object} secretObj - object where keys are secret keys and values are comments
*/
static async getSecretComments({
workspaceId,
environment,
secretPath
}: {
workspaceId: Types.ObjectId;
environment: string;
secretPath: string;
}) {
return await getSecretsCommentBotHelper({
workspaceId,
environment,
secretPath
});
}
}
export default BotService;

View File

@ -128,16 +128,19 @@ export default async (app: Probot) => {
const adminOrOwnerEmails = userEmails.map(userObject => userObject.email)
await sendMail({
template: "secretLeakIncident.handlebars",
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.full_name}`,
recipients: ["pusher.email", ...adminOrOwnerEmails],
substitutions: {
numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
pusher_email: pusher.email,
pusher_name: pusher.name
}
});
const usersToNotify = pusher?.email ? [pusher.email, ...adminOrOwnerEmails] : [...adminOrOwnerEmails]
if (Object.keys(allFindingsByFingerprint).length) {
await sendMail({
template: "secretLeakIncident.handlebars",
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.full_name}`,
recipients: usersToNotify,
substitutions: {
numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
pusher_email: pusher.email,
pusher_name: pusher.name
}
});
}
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {

View File

@ -0,0 +1,87 @@
import { Types } from "mongoose";
import Folder from "../models/folder";
import Secret, { ISecret } from "../models/secret";
import SecretImport from "../models/secretImports";
import { getFolderByPath } from "./FolderService";
type TSecretImportFid = { environment: string; folderId: string; secretPath: string };
export const getAllImportedSecrets = async (
workspaceId: string,
environment: string,
folderId = "root"
) => {
const secImports = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!secImports) return [];
if (secImports.imports.length === 0) return [];
const importedEnv: Record<string, boolean> = {}; // to get folders from all environment
secImports.imports.forEach((el) => (importedEnv[el.environment] = true));
const folders = await Folder.find({
workspace: workspaceId,
environment: { $in: Object.keys(importedEnv) }
});
const importedSecByFid: TSecretImportFid[] = [];
secImports.imports.forEach((el) => {
const folder = folders.find((fl) => fl.environment === el.environment);
if (folder) {
const secPathFolder = getFolderByPath(folder.nodes, el.secretPath);
if (secPathFolder)
importedSecByFid.push({
environment: el.environment,
folderId: secPathFolder.id,
secretPath: el.secretPath
});
} else {
if (el.secretPath === "/") {
// this happens when importing with a fresh env without any folders
importedSecByFid.push({ environment: el.environment, folderId: "root", secretPath: "/" });
}
}
});
if (importedSecByFid.length === 0) return [];
const secsGroupedByRef = await Secret.aggregate([
{
$match: {
workspace: new Types.ObjectId(workspaceId),
type: "shared"
}
},
{
$group: {
_id: {
environment: "$environment",
folderId: "$folder"
},
secrets: { $push: "$$ROOT" }
}
},
{
$match: {
$or: importedSecByFid.map(({ environment, folderId: fid }) => ({
"_id.environment": environment,
"_id.folderId": fid
}))
}
}
]);
// now let stitch together secrets.
const importedSecrets: Array<TSecretImportFid & { secrets: ISecret[] }> = [];
importedSecByFid.forEach(({ environment, folderId, secretPath }) => {
const secretsGrouped = secsGroupedByRef.find(
(el) => el._id.environment === environment && el._id.folderId === folderId
);
if (secretsGrouped) {
importedSecrets.push({ secretPath, folderId, environment, secrets: secretsGrouped.secrets });
}
});
return importedSecrets;
};

View File

@ -2,6 +2,7 @@ import DatabaseService from "./DatabaseService";
// import { logTelemetryMessage, getPostHogClient } from './TelemetryService';
import TelemetryService from "./TelemetryService";
import BotService from "./BotService";
import BotOrgService from "./BotOrgService";
import EventService from "./EventService";
import IntegrationService from "./IntegrationService";
import TokenService from "./TokenService";
@ -9,12 +10,13 @@ import SecretService from "./SecretService";
import GithubSecretScanningService from "./GithubSecretScanningService"
export {
TelemetryService,
DatabaseService,
BotService,
EventService,
IntegrationService,
TokenService,
SecretService,
GithubSecretScanningService
}
TelemetryService,
DatabaseService,
BotService,
BotOrgService,
EventService,
IntegrationService,
TokenService,
SecretService,
GithubSecretScanningService
}

View File

@ -20,6 +20,7 @@ declare global {
workspace: any;
membership: any;
targetMembership: any;
isUserCompleted: boolean;
providerAuthToken: any;
organization: any;
membershipOrg: any;

View File

@ -4,8 +4,6 @@ const ALGORITHM = "aes-256-gcm";
const BLOCK_SIZE_BYTES = 16;
export default class AesGCM {
constructor() {}
static encrypt(
text: string,
secret: string

View File

@ -1,11 +1,14 @@
import express from "express";
import passport from "passport";
import { Types } from "mongoose";
import { AuthData } from "../interfaces/middleware";
import {
AuthProvider,
MembershipOrg,
Organization,
ServiceAccount,
ServiceTokenData,
User,
User
} from "../models";
import { createToken } from "../helpers/auth";
import {
@ -14,11 +17,15 @@ import {
getJwtProviderAuthLifetime,
getJwtProviderAuthSecret,
} from "../config";
import { getSSOConfigHelper } from "../ee/helpers/organizations";
import { InternalServerError, OrganizationNotFoundError } from "./errors";
import { ACCEPTED, INVITED, MEMBER } from "../variables";
import { getSiteURL } from "../config";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const GoogleStrategy = require("passport-google-oauth20").Strategy;
// TODO: find a more optimal folder structure to store these types of functions
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { MultiSamlStrategy } = require("@node-saml/passport-saml");
/**
* Returns an object containing the id of the authentication data payload
@ -39,7 +46,6 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
}
};
/**
* Returns an object containing the user associated with the authentication data payload
* @param {AuthData} authData - authentication data object
@ -56,7 +62,7 @@ const getAuthDataPayloadUserObj = (authData: AuthData) => {
}
if (authData.authPayload instanceof ServiceTokenData) {
return { user: authData.authPayload.user };
return { user: authData.authPayload.user };0
}
}
@ -68,47 +74,191 @@ const initializePassport = async () => {
passReqToCallback: true,
clientID: googleClientId,
clientSecret: googleClientSecret,
callbackURL: "/api/v1/auth/callback/google",
callbackURL: "/api/v1/sso/google",
scope: ["profile", " email"],
}, async (
req: express.Request,
accessToken: string,
refreshToken: string,
profile: any,
cb: any
done: any
) => {
try {
const email = profile.emails[0].value;
const firstName = profile.name.givenName;
const lastName = profile.name.familyName;
let user = await User.findOne({
authProvider: AuthProvider.GOOGLE,
authId: profile.id,
}).select("+publicKey")
email
}).select("+publicKey");
if (user && user.authProvider !== AuthProvider.GOOGLE) {
done(InternalServerError());
}
if (!user) {
user = await new User({
email,
authProvider: AuthProvider.GOOGLE,
authId: profile.id,
firstName,
lastName
}).save();
}
const isUserCompleted = !!user.publicKey;
const providerAuthToken = createToken({
payload: {
userId: user._id.toString(),
email: user.email,
firstName,
lastName,
authProvider: user.authProvider,
isUserCompleted: !!user.publicKey,
isUserCompleted,
...(req.query.state ? {
callbackPort: req.query.state as string
} : {})
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(),
});
req.isUserCompleted = isUserCompleted;
req.providerAuthToken = providerAuthToken;
cb(null, profile);
done(null, profile);
} catch (err) {
cb(null, false);
done(null, false);
}
}));
passport.use("saml", new MultiSamlStrategy(
{
passReqToCallback: true,
getSamlOptions: async (req: any, done: any) => {
const { ssoIdentifier } = req.params;
const ssoConfig = await getSSOConfigHelper({
ssoConfigId: new Types.ObjectId(ssoIdentifier)
});
interface ISAMLConfig {
path: string;
callbackURL: string;
entryPoint: string;
issuer: string;
cert: string;
audience: string;
wantAuthnResponseSigned?: boolean;
}
const samlConfig: ISAMLConfig = ({
path: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`,
callbackURL: `${await getSiteURL()}/api/v1/sso/saml2${ssoIdentifier}`,
entryPoint: ssoConfig.entryPoint,
issuer: ssoConfig.issuer,
cert: ssoConfig.cert,
audience: await getSiteURL()
});
if (ssoConfig.authProvider === AuthProvider.JUMPCLOUD_SAML) {
samlConfig.wantAuthnResponseSigned = false;
}
req.ssoConfig = ssoConfig;
done(null, samlConfig);
},
},
async (req: any, profile: any, done: any) => {
if (!req.ssoConfig.isActive) return done(InternalServerError());
const organization = await Organization.findById(req.ssoConfig.organization);
if (!organization) return done(OrganizationNotFoundError());
const email = profile.email;
const firstName = profile.firstName;
const lastName = profile.lastName;
let user = await User.findOne({
email
}).select("+publicKey");
if (user) {
if (!user.authProvider || user.authProvider === AuthProvider.EMAIL || user.authProvider === AuthProvider.GOOGLE) {
await User.findByIdAndUpdate(
user._id,
{
authProvider: req.ssoConfig.authProvider
},
{
new: true
}
);
}
let membershipOrg = await MembershipOrg.findOne(
{
user: user._id,
organization: organization._id
}
);
if (!membershipOrg) {
membershipOrg = await new MembershipOrg({
inviteEmail: email,
user: user._id,
organization: organization._id,
role: MEMBER,
status: ACCEPTED
}).save();
}
if (membershipOrg.status === INVITED) {
membershipOrg.status = ACCEPTED;
await membershipOrg.save();
}
} else {
user = await new User({
email,
authProvider: req.ssoConfig.authProvider,
firstName,
lastName
}).save();
await new MembershipOrg({
inviteEmail: email,
user: user._id,
organization: organization._id,
role: MEMBER,
status: INVITED
}).save();
}
const isUserCompleted = !!user.publicKey;
const providerAuthToken = createToken({
payload: {
userId: user._id.toString(),
email: user.email,
firstName,
lastName,
organizationName: organization?.name,
authProvider: user.authProvider,
isUserCompleted,
...(req.body.RelayState ? {
callbackPort: req.body.RelayState as string
} : {})
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(),
});
req.isUserCompleted = isUserCompleted;
req.providerAuthToken = providerAuthToken;
done(null, profile);
}
));
}
export {

View File

@ -46,7 +46,7 @@ export const BadRequestError = (error?: Partial<RequestErrorContext>) => new Req
stack: error?.stack,
});
export const ResourceNotFound = (error?: Partial<RequestErrorContext>) => new RequestError({
export const ResourceNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO,
statusCode: error?.statusCode ?? 404,
type: error?.type ?? "resource_not_found",

View File

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

101
backend/src/utils/ip/ip.ts Normal file
View File

@ -0,0 +1,101 @@
import net from "net";
import { IPType } from "../../ee/models";
import { InternalServerError } from "../errors";
/**
* Return details of IP [ip]:
* - If [ip] is a specific IP address then return the IPv4/IPv6 address
* - If [ip] is a subnet then return the network IPv4/IPv6 address and prefix
* @param {String} ip - ip whose details to return
* @returns
*/
export const extractIPDetails = (ip: string) => {
if (net.isIPv4(ip)) return ({
ipAddress: ip,
type: IPType.IPV4
});
if (net.isIPv6(ip)) return ({
ipAddress: ip,
type: IPType.IPV6
});
const [ipNet, prefix] = ip.split("/");
let type;
switch (net.isIP(ipNet)) {
case 4:
type = IPType.IPV4;
break;
case 6:
type = IPType.IPV6;
break;
default:
throw InternalServerError({
message: "Failed to extract IP details"
});
}
return ({
ipAddress: ipNet,
type,
prefix: parseInt(prefix, 10)
});
}
/**
* Checks if a given string is a valid CIDR block.
*
* The function checks if the input string is a valid IPv4 or IPv6 address in CIDR notation.
*
* CIDR notation includes a network address followed by a slash ('/') and a prefix length.
* For IPv4, the prefix length must be between 0 and 32. For IPv6, it must be between 0 and 128.
* If the input string is not a valid CIDR block, the function returns `false`.
*
* @param {string} cidr - string in CIDR notation
* @returns {boolean} Returns `true` if the string is a valid CIDR block, `false` otherwise.
*
*/
export const isValidCidr = (cidr: string): boolean => {
const [ip, prefix] = cidr.split("/");
const prefixNum = parseInt(prefix, 10);
// ensure prefix exists and is a number within the appropriate range for each IP version
if (!prefix || isNaN(prefixNum) ||
(net.isIPv4(ip) && (prefixNum < 0 || prefixNum > 32)) ||
(net.isIPv6(ip) && (prefixNum < 0 || prefixNum > 128))) {
return false;
}
// ensure the IP portion of the CIDR block is a valid IPv4 or IPv6 address
if (!net.isIPv4(ip) && !net.isIPv6(ip)) {
return false;
}
return true;
}
/**
* Checks if a given string is a valid IPv4/IPv6 address or a valid CIDR block.
*
* If the string contains a slash ('/'), it treats the input as a CIDR block and checks its validity.
* Otherwise, it treats the string as a standalone IP address (either IPv4 or IPv6) and checks its validity.
*
* @param {string} input - The string to be checked. It could be an IP address or a CIDR block.
* @returns {boolean} Returns `true` if the string is a valid IP address (either IPv4 or IPv6) or a valid CIDR block, `false` otherwise.
*
*/
export const isValidIpOrCidr = (ip: string): boolean => {
// if the string contains a slash, treat it as a CIDR block
if (ip.includes("/")) {
return isValidCidr(ip);
}
// otherwise, treat it as a standalone IP address
if (net.isIPv4(ip) || net.isIPv6(ip)) {
return true;
}
return false;
}

View File

@ -3,13 +3,21 @@ import crypto from "crypto";
import { Types } from "mongoose";
import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto";
import { EESecretService } from "../../ee/services";
import { ISecretVersion, SecretSnapshot, SecretVersion } from "../../ee/models";
import {
IPType,
ISecretVersion,
SecretSnapshot,
SecretVersion,
TrustedIP
} from "../../ee/models";
import {
BackupPrivateKey,
Bot,
BotOrg,
ISecret,
Integration,
IntegrationAuth,
Organization,
Secret,
SecretBlindIndexData,
ServiceTokenData,
@ -137,6 +145,101 @@ export const backfillBots = async () => {
await Bot.insertMany(botsToInsert);
};
/**
* Backfill organization bots to ensure that every organization has a bot
*/
export const backfillBotOrgs = async () => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const organizationIdsWithBot = await BotOrg.distinct("organization");
const organizationIdsToAddBot = await Organization.distinct("_id", {
_id: {
$nin: organizationIdsWithBot
}
});
if (organizationIdsToAddBot.length === 0) return;
const botsToInsert = await Promise.all(
organizationIdsToAddBot.map(async (organizationToAddBot) => {
const { publicKey, privateKey } = generateKeyPair();
const key = client.createSymmetricKey();
if (rootEncryptionKey) {
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag
} = client.encryptSymmetric(key, rootEncryptionKey);
return new BotOrg({
name: "Infisical Bot",
organization: organizationToAddBot,
publicKey,
encryptedSymmetricKey,
symmetricKeyIV,
symmetricKeyTag,
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
symmetricKeyKeyEncoding: ENCODING_SCHEME_BASE64,
encryptedPrivateKey,
privateKeyIV,
privateKeyTag,
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
privateKeyKeyEncoding: ENCODING_SCHEME_BASE64
});
} else if (encryptionKey) {
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey,
key: encryptionKey
});
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: key,
key: encryptionKey
});
return new BotOrg({
name: "Infisical Bot",
organization: organizationToAddBot,
publicKey,
encryptedSymmetricKey,
symmetricKeyIV,
symmetricKeyTag,
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
symmetricKeyKeyEncoding: ENCODING_SCHEME_UTF8,
encryptedPrivateKey,
privateKeyIV,
privateKeyTag,
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
privateKeyKeyEncoding: ENCODING_SCHEME_UTF8
});
}
throw InternalServerError({
message: "Failed to backfill organization bots due to missing encryption key"
});
})
);
await BotOrg.insertMany(botsToInsert);
};
/**
* Backfill secret blind index data to ensure that every workspace
* has a secret blind index data
@ -387,7 +490,7 @@ export const backfillSecretFolders = async () => {
});
await SecretSnapshot.insertMany(newSnapshots);
await secSnapshot.delete();
await secSnapshot.deleteOne();
}
secretSnapshots = await SecretSnapshot.find({
@ -452,3 +555,79 @@ export const backfillServiceTokenMultiScope = async () => {
console.log("Migration: Service token migration v2 complete");
};
/**
* Backfill each workspace without any registered trusted IPs to
* have default trusted ip of 0.0.0.0/0
*/
export const backfillTrustedIps = async () => {
const workspaceIdsWithTrustedIps = await TrustedIP.distinct("workspace");
const workspaceIdsToAddTrustedIp = await Workspace.distinct("_id", {
_id: {
$nin: workspaceIdsWithTrustedIps
}
});
if (workspaceIdsToAddTrustedIp.length > 0) {
const operations: {
updateOne: {
filter: {
workspace: Types.ObjectId;
ipAddress: string;
},
update: {
workspace: Types.ObjectId;
ipAddress: string;
type: string;
prefix: number;
isActive: boolean;
comment: string;
},
upsert: boolean;
}
}[] = [];
workspaceIdsToAddTrustedIp.forEach((workspaceId) => {
// default IPv4 trusted CIDR
operations.push({
updateOne: {
filter: {
workspace: workspaceId,
ipAddress: "0.0.0.0"
},
update: {
workspace: workspaceId,
ipAddress: "0.0.0.0",
type: IPType.IPV4.toString(),
prefix: 0,
isActive: true,
comment: ""
},
upsert: true
}
});
// default IPv6 trusted CIDR
operations.push({
updateOne: {
filter: {
workspace: workspaceId,
ipAddress: "::"
},
update: {
workspace: workspaceId,
ipAddress: "::",
type: IPType.IPV6.toString(),
prefix: 0,
isActive: true,
comment: ""
},
upsert: true
}
});
});
await TrustedIP.bulkWrite(operations);
console.log("Backfill: Trusted IPs complete");
}
}

View File

@ -7,6 +7,7 @@ import { createTestUserForDevelopment } from "../addDevelopmentUser";
// eslint-disable-next-line @typescript-eslint/no-var-requires
import { validateEncryptionKeysConfig } from "./validateConfig";
import {
backfillBotOrgs,
backfillBots,
backfillEncryptionMetadata,
backfillIntegration,
@ -14,9 +15,14 @@ import {
backfillSecretFolders,
backfillSecretVersions,
backfillServiceToken,
backfillServiceTokenMultiScope
backfillServiceTokenMultiScope,
backfillTrustedIps
} from "./backfillData";
import { reencryptBotPrivateKeys, reencryptSecretBlindIndexDataSalts } from "./reencryptData";
import {
reencryptBotOrgKeys,
reencryptBotPrivateKeys,
reencryptSecretBlindIndexDataSalts
} from "./reencryptData";
import {
getClientIdGoogle,
getClientSecretGoogle,
@ -72,16 +78,19 @@ export const setup = async () => {
// backfilling data to catch up with new collections and updated fields
await backfillSecretVersions();
await backfillBots();
await backfillBotOrgs();
await backfillSecretBlindIndexData();
await backfillEncryptionMetadata();
await backfillSecretFolders();
await backfillServiceToken();
await backfillIntegration();
await backfillServiceTokenMultiScope();
await backfillTrustedIps();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY
await reencryptBotPrivateKeys();
await reencryptBotOrgKeys();
await reencryptSecretBlindIndexDataSalts();
// initializing Sentry

View File

@ -1,6 +1,8 @@
import {
Bot,
BotOrg,
IBot,
IBotOrg,
ISecretBlindIndexData,
SecretBlindIndexData,
} from "../../models";
@ -17,7 +19,7 @@ import {
} from "../../variables";
/**
* Re-encrypt bot private keys from hex 128-bit ENCRYPTION_KEY
* Re-encrypt bot private keys from under hex 128-bit ENCRYPTION_KEY
* to base64 256-bit ROOT_ENCRYPTION_KEY
*/
export const reencryptBotPrivateKeys = async () => {
@ -70,6 +72,79 @@ export const reencryptBotPrivateKeys = async () => {
}
}
/**
* Re-encrypt organization bot keys (symmetric and private) from under hex 128-bit ENCRYPTION_KEY
* to base64 256-bit ROOT_ENCRYPTION_KEY
*/
export const reencryptBotOrgKeys = async () => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
if (encryptionKey && rootEncryptionKey) {
// 1: re-encrypt organization bot keys under ROOT_ENCRYPTION_KEY
const botOrgs = await BotOrg.find({
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
symmetricKeyKeyEncoding: ENCODING_SCHEME_UTF8,
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
privateKeyKeyEncoding: ENCODING_SCHEME_UTF8
}).select("+encryptedPrivateKey iv tag algorithm keyEncoding");
if (botOrgs.length === 0) return;
const operationsBotOrg = await Promise.all(
botOrgs.map(async (botOrg: IBotOrg) => {
const privateKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: botOrg.encryptedPrivateKey,
iv: botOrg.privateKeyIV,
tag: botOrg.privateKeyTag,
key: encryptionKey
});
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag,
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
const symmetricKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: botOrg.encryptedSymmetricKey,
iv: botOrg.symmetricKeyIV,
tag: botOrg.symmetricKeyTag,
key: encryptionKey
});
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag,
} = client.encryptSymmetric(symmetricKey, rootEncryptionKey);
return ({
updateOne: {
filter: {
_id: botOrg._id,
},
update: {
encryptedSymmetricKey,
symmetricKeyIV,
symmetricKeyTag,
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
symmetricKeyKeyEncoding: ENCODING_SCHEME_BASE64,
encryptedPrivateKey,
privateKeyIV,
privateKeyTag,
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
privateKeyKeyEncoding: ENCODING_SCHEME_BASE64,
},
},
})
})
);
await BotOrg.bulkWrite(operationsBotOrg);
}
}
/**
* Re-encrypt secret blind index data salts from hex 128-bit ENCRYPTION_KEY
* to base64 256-bit ROOT_ENCRYPTION_KEY

View File

@ -1,14 +1,15 @@
import net from "net";
import { Types } from "mongoose";
import {
IServiceAccount,
IServiceTokenData,
IUser,
SecretBlindIndexData,
ServiceAccount,
ServiceTokenData,
User,
Workspace,
} from "../models";
import {
TrustedIP
} from "../ee/models";
import { validateServiceAccountClientForWorkspace } from "./serviceAccount";
import { validateUserClientForWorkspace } from "./user";
import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData";
@ -24,6 +25,8 @@ import {
AUTH_MODE_SERVICE_TOKEN,
} from "../variables";
import { BotService } from "../services";
import { AuthData } from "../interfaces/middleware";
import { extractIPDetails } from "../utils/ip";
/**
* Validate authenticated clients for workspace with id [workspaceId] based
@ -43,17 +46,16 @@ export const validateClientForWorkspace = async ({
requiredPermissions,
requireBlindIndicesEnabled,
requireE2EEOff,
checkIPAllowlist
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
authData: AuthData;
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<"admin" | "member">;
requiredPermissions?: string[];
requireBlindIndicesEnabled: boolean;
requireE2EEOff: boolean;
checkIPAllowlist: boolean;
}) => {
const workspace = await Workspace.findById(workspaceId);
@ -82,6 +84,8 @@ export const validateClientForWorkspace = async ({
message: "Failed workspace authorization due to end-to-end encryption not being disabled",
});
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
@ -107,6 +111,40 @@ export const validateClientForWorkspace = async ({
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
if (checkIPAllowlist) {
const trustedIps = await TrustedIP.find({
workspace: workspaceId
});
if (trustedIps.length > 0) {
// case: check the IP address of the inbound request against trusted IPs
const blockList = new net.BlockList();
for (const trustedIp of trustedIps) {
if (trustedIp.prefix !== undefined) {
blockList.addSubnet(
trustedIp.ipAddress,
trustedIp.prefix,
trustedIp.type
);
} else {
blockList.addAddress(
trustedIp.ipAddress,
trustedIp.type
);
}
}
const { type } = extractIPDetails(authData.authIP);
const check = blockList.check(authData.authIP, type);
if (!check) throw UnauthorizedRequestError({
message: "Failed workspace authorization"
});
}
}
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId,

View File

@ -1,5 +1,6 @@
import {
getClientIdAzure,
getClientIdBitBucket,
getClientIdGitHub,
getClientIdGitLab,
getClientIdHeroku,
@ -22,10 +23,18 @@ export const INTEGRATION_FLYIO = "flyio";
export const INTEGRATION_LARAVELFORGE = "laravel-forge"
export const INTEGRATION_CIRCLECI = "circleci";
export const INTEGRATION_TRAVISCI = "travisci";
export const INTEGRATION_TEAMCITY = "teamcity";
export const INTEGRATION_SUPABASE = "supabase";
export const INTEGRATION_CHECKLY = "checkly";
export const INTEGRATION_TERRAFORM_CLOUD = "terraform-cloud";
export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault";
export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages";
export const INTEGRATION_BITBUCKET = "bitbucket";
export const INTEGRATION_CODEFRESH = "codefresh";
export const INTEGRATION_WINDMILL = "windmill";
export const INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform";
export const INTEGRATION_CLOUD_66 = "cloud-66";
export const INTEGRATION_NORTHFLANK = "northflank";
export const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
@ -38,10 +47,18 @@ export const INTEGRATION_SET = new Set([
INTEGRATION_CIRCLECI,
INTEGRATION_LARAVELFORGE,
INTEGRATION_TRAVISCI,
INTEGRATION_TEAMCITY,
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH,
INTEGRATION_WINDMILL,
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CLOUD_66,
INTEGRATION_NORTHFLANK
]);
// integration types
@ -56,6 +73,7 @@ export const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/toke
export const INTEGRATION_GITHUB_TOKEN_URL =
"https://github.com/login/oauth/access_token";
export const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token";
export const INTEGRATION_BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token"
// integration apps endpoints
export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
@ -70,7 +88,14 @@ export const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
export const INTEGRATION_SUPABASE_API_URL = "https://api.supabase.com";
export const INTEGRATION_LARAVELFORGE_API_URL = "https://forge.laravel.com";
export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com";
export const INTEGRATION_TERRAFORM_CLOUD_API_URL = "https://app.terraform.io";
export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com";
export const INTEGRATION_BITBUCKET_API_URL = "https://api.bitbucket.org";
export const INTEGRATION_CODEFRESH_API_URL = "https://g.codefresh.io/api";
export const INTEGRATION_WINDMILL_API_URL = "https://app.windmill.dev/api";
export const INTEGRATION_DIGITAL_OCEAN_API_URL = "https://api.digitalocean.com";
export const INTEGRATION_CLOUD_66_API_URL = "https://app.cloud66.com/api";
export const INTEGRATION_NORTHFLANK_API_URL = "https://api.northflank.com";
export const getIntegrationOptions = async () => {
const INTEGRATION_OPTIONS = [
@ -192,6 +217,15 @@ export const getIntegrationOptions = async () => {
clientId: await getClientIdGitLab(),
docsLink: "",
},
{
name: "Terraform Cloud",
slug: "terraform-cloud",
image: "Terraform Cloud.png",
isAvailable: true,
type: "pat",
cliendId: "",
docsLink: "",
},
{
name: "Travis CI",
slug: "travisci",
@ -201,6 +235,15 @@ export const getIntegrationOptions = async () => {
clientId: "",
docsLink: "",
},
{
name: "TeamCity",
slug: "teamcity",
image: "TeamCity.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
},
{
name: "Supabase",
slug: "supabase",
@ -245,7 +288,61 @@ export const getIntegrationOptions = async () => {
type: "pat",
clientId: "",
docsLink: ""
}
},
{
name: "BitBucket",
slug: "bitbucket",
image: "BitBucket.png",
isAvailable: true,
type: "oauth",
clientId: await getClientIdBitBucket(),
docsLink: ""
},
{
name: "Codefresh",
slug: "codefresh",
image: "Codefresh.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
},
{
name: "Windmill",
slug: "windmill",
image: "Windmill.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
},
{
name: "Digital Ocean App Platform",
slug: "digital-ocean-app-platform",
image: "Digital Ocean.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
},
{
name: "Cloud 66",
slug: "cloud-66",
image: "Cloud 66.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
},
{
name: "Northflank",
slug: "northflank",
image: "Northflank.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
]
return INTEGRATION_OPTIONS;

View File

@ -235,6 +235,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId)
if request.IncludeImport {
httpRequest.SetQueryParam("include_imports", "true")
}
if request.SecretPath != "" {
httpRequest.SetQueryParam("secretPath", request.SecretPath)
}

View File

@ -272,40 +272,51 @@ type GetNewAccessTokenWithRefreshTokenResponse struct {
}
type GetEncryptedSecretsV3Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
IncludeImport bool `json:"include_imports"`
}
type EncryptedSecretV3 struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Workspace string `json:"workspace"`
} `json:"tags"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
Algorithm string `json:"algorithm"`
KeyEncoding string `json:"keyEncoding"`
Folder string `json:"folder"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type ImportedSecretV3 struct {
Environment string `json:"environment"`
FolderId string `json:"folderId"`
SecretPath string `json:"secretPath"`
Secrets []EncryptedSecretV3 `json:"secrets"`
}
type GetEncryptedSecretsV3Response struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Workspace string `json:"workspace"`
} `json:"tags"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
Algorithm string `json:"algorithm"`
KeyEncoding string `json:"keyEncoding"`
Folder string `json:"folder"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"secrets"`
Secrets []EncryptedSecretV3 `json:"secrets"`
ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`
}
type CreateSecretV3Request struct {

View File

@ -87,7 +87,12 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath})
includeImports, err := cmd.Flags().GetBool("include-imports")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports})
if err != nil {
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
@ -138,13 +143,13 @@ var runCmd = &cobra.Command{
err = executeMultipleCommandWithEnvs(command, len(secretsByKey), env)
if err != nil {
util.HandleError(err, "Unable to execute your chained command")
fmt.Println(err)
}
} else {
err = executeSingleCommandWithEnvs(args, len(secretsByKey), env)
if err != nil {
util.HandleError(err, "Unable to execute your single command")
fmt.Println(err)
}
}
},
@ -186,6 +191,7 @@ func init() {
runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
runCmd.Flags().Bool("include-imports", true, "Import linked secrets ")
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")

View File

@ -54,12 +54,17 @@ var secretsCmd = &cobra.Command{
util.HandleError(err)
}
includeImports, err := cmd.Flags().GetBool("include-imports")
if err != nil {
util.HandleError(err)
}
tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath})
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports})
if err != nil {
util.HandleError(err)
}
@ -647,6 +652,7 @@ func init() {
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ")
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
secretsCmd.Flags().String("path", "/", "get secrets within a folder path")
rootCmd.AddCommand(secretsCmd)

View File

@ -65,4 +65,5 @@ type GetAllSecretsParameters struct {
TagSlugs string
WorkspaceId string
SecretsPath string
IncludeImport bool
}

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