Compare commits

..

198 Commits

Author SHA1 Message Date
Maidul Islam
149c58fa3e cli: switch from v2 secrets to v3 2023-06-16 17:49:25 -04:00
Maidul Islam
965a5cc113 update rate limits 2023-06-16 10:03:12 -04:00
Tuan Dang
af31549309 Update pairing-session link 2023-06-16 01:15:24 +01:00
BlackMagiq
072e5013fc Merge pull request #653 from pgaijin66/bugfix/docs/remove-duplicate-api-key-header
bugfix(docs): remove duplicate api key header from API reference docu…
2023-06-16 00:54:20 +01:00
Prabesh Thapa
43f2cf8dc3 bugfix(docs): remove duplicate api key header from API reference documentation 2023-06-15 16:49:50 -07:00
vmatsiiako
0aca308bbd Update README.md 2023-06-15 15:01:52 -07:00
BlackMagiq
c77ebd4d0e Merge pull request #649 from Infisical/environment-paywall
Update implementation for environment limit paywall
2023-06-15 15:56:32 +01:00
Tuan Dang
ccaf9a9ffc Update implementation for environment limit paywall 2023-06-15 15:48:19 +01:00
Vladyslav Matsiiako
391e37d49e fixed bugs with env and password reset 2023-06-14 21:27:37 -07:00
Maidul Islam
7088b3c9d8 patch refresh token cli 2023-06-14 17:32:01 -04:00
Maidul Islam
ccf0877b81 Revert "Revert "add refresh token to cli""
This reverts commit 6b0e0f70d2.
2023-06-14 17:32:01 -04:00
Maidul Islam
0aa9390ece Merge pull request #647 from Budhathoki356/fix/typo
fix: minor typos in code
2023-06-14 14:51:44 -04:00
Maidul Islam
e47934a08a Merge branch 'main' into fix/typo 2023-06-14 14:47:22 -04:00
Eklal Budhathoki
04b7383bbe fix: minor typos in code 2023-06-15 00:17:00 +05:45
BlackMagiq
930b1e8d0c Merge pull request #645 from Infisical/environment-paywall
Update getPlan to consider the user's current workspace
2023-06-14 12:32:42 +01:00
Tuan Dang
82a026a426 Update refreshPlan to consider workspace 2023-06-14 12:28:01 +01:00
Tuan Dang
92647341a9 Update getPlan with workspace-specific consideration and add environmentLimit to returned plan 2023-06-14 11:52:48 +01:00
Maidul Islam
776cecc3ef create prod release action 2023-06-13 22:16:26 -04:00
Maidul Islam
a4fb2378bb wait for helm upgrade before mark complete 2023-06-13 22:06:53 -04:00
Maidul Islam
9742fdc770 rename docker image 2023-06-13 22:00:51 -04:00
Maidul Islam
786778fef6 isolate gamma environment 2023-06-13 21:56:15 -04:00
Maidul Islam
3f946180dd add terraform docs 2023-06-13 18:28:41 -04:00
Tuan Dang
3d70333f9c Update password-reset email response 2023-06-13 15:31:55 +01:00
Tuan Dang
f4404f66b8 Correct link to E2EE API usage example 2023-06-13 11:30:47 +01:00
BlackMagiq
9a62496d5c Merge pull request #641 from Infisical/improve-api-docs
Add REST API integration option to the introduction in docs
2023-06-13 11:26:53 +01:00
Tuan Dang
e24c1f38e0 Add REST API integration option in docs introduction 2023-06-13 11:23:13 +01:00
BlackMagiq
3ca9b7d6bf Merge pull request #640 from Infisical/improve-api-docs
Improve API docs for non-E2EE examples
2023-06-13 10:05:43 +01:00
Tuan Dang
37d2d580f4 Improve API docs for non-E2EE 2023-06-13 10:02:10 +01:00
Vladyslav Matsiiako
41dd2fda8a Changed the intercom to aprovider model 2023-06-12 21:42:29 -07:00
Vladyslav Matsiiako
22ca4f2e92 Fixed the typeerror issue 2023-06-12 20:56:19 -07:00
vmatsiiako
5882eb6f8a Merge pull request #639 from Infisical/intercom-tour
Switched intercom to AppLayout
2023-06-12 20:20:06 -07:00
Maidul Islam
c13d5e29f4 add intercom env replace during start up 2023-06-12 16:19:27 -07:00
Vladyslav Matsiiako
d99c54ca50 Switched intercom to layout 2023-06-12 15:38:12 -07:00
Tuan Dang
9dd0dac2f9 Patch frontend lint error 2023-06-12 18:07:15 +01:00
Tuan Dang
98efffafaa Patch subscription plan frontend validation 2023-06-12 17:47:32 +01:00
BlackMagiq
342ee50063 Merge pull request #638 from Infisical/non-e2ee-secrets
Add support for Encrypted Standard (ES) mode — i.e. read/write secrets in plaintext
2023-06-12 12:19:02 +01:00
Tuan Dang
553cf11ad2 Fix lint issue 2023-06-12 12:16:23 +01:00
Tuan Dang
4616cffecd Add support for read/write non-e2ee secrets 2023-06-12 12:04:28 +01:00
Vladyslav Matsiiako
39feb9a6ae Merge branch 'main' of https://github.com/Infisical/infisical 2023-06-11 19:24:38 -07:00
Vladyslav Matsiiako
82c1f8607d Added intercom 2023-06-11 19:23:30 -07:00
vmatsiiako
d4c3cbb53a Merge pull request #636 from mswider/self-hosted-env
Allow custom environments in self-hosted instances
2023-06-11 16:53:40 -07:00
mswider
1dea6749ba Allow custom environments in self-hosted instances 2023-06-11 18:19:01 -05:00
Tuan Dang
631eac803e Finish preliminary v3/secrets/raw endpoints 2023-06-11 12:11:25 +01:00
Tuan Dang
facabc683b Fix merge conflicts 2023-06-10 11:07:31 +01:00
BlackMagiq
4b99a9ea93 Merge pull request #633 from akhilmhdh/feat/folders-service-token
Folder scoped service token
2023-06-10 11:02:16 +01:00
akhilmhdh
445afb397c feat(folder-scoped-st): added batch,create secrets v2 secretpath support and service token 2023-06-10 12:10:43 +05:30
akhilmhdh
7d554f46d5 feat(folder-scoped-st): changed text css transformation in folders 2023-06-10 12:09:43 +05:30
Maidul Islam
bbef7d415c remove old commit 2023-06-09 18:41:10 -07:00
Maidul Islam
bb7b398fa7 throw unauthorized error instead of 500 for permission denied 2023-06-09 18:40:41 -07:00
Maidul Islam
570457c7c9 check path before service token create 2023-06-09 18:38:39 -07:00
Vladyslav Matsiiako
1b77b1d70b fixed the etxt issue 2023-06-09 17:02:41 -07:00
Vladyslav Matsiiako
0f697a91ab updated the workspace limit 2023-06-09 16:14:35 -07:00
Vladyslav Matsiiako
df6d23d1d3 fixed the ts error 2023-06-09 15:31:38 -07:00
Tuan Dang
0187d3012b Add non-e2ee option for getSecret, getSecrets, start createSecret 2023-06-09 21:20:12 +01:00
Vladyslav Matsiiako
4299a76fcd changed the default envs 2023-06-09 12:52:44 -07:00
Vladyslav Matsiiako
2bae6cf084 lots of frontend improvements 2023-06-09 12:50:17 -07:00
akhilmhdh
22beebc5d0 feat(folder-scoped-st): implemented frontend ui for folder scoped service token 2023-06-09 23:44:33 +05:30
akhilmhdh
6cb0a20675 feat(folder-scoped-st): implemented backend api for folder scoped service tokens 2023-06-09 23:44:33 +05:30
Tuan Dang
00fae0023a Add cluster URL image to docs for Vault integration 2023-06-09 15:57:47 +01:00
BlackMagiq
0377219a7a Merge pull request #632 from Infisical/vault-integration
Finish preliminary Vault integration, made docs for Vault and Checkly
2023-06-09 15:45:00 +01:00
Tuan Dang
00dfcfcf4e Finish preliminary Vault integration, made docs for Vault and Checkly 2023-06-09 15:36:37 +01:00
Vladyslav Matsiiako
f5441e9996 Merge branch 'main' of https://github.com/Infisical/infisical 2023-06-08 11:08:48 -07:00
Vladyslav Matsiiako
ee2fb33b50 changed the docs order 2023-06-08 11:08:27 -07:00
BlackMagiq
c51b194ba6 Merge pull request #629 from Infisical/optimize-checkly
Optimize Checkly integration
2023-06-08 11:21:28 +01:00
Tuan Dang
2920ba5195 Update Checkly envars only if changed 2023-06-08 11:18:23 +01:00
Tuan Dang
cd837b07aa Remove Sentry, part-try-catch from sync Checkly 2023-06-08 11:04:34 +01:00
BlackMagiq
a8e71e8170 Merge pull request #627 from Infisical/checkly-integration
Checkly integration
2023-06-08 10:56:19 +01:00
BlackMagiq
5fa96411d6 Merge branch 'main' into checkly-integration 2023-06-08 10:53:10 +01:00
Tuan Dang
329ab8ae61 Add +devices for verifyMfaToken user 2023-06-08 00:58:23 +01:00
Tuan Dang
3242d9b44e Fix change password button active state on no errors 2023-06-08 00:28:51 +01:00
Tuan Dang
8ce48fea43 Fix change password button active state on no errors 2023-06-08 00:27:59 +01:00
Maidul Islam
b011144258 reduce password forgot limit 2023-06-07 16:27:16 -07:00
Tuan Dang
674828e8e4 Copy data folder into backend build folder 2023-06-07 23:57:37 +01:00
Tuan Dang
c0563aff77 Bring back try-catch for initGlobalFeatureSet 2023-06-07 23:13:25 +01:00
BlackMagiq
7cec42a7fb Merge pull request #628 from Infisical/pentest-remediation
Fix issues/bugs
2023-06-07 22:52:08 +01:00
Tuan Dang
78493d9521 Fix lint errors 2023-06-07 22:47:47 +01:00
Vladyslav Matsiiako
49b3e8b538 comment fixes 2023-06-07 13:12:58 -07:00
Vladyslav Matsiiako
a3fca200fc comment fixes 2023-06-07 13:12:21 -07:00
Vladyslav Matsiiako
158eb584d2 integration with checkly done 2023-06-07 13:11:39 -07:00
Maidul Islam
e8bffb7217 Merge pull request #626 from akhilmhdh/fix/reload-submit
fix(ui): resolved reloading when form submission
2023-06-07 11:46:03 -07:00
akhilmhdh
604810ebd2 fix(ui): resolved reloading when form submission 2023-06-07 22:45:50 +05:30
Maidul Islam
d4108d1fab update email docs for self hosting 2023-06-07 10:13:43 -07:00
Tuan Dang
4d6ae0eef8 Merge remote-tracking branch 'origin' into pentest-remediation 2023-06-07 16:30:13 +01:00
BlackMagiq
8193490d7f Merge pull request #624 from Infisical/stabilize-server-try-catch
Bring back express-async-errors
2023-06-07 16:27:16 +01:00
Tuan Dang
0deba5e345 Bring back express-async-errors 2023-06-07 16:25:13 +01:00
Tuan Dang
a2055194c5 Fix merge conflicts 2023-06-07 13:12:54 +01:00
Tuan Dang
8c0d643a37 Fix merge conflicts 2023-06-07 12:58:24 +01:00
BlackMagiq
547a1fd142 Merge pull request #617 from Spelchure/removing-sentry-logs
feat: remove try-catch blocks for handling errors in middleware
2023-06-07 12:17:17 +01:00
Maidul Islam
04765ffb94 update email setup docs 2023-06-06 23:27:15 -07:00
Vladyslav Matsiiako
6b9aa200b5 login/signup styling fixes 2023-06-06 19:41:02 -07:00
Tuan Dang
5667e47b31 Add default rely on Cloudflare for IP addresses 2023-06-07 00:50:25 +01:00
Tuan Dang
a8ed187443 Add check for most common passwords 2023-06-07 00:06:35 +01:00
Tuan Dang
c5be497052 Strengthen password requirement 2023-06-06 23:06:44 +01:00
Maidul Islam
77d47e071b add folder id to versions in batch update 2023-06-06 13:31:08 -07:00
Maidul Islam
4bf2407d13 remove encryptionKey validation check 2023-06-06 09:43:11 -07:00
Tuan Dang
846f5c6680 Upgraded JWT invalidation/session logic to separate TokenVersion model. 2023-06-06 16:36:52 +01:00
BlackMagiq
6f1f07c9a5 Merge branch 'main' into removing-sentry-logs 2023-06-06 15:17:59 +01:00
Tuan Dang
aaca66e5a4 Patch support for ENCRYPTION_KEY and ROOT_ENCRYPTION_KEY in generateSecretBlindIndexHelper 2023-06-06 14:24:06 +01:00
Tuan Dang
b9dad5c3f0 Begin preliminary tokenVersion impl 2023-06-06 11:25:08 +01:00
Maidul Islam
3a79a855cb Merge pull request #622 from Infisical/folder-patch-v2
Patch backfill data
2023-06-05 23:14:14 -07:00
Maidul Islam
e28d0cbace bring back tags to secret version 2023-06-05 23:12:48 -07:00
Maidul Islam
c0fbe82ecb update populate number 2023-06-05 20:21:09 -07:00
Maidul Islam
b0e7304bff Patch backfill data 2023-06-05 20:19:15 -07:00
Tuan Dang
5a1b6acc93 Fix merge conflicts with auth changes 2023-06-05 21:27:01 +01:00
Tuan Dang
5f5ed5d0a9 Change export convention for helper functions 2023-06-05 21:00:23 +01:00
Spelchure
bfee0a6d30 feat: remove try-catch blocks for handling errors in middleware 2023-06-05 21:15:35 +03:00
Maidul Islam
08868681d8 Merge pull request #621 from akhilmhdh/fix/folder-breadcrumb
feat(folders): resolved auth issues and added the env dropdown change…
2023-06-05 09:51:59 -07:00
akhilmhdh
6dee858154 feat(folders): resolved auth issues and added the env dropdown change inside folders 2023-06-05 20:47:58 +05:30
Tuan Dang
0c18bd71c4 Implement preliminary pentest remediations 2023-06-05 00:44:10 +01:00
Maidul Islam
b9dfff1cd8 add migration complete logs 2023-06-04 16:37:57 -07:00
Maidul Islam
44b9533636 Merge pull request #609 from akhilmhdh/feat/folders
Feat/folders
2023-06-04 16:24:43 -07:00
Maidul Islam
599c8d94c9 Merge pull request #620 from Infisical/single-rate-limit-store
use mongo rate limit store
2023-06-04 13:45:56 -07:00
Maidul Islam
77788e1524 use mongo rate limit store 2023-06-04 13:43:57 -07:00
Maidul Islam
3df62a6e0a patch dup email bug for login 2023-06-04 11:07:52 -07:00
akhilmhdh
e74cc471db fix(folders): changed to secret path in controllers for get by path op 2023-06-04 13:26:20 +05:30
akhilmhdh
58d3f3945a feat(folders): removed old comments 2023-06-04 13:22:29 +05:30
akhilmhdh
29fa618bff feat(folders): changed / to root in breadcrumbs for folders 2023-06-04 13:18:05 +05:30
akhilmhdh
668b5a9cfd feat(folders): adopted new strategy for rollback on folders 2023-06-04 13:18:05 +05:30
akhilmhdh
6ce0f48b2c fix(folders): fixed algorithm missing in rollback versions and resolved env change reset folderid 2023-06-04 13:18:05 +05:30
Vladyslav Matsiiako
467e85b717 Minor style changes 2023-06-04 13:18:05 +05:30
akhilmhdh
579516bd38 feat(folders): implemented ui for folders in dashboard 2023-06-04 13:18:05 +05:30
akhilmhdh
deaa85cbe7 feat(folders): added support for snapshot by env and folder 2023-06-04 13:18:05 +05:30
Vladyslav Matsiiako
08a4404fed fixed the email issue 2023-06-03 13:58:53 -07:00
Maidul Islam
73aa01c568 prevent passport init when envs are undefined 2023-06-03 12:20:04 -07:00
vmatsiiako
b926601a29 Merge pull request #535 from sheensantoscapadngan/feature/google-signin-signup-integration
Feature/google signin signup integration
2023-06-02 23:44:45 -07:00
Maidul Islam
f619ee7297 revise self hosting order 2023-06-02 14:10:40 -07:00
Maidul Islam
bb825c3d68 add DO docs link 2023-06-02 14:06:04 -07:00
Maidul Islam
6bbd7f05a2 add digital ocean docs 2023-06-02 14:05:00 -07:00
Vladyslav Matsiiako
4865b69e6d updated the slack link 2023-06-01 13:54:02 -07:00
Vladyslav Matsiiako
6f3c7c0fbf fix ts issues 2023-05-31 11:29:08 -07:00
Vladyslav Matsiiako
be80b5124f Final style changes to login/signup 2023-05-31 11:25:37 -07:00
BlackMagiq
4232776637 Merge pull request #614 from Infisical/check-in-process
Update contributing docs to include expectations and procedures for submitting pull requests
2023-05-31 12:46:21 +03:00
Tuan Dang
2c12ede694 Add note for prioritization of first 3 PRs for contribution due to high volume of issues, PRs, and other initiatives in the pipeline 2023-05-31 12:44:30 +03:00
Tuan Dang
94141fedd6 Update contributing docs, add pull requests section 2023-05-31 12:25:08 +03:00
BlackMagiq
720ab446f9 Merge pull request #612 from Infisical/migration-script
Add migration script for server key re-encryption
2023-05-30 21:03:14 +03:00
Tuan Dang
1a1693dbbf Add encryption key validation to validation script 2023-05-30 20:46:20 +03:00
Tuan Dang
9440afa386 Remove re-encryption from Infisical, move to migration script 2023-05-30 20:30:58 +03:00
Maidul Islam
8b1ec1424d patch quote type in docs 2023-05-30 11:19:15 -04:00
Maidul Islam
bd56f3b64c update docker run command for self host 2023-05-30 11:14:22 -04:00
Sheen Capadngan
f20ea723b7 Merge branch 'main' into feature/google-signin-signup-integration 2023-05-30 20:46:20 +08:00
Maidul Islam
86f76ebe70 no default user for selfhosting docs 2023-05-30 08:39:43 -04:00
Maidul Islam
821385c2f3 Revert "add prod img publish ste p"
This reverts commit f7dbd41431.
2023-05-30 07:29:45 -04:00
Maidul Islam
03c65c8635 Revert "add dummy step"
This reverts commit 893c4777fe.
2023-05-30 07:29:31 -04:00
Maidul Islam
893c4777fe add dummy step 2023-05-29 18:49:14 -04:00
Maidul Islam
f7dbd41431 add prod img publish ste p 2023-05-29 18:46:04 -04:00
Maidul Islam
8d1f3e930a revert keychain name 2023-05-29 18:08:49 -04:00
Tuan Dang
37251ed607 Begin migration script for re-encryption 2023-05-29 23:46:16 +03:00
Sheen Capadngan
e6027b3c72 removed try catch from requireAuth middleware 2023-05-29 18:14:16 +08:00
Sheen Capadngan
7f4db518cc Merge branch 'main' into feature/google-signin-signup-integration 2023-05-29 18:00:43 +08:00
Vladyslav Matsiiako
d4f65e23c7 Merge branch 'feature/google-signin-signup-integration' of https://github.com/sheensantoscapadngan/infisical into feature/google-signin-signup-integration 2023-05-28 14:22:42 -07:00
Vladyslav Matsiiako
50609d06f5 Final style changes to signup 2023-05-28 14:22:37 -07:00
Vladyslav Matsiiako
3a5ad93450 Final style changes to signup 2023-05-28 14:22:17 -07:00
Sheen Capadngan
8e972c704a resolved error handling issue with requireAuth middleware 2023-05-28 23:06:20 +08:00
Sheen Capadngan
b975115443 Merge branch 'main' into feature/google-signin-signup-integration 2023-05-28 22:12:02 +08:00
Sheen Capadngan
fac4968193 moved oauth controller endpoints to auth 2023-05-24 23:44:30 +08:00
Sheen Capadngan
93cf7cde2d fixed login issue after mfa 2023-05-24 21:33:24 +08:00
Sheen Capadngan
422d04d7d7 migrated to standard request 2023-05-24 18:50:59 +08:00
Sheen Capadngan
4c41d279e9 Merge branch 'main' into feature/google-signin-signup-integration 2023-05-24 18:39:18 +08:00
Sheen Capadngan
51914c6a2e resolved package-lock conflicts 2023-05-22 22:07:30 +08:00
Sheen Capadngan
ad37a14f2e Merge branch 'main' into feature/google-signin-signup-integration 2023-05-22 21:54:51 +08:00
Sheen Capadngan
bc61de4a80 add provider auth secret to kubernetes and docker yaml 2023-05-20 23:15:36 +08:00
Sheen Capadngan
4367822777 re-added token caching and redirection 2023-05-18 23:04:55 +08:00
Sheen Capadngan
ca4a9b9937 resolved MFA not appearing 2023-05-18 02:07:07 +08:00
Sheen Capadngan
ec8d62d106 show toast when oauth login error 2023-05-18 01:58:35 +08:00
Sheen Capadngan
6ca3b8ba61 handled error cases for external auth login 2023-05-18 00:39:20 +08:00
Sheen Capadngan
4b2e91da74 added proper error handling for user creation 2023-05-18 00:12:28 +08:00
Sheen Capadngan
fac8affe78 added missing envs for documentation 2023-05-17 22:24:04 +08:00
Sheen Capadngan
1ccec486cc removed caching of providerAuthToken 2023-05-17 21:43:18 +08:00
Sheen Capadngan
6746f04f33 added self-hosting documentation for google 2023-05-15 23:39:19 +08:00
Sheen Capadngan
dba19b4a1d Merge branch 'main' into feature/google-signin-signup-integration 2023-05-15 20:41:08 +08:00
Sheen Capadngan
884aed74a5 made last name optional 2023-05-15 20:39:45 +08:00
Sheen Capadngan
9dc7cc58a7 uncommented code 2023-05-15 00:41:27 +08:00
Sheen Capadngan
6f66b56e7c updated package-lock 2023-05-12 22:24:38 +08:00
Sheen Capadngan
be2bac41bb Merge branch 'main' into feature/google-signin-signup-integration 2023-05-12 22:22:51 +08:00
Sheen Capadngan
0afa44a9f0 removed express-session types 2023-05-11 15:39:04 +08:00
Vladyslav Matsiiako
5a99878d15 Final style edits to the login and signup flows 2023-05-10 16:12:57 -07:00
Vladyslav Matsiiako
0d3e7f3c0c Merge branch 'feature/google-signin-signup-integration' of https://github.com/sheensantoscapadngan/infisical into feature/google-signin-signup-integration 2023-05-10 12:55:19 -07:00
Vladyslav Matsiiako
967e520173 More designed changes to login flow 2023-05-10 12:55:08 -07:00
Sheen Capadngan
ccfe0b1eb9 reverted changes made to nginx config 2023-05-11 00:52:49 +08:00
Sheen Capadngan
0ef5779776 add providerAuthToken for MFA login 2023-05-11 00:47:16 +08:00
Sheen Capadngan
a194e90644 removed session references 2023-05-11 00:41:38 +08:00
Sheen Capadngan
addc849fa6 changed google-auth strategy and removed session use 2023-05-11 00:37:02 +08:00
Sheen Capadngan
074c0bdd77 utilized mongodb as persistent store for sessions 2023-05-10 23:01:44 +08:00
Sheen Capadngan
7ee33e9393 resolved merge conflict issues and updated use of translations 2023-05-10 21:39:11 +08:00
Sheen Capadngan
32cef27e8e Merge branch 'main' into feature/google-sign 2023-05-10 21:11:42 +08:00
Vladyslav Matsiiako
1fce8cc769 More style changes to login 2023-05-09 23:24:37 -07:00
Vladyslav Matsiiako
4e7145dfe5 Style changes to login 2023-05-09 20:45:59 -07:00
Sheen Capadngan
4c434555a4 finalized signup/signin ux regarding redirects 2023-05-07 20:54:30 +08:00
Sheen Capadngan
ea86e59d4f resolved component alignment of signup 2023-05-06 19:26:42 +08:00
Sheen Capadngan
3e19e6fd99 finalized login and signup ui 2023-05-06 18:52:01 +08:00
Sheen Capadngan
b734b51954 developed new ui for new login and signup page 2023-05-05 00:39:28 +08:00
Sheen Capadngan
1172726e74 added signup v3 endpoints and developed initial new signup flow 2023-05-04 01:32:40 +08:00
Sheen Capadngan
f703ee29e5 implemented comments 2023-05-03 18:58:32 +08:00
Sheen Capadngan
dfb84e9932 developed initial version of new login page 2023-04-27 23:10:27 +08:00
Sheen Capadngan
2dd1570200 updated use of environment variables to utilize await 2023-04-27 02:00:16 +08:00
Sheen Capadngan
69472514af Merge branch 'main' into feature/google-signin-signup-integration 2023-04-27 01:46:26 +08:00
Sheen Capadngan
f956170820 added auth v3 endpoints for login1 and login2 2023-04-27 01:38:06 +08:00
Sheen Capadngan
007e8c4442 initial setup for google signin signup integration 2023-04-25 23:47:46 +08:00
318 changed files with 27439 additions and 18090 deletions

View File

@@ -9,6 +9,7 @@ JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201
# JWT lifetime
# Optional lifetimes for JWT tokens expressed in seconds or a string
@@ -16,6 +17,7 @@ JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
JWT_AUTH_LIFETIME=
JWT_REFRESH_LIFETIME=
JWT_SIGNUP_LIFETIME=
JWT_PROVIDER_AUTH_LIFETIME=
# MongoDB
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
@@ -66,3 +68,6 @@ STRIPE_PRODUCT_STARTER=
STRIPE_PRODUCT_TEAM=
STRIPE_PRODUCT_PRO=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
CLIENT_ID_GOOGLE=
CLIENT_SECRET_GOOGLE=

BIN
.github/images/deploy-aws-button.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
.github/images/do-k8-install-btn.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

4
.github/values.yaml vendored
View File

@@ -6,7 +6,7 @@ frontend:
secrets.infisical.com/auto-reload: "true"
replicaCount: 2
image:
repository: infisical/frontend
repository: infisical/staging_deployment_frontend
tag: "latest"
pullPolicy: Always
kubeSecretRef: managed-secret-frontend
@@ -25,7 +25,7 @@ backend:
secrets.infisical.com/auto-reload: "true"
replicaCount: 2
image:
repository: infisical/backend
repository: infisical/staging_deployment_backend
tag: "latest"
pullPolicy: Always
kubeSecretRef: managed-backend-secret

View File

@@ -0,0 +1,118 @@
name: Release production images (frontend, backend)
on:
push:
tags:
- "infisical/v*.*.*"
jobs:
backend-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: 🧪 Run tests
run: npm run test:ci
working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true
context: backend
tags: infisical/backend:test
- name: ⏻ Spawn backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
- name: 🧪 Test backend image
run: |
./.github/resources/healthcheck.sh infisical-backend-test
- name: ⏻ Shut down backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml down
- name: 🏗️ Build backend and push
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: backend
tags: |
infisical/backend:${{ steps.commit.outputs.short }}
infisical/backend:latest
infisical/backend:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
frontend-image:
name: Build frontend image
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build frontend and export to Docker
uses: depot/build-push-action@v1
with:
load: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
project: 64mmf0n610
context: frontend
tags: infisical/frontend:test
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
- name: ⏻ Spawn frontend container
run: |
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
- name: 🧪 Test frontend image
run: |
./.github/resources/healthcheck.sh infisical-frontend-test
- name: ⏻ Shut down frontend container
run: |
docker stop infisical-frontend-test
- name: 🏗️ Build frontend and push
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend
tags: |
infisical/frontend:${{ steps.commit.outputs.short }}
infisical/frontend:latest
infisical/frontend:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}

View File

@@ -1,17 +1,11 @@
name: Build, Publish and Deploy to Gamma
on:
push:
tags:
- "infisical/v*.*.*"
on: [workflow_dispatch]
jobs:
backend-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
@@ -57,18 +51,14 @@ jobs:
push: true
context: backend
tags: |
infisical/backend:${{ steps.commit.outputs.short }}
infisical/backend:latest
infisical/backend:${{ steps.extract_version.outputs.version }}
infisical/staging_deployment_backend:${{ steps.commit.outputs.short }}
infisical/staging_deployment_backend:latest
platforms: linux/amd64,linux/arm64
frontend-image:
name: Build frontend image
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Save commit hashes for tag
@@ -90,12 +80,12 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
project: 64mmf0n610
context: frontend
tags: infisical/frontend:test
tags: infisical/staging_deployment_frontend:test
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
- name: ⏻ Spawn frontend container
run: |
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test
- name: 🧪 Test frontend image
run: |
./.github/resources/healthcheck.sh infisical-frontend-test
@@ -110,9 +100,8 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend
tags: |
infisical/frontend:${{ steps.commit.outputs.short }}
infisical/frontend:latest
infisical/frontend:${{ steps.extract_version.outputs.version }}
infisical/staging_deployment_frontend:${{ steps.commit.outputs.short }}
infisical/staging_deployment_frontend:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
@@ -146,7 +135,7 @@ jobs:
- name: Download helm values to file and upgrade gamma deploy
run: |
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --recreate-pods
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --wait
if [[ $(helm status infisical) == *"FAILED"* ]]; then
echo "Helm upgrade failed"
exit 1

View File

@@ -25,6 +25,8 @@ ARG POSTHOG_HOST
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
# Build
RUN npm run build
@@ -42,6 +44,9 @@ VOLUME /app/.next/cache/images
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public

View File

@@ -7,7 +7,7 @@
</p>
<h4 align="center">
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">Slack</a> |
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg">Slack</a> |
<a href="https://infisical.com/">Infisical Cloud</a> |
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
<a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> |
@@ -25,9 +25,9 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a>
<a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-240.2k-orange" alt="Cloudsmith downloads" />
<img src="https://img.shields.io/badge/Downloads-305.8k-orange" alt="Cloudsmith downloads" />
</a>
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
</a>
<a href="https://twitter.com/infisical">
@@ -69,7 +69,9 @@ The fastest and most reliable way to get started with Infisical is signing up fo
### Deploy Infisical on premise
Deployment options: [AWS EC2](https://infisical.com/docs/self-hosting/overview), [Kubernetes](https://infisical.com/docs/self-hosting/overview), and [more](https://infisical.com/docs/self-hosting/overview).
<a href="https://infisical.com/docs/self-hosting/deployment-options/digital-ocean-marketplace"><img src=".github/images/do-k8-install-btn.png" width="200"/></a> <a href="https://infisical.com/docs/self-hosting/deployment-options/aws-ec2"><img src=".github/images/deploy-aws-button.png" width="150" width="300" /></a>
View all [deployment options](https://infisical.com/docs/self-hosting/overview)
### Run Infisical locally
@@ -125,13 +127,13 @@ Whether it's big or small, we love contributions. Check out our guide to see how
Not sure where to get started? You can:
- [Book a free, non-pressure pairing sessions with one of our teammates](mailto:tony@infisical.com?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)!
- Join our <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">Slack</a>, and ask us any questions there.
- [Book a free, non-pressure pairing session / code walkthrough with one of our teammates](https://cal.com/tony-infisical/30-min-meeting-contributing)!
- Join our <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg">Slack</a>, and ask us any questions there.
## Resources
- [Docs](https://infisical.com/docs/documentation/getting-started/introduction) for comprehensive documentation and guides
- [Slack](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g) for discussion with the community and Infisical team.
- [Slack](https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg) for discussion with the community and Infisical team.
- [GitHub](https://github.com/Infisical/infisical) for code, issues, and pull requests
- [Twitter](https://twitter.com/infisical) for fast news
- [YouTube](https://www.youtube.com/@infisical5306) for videos on secret management

7523
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -32,11 +32,14 @@
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.10.5",
"nanoid": "^3.3.6",
"node-cache": "^5.1.2",
"nodemailer": "^6.8.0",
"passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0",
"query-string": "^7.1.3",
"request-ip": "^3.3.0",
"rate-limit-mongo": "^2.3.2",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
"swagger-autogen": "^2.22.0",
@@ -55,7 +58,7 @@
"start": "node build/index.js",
"dev": "nodemon",
"swagger-autogen": "node ./swagger/index.ts",
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build && cp -R ./src/data ./build",
"lint": "eslint . --ext .ts",
"lint-and-fix": "eslint . --ext .ts --fix",
"lint-staged": "lint-staged",
@@ -88,6 +91,7 @@
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6",
"@types/passport": "^1.0.12",
"@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",

View File

@@ -23,6 +23,8 @@ export const getJwtRefreshLifetime = async () => (await client.getSecret('JWT_RE
export const getJwtRefreshSecret = async () => (await client.getSecret('JWT_REFRESH_SECRET')).secretValue;
export const getJwtServiceSecret = async () => (await client.getSecret('JWT_SERVICE_SECRET')).secretValue;
export const getJwtSignupLifetime = async () => (await client.getSecret('JWT_SIGNUP_LIFETIME')).secretValue || '15m';
export const getJwtProviderAuthSecret = async () => (await client.getSecret('JWT_PROVIDER_AUTH_SECRET')).secretValue;
export const getJwtProviderAuthLifetime = async () => (await client.getSecret('JWT_PROVIDER_AUTH_LIFETIME')).secretValue || '15m';
export const getJwtSignupSecret = async () => (await client.getSecret('JWT_SIGNUP_SECRET')).secretValue;
export const getMongoURL = async () => (await client.getSecret('MONGO_URL')).secretValue;
export const getNodeEnv = async () => (await client.getSecret('NODE_ENV')).secretValue || 'production';
@@ -34,12 +36,14 @@ export const getClientIdVercel = async () => (await client.getSecret('CLIENT_ID_
export const getClientIdNetlify = async () => (await client.getSecret('CLIENT_ID_NETLIFY')).secretValue;
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 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;
export const getClientSecretNetlify = async () => (await client.getSecret('CLIENT_SECRET_NETLIFY')).secretValue;
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 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

@@ -1,19 +1,28 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import jwt from 'jsonwebtoken';
import * as bigintConversion from 'bigint-conversion';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models';
import {
User,
LoginSRPDetail,
TokenVersion
} from '../../models';
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
import { checkUserDevice } from '../../helpers/user';
import {
ACTION_LOGIN,
ACTION_LOGOUT
ACTION_LOGOUT,
AUTH_MODE_JWT
} from '../../variables';
import { BadRequestError } from '../../utils/errors';
import {
BadRequestError,
UnauthorizedRequestError
} from '../../utils/errors';
import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import { getChannelFromUserAgent } from '../../utils/posthog';
import {
getJwtRefreshSecret,
getJwtAuthLifetime,
@@ -24,6 +33,7 @@ import {
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string;
refreshVersion?: number;
}
}
@@ -34,47 +44,39 @@ declare module 'jsonwebtoken' {
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
try {
const {
email,
clientPublicKey
}: { email: string; clientPublicKey: string } = req.body;
const {
email,
clientPublicKey
}: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({
email
}).select('+salt +verifier');
const user = await User.findOne({
email
}).select('+salt +verifier');
if (!user) throw new Error('Failed to find user');
if (!user) throw new Error('Failed to find user');
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: email }, {
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false })
await LoginSRPDetail.findOneAndReplace({ email: email }, {
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false })
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
};
/**
@@ -85,84 +87,80 @@ export const login1 = async (req: Request, res: Response) => {
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
try {
const { email, clientProof } = req.body;
const user = await User.findOne({
email
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
const { email, clientProof } = req.body;
const user = await User.findOne({
email
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
if (!user) throw new Error('Failed to find user');
if (!user) throw new Error('Failed to find user');
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email })
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email })
if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
}
if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// issue tokens
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// issue tokens
await checkUserDevice({
user,
ip: req.ip,
userAgent: req.headers['user-agent'] ?? ''
});
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
const tokens = await issueAuthTokens({ userId: user._id.toString() });
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.realIP
});
// return (access) token in response
return res.status(200).send({
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
});
}
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
// return (access) token in response
return res.status(200).send({
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
);
};
/**
@@ -172,44 +170,59 @@ export const login2 = async (req: Request, res: Response) => {
* @returns
*/
export const logout = async (req: Request, res: Response) => {
try {
await clearTokens({
userId: req.user._id.toString()
});
// clear httpOnly cookie
res.cookie('jid', '', {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: (await getHttpsEnabled()) as boolean
});
const logoutAction = await EELogService.createAction({
name: ACTION_LOGOUT,
userId: req.user._id
});
logoutAction && await EELogService.createLog({
userId: req.user._id,
actions: [logoutAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to logout'
});
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId)
}
// clear httpOnly cookie
res.cookie('jid', '', {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: (await getHttpsEnabled()) as boolean
});
const logoutAction = await EELogService.createAction({
name: ACTION_LOGOUT,
userId: req.user._id
});
logoutAction && await EELogService.createLog({
userId: req.user._id,
actions: [logoutAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.realIP
});
return res.status(200).send({
message: 'Successfully logged out.'
});
};
export const getCommonPasswords = async (req: Request, res: Response) => {
const commonPasswords = fs.readFileSync(
path.resolve(__dirname, '../../data/' + 'common_passwords.txt'),
'utf8'
).split('\n');
return res.status(200).send(commonPasswords);
}
export const revokeAllSessions = async (req: Request, res: Response) => {
await TokenVersion.updateMany({
user: req.user._id
}, {
$inc: {
refreshVersion: 1,
accessVersion: 1
}
});
return res.status(200).send({
message: 'Successfully revoked all sessions.'
});
}
/**
* Return user is authenticated
* @param req
@@ -223,47 +236,55 @@ export const checkAuth = async (req: Request, res: Response) => {
}
/**
* Return new token by redeeming refresh token
* Return new JWT access token by first validating the refresh token
* @param req
* @param res
* @returns
*/
export const getNewToken = async (req: Request, res: Response) => {
try {
const refreshToken = req.cookies.jid;
const refreshToken = req.cookies.jid;
if (!refreshToken) {
throw new Error('Failed to find token in request cookies');
}
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(refreshToken, await getJwtRefreshSecret())
);
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
if (!user) throw new Error('Failed to authenticate unfound user');
if (!user?.publicKey)
throw new Error('Failed to authenticate not fully set up account');
const token = createToken({
payload: {
userId: decodedToken.userId
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret()
});
return res.status(200).send({
token
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Invalid request'
});
if (!refreshToken) {
throw new Error('Failed to find refresh token in request cookies');
}
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(refreshToken, await getJwtRefreshSecret())
);
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey +refreshVersion +accessVersion');
if (!user) throw new Error('Failed to authenticate unfound user');
if (!user?.publicKey)
throw new Error('Failed to authenticate not fully set up account');
const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId);
if (!tokenVersion) throw UnauthorizedRequestError({
message: 'Failed to validate refresh token'
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({
message: 'Failed to validate refresh token'
});
const token = createToken({
payload: {
userId: decodedToken.userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.refreshVersion
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret()
});
return res.status(200).send({
token
});
};
export const handleAuthProviderCallback = (req: Request, res: Response) => {
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
}

View File

@@ -1,6 +1,5 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import { Bot, BotKey } from '../../models';
import { createBot } from '../../helpers/bot';
@@ -17,33 +16,24 @@ interface BotKey {
* @returns
*/
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
let bot;
try {
const { workspaceId } = req.params;
const { workspaceId } = req.params;
bot = await Bot.findOne({
workspace: workspaceId
});
if (!bot) {
// case: bot doesn't exist for workspace with id [workspaceId]
// -> create a new bot and return it
bot = await createBot({
name: 'Infisical Bot',
workspaceId: new Types.ObjectId(workspaceId)
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get bot for workspace'
});
}
let bot = await Bot.findOne({
workspace: workspaceId
});
if (!bot) {
// case: bot doesn't exist for workspace with id [workspaceId]
// -> create a new bot and return it
bot = await createBot({
name: 'Infisical Bot',
workspaceId: new Types.ObjectId(workspaceId)
});
}
return res.status(200).send({
bot
});
return res.status(200).send({
bot
});
};
/**
@@ -53,54 +43,44 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
* @returns
*/
export const setBotActiveState = async (req: Request, res: Response) => {
let bot;
try {
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
if (isActive) {
// bot state set to active -> share workspace key with bot
if (!botKey?.encryptedKey || !botKey?.nonce) {
return res.status(400).send({
message: 'Failed to set bot state to active - missing bot key'
});
}
await BotKey.findOneAndUpdate({
workspace: req.bot.workspace
}, {
encryptedKey: botKey.encryptedKey,
nonce: botKey.nonce,
sender: req.user._id,
bot: req.bot._id,
workspace: req.bot.workspace
}, {
upsert: true,
new: true
});
} else {
// case: bot state set to inactive -> delete bot's workspace key
await BotKey.deleteOne({
bot: req.bot._id
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
if (isActive) {
// bot state set to active -> share workspace key with bot
if (!botKey?.encryptedKey || !botKey?.nonce) {
return res.status(400).send({
message: 'Failed to set bot state to active - missing bot key'
});
}
bot = await Bot.findOneAndUpdate({
_id: req.bot._id
await BotKey.findOneAndUpdate({
workspace: req.bot.workspace
}, {
isActive
encryptedKey: botKey.encryptedKey,
nonce: botKey.nonce,
sender: req.user._id,
bot: req.bot._id,
workspace: req.bot.workspace
}, {
upsert: true,
new: true
});
if (!bot) throw new Error('Failed to update bot active state');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update bot active state'
});
}
} else {
// case: bot state set to inactive -> delete bot's workspace key
await BotKey.deleteOne({
bot: req.bot._id
});
}
let bot = await Bot.findOneAndUpdate({
_id: req.bot._id
}, {
isActive
}, {
new: true
});
if (!bot) throw new Error('Failed to update bot active state');
return res.status(200).send({
bot

View File

@@ -1,6 +1,5 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
IntegrationAuth,
Bot
@@ -22,22 +21,13 @@ import { standardRequest } from '../../config/request';
* Return integration authorization with id [integrationAuthId]
*/
export const getIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth;
try {
const { integrationAuthId } = req.params;
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) return res.status(400).send({
message: 'Failed to find integration authorization'
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get integration authorization'
});
}
const { integrationAuthId } = req.params;
const integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) return res.status(400).send({
message: 'Failed to find integration authorization'
});
return res.status(200).send({
integrationAuth
});
@@ -61,33 +51,25 @@ export const oAuthExchange = async (
req: Request,
res: Response
) => {
try {
const { workspaceId, code, integration } = req.body;
if (!INTEGRATION_SET.has(integration))
throw new Error('Failed to validate integration');
const environments = req.membership.workspace?.environments || [];
if(environments.length === 0){
throw new Error("Failed to get environments")
}
const integrationAuth = await IntegrationService.handleOAuthExchange({
workspaceId,
integration,
code,
environment: environments[0].slug,
});
return res.status(200).send({
integrationAuth
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get OAuth2 code-token exchange'
});
}
const { workspaceId, code, integration } = req.body;
if (!INTEGRATION_SET.has(integration))
throw new Error('Failed to validate integration');
const environments = req.membership.workspace?.environments || [];
if(environments.length === 0){
throw new Error("Failed to get environments")
}
const integrationAuth = await IntegrationService.handleOAuthExchange({
workspaceId,
integration,
code,
environment: environments[0].slug,
});
return res.status(200).send({
integrationAuth
});
};
/**
@@ -104,55 +86,53 @@ export const saveIntegrationAccessToken = async (
// TODO: check if access token is valid for each integration
let integrationAuth;
try {
const {
workspaceId,
accessId,
accessToken,
integration
}: {
workspaceId: string;
accessId: string | null;
accessToken: string;
integration: string;
} = req.body;
const {
workspaceId,
accessId,
accessToken,
url,
namespace,
integration
}: {
workspaceId: string;
accessId: string | null;
accessToken: string;
url: string;
namespace: string;
integration: string;
} = req.body;
const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId),
isActive: true
});
if (!bot) throw new Error('Bot must be enabled to save integration access token');
const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId),
isActive: true
});
if (!bot) throw new Error('Bot must be enabled to save integration access token');
integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: new Types.ObjectId(workspaceId),
integration
}, {
workspace: new Types.ObjectId(workspaceId),
integration,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true,
upsert: true
});
// encrypt and save integration access details
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId,
accessToken,
accessExpiresAt: undefined
});
if (!integrationAuth) throw new Error('Failed to save integration access token');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to save access token for integration'
});
}
integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: new Types.ObjectId(workspaceId),
integration
}, {
workspace: new Types.ObjectId(workspaceId),
integration,
url,
namespace,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true,
upsert: true
});
// encrypt and save integration access details
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId,
accessToken,
accessExpiresAt: undefined
});
if (!integrationAuth) throw new Error('Failed to save integration access token');
return res.status(200).send({
integrationAuth
@@ -166,22 +146,13 @@ export const saveIntegrationAccessToken = async (
* @returns
*/
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
let apps;
try {
const teamId = req.query.teamId as string;
apps = await getApps({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
...teamId && { teamId }
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get integration authorization applications",
});
}
const teamId = req.query.teamId as string;
const apps = await getApps({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
...teamId && { teamId }
});
return res.status(200).send({
apps
@@ -402,19 +373,10 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo
* @returns
*/
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth;
try {
integrationAuth = await revokeAccess({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete integration authorization",
});
}
const integrationAuth = await revokeAccess({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
});
return res.status(200).send({
integrationAuth,

View File

@@ -1,6 +1,5 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
Integration
} from '../../models';
@@ -14,61 +13,50 @@ import { eventPushSecrets } from '../../events';
* @returns
*/
export const createIntegration = async (req: Request, res: Response) => {
let integration;
const {
integrationAuthId,
app,
appId,
isActive,
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region
} = req.body;
// TODO: validate [sourceEnvironment] and [targetEnvironment]
try {
const {
integrationAuthId,
app,
appId,
isActive,
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region
} = req.body;
// TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token
integration = await new Integration({
workspace: req.integrationAuth.workspace._id,
environment: sourceEnvironment,
isActive,
app,
appId,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region,
integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId)
}).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment: sourceEnvironment
})
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create integration'
});
}
// initialize new integration after saving integration access token
const integration = await new Integration({
workspace: req.integrationAuth.workspace._id,
environment: sourceEnvironment,
isActive,
app,
appId,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region,
integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId)
}).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment: sourceEnvironment
})
});
}
return res.status(200).send({
integration,
@@ -82,52 +70,43 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns
*/
export const updateIntegration = async (req: Request, res: Response) => {
let integration;
// TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration]
try {
const {
const {
environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
} = req.body;
const integration = await Integration.findOneAndUpdate(
{
_id: req.integration._id,
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
} = req.body;
integration = await Integration.findOneAndUpdate(
{
_id: req.integration._id,
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner,
},
{
new: true,
}
);
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment
}),
});
owner,
},
{
new: true,
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to update integration",
);
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment
}),
});
}
@@ -144,22 +123,13 @@ export const updateIntegration = async (req: Request, res: Response) => {
* @returns
*/
export const deleteIntegration = async (req: Request, res: Response) => {
let integration;
try {
const { integrationId } = req.params;
const { integrationId } = req.params;
integration = await Integration.findOneAndDelete({
_id: integrationId,
});
const integration = await Integration.findOneAndDelete({
_id: integrationId,
});
if (!integration) throw new Error("Failed to find integration");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete integration",
});
}
if (!integration) throw new Error("Failed to find integration");
return res.status(200).send({
integration,

View File

@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Key } from '../../models';
import { findMembership } from '../../helpers/membership';
@@ -11,34 +10,26 @@ import { findMembership } from '../../helpers/membership';
* @returns
*/
export const uploadKey = async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
const { key } = req.body;
const { workspaceId } = req.params;
const { key } = req.body;
// validate membership of receiver
const receiverMembership = await findMembership({
user: key.userId,
workspace: workspaceId
});
// validate membership of receiver
const receiverMembership = await findMembership({
user: key.userId,
workspace: workspaceId
});
if (!receiverMembership) {
throw new Error('Failed receiver membership validation for workspace');
}
if (!receiverMembership) {
throw new Error('Failed receiver membership validation for workspace');
}
await new Key({
encryptedKey: key.encryptedKey,
nonce: key.nonce,
sender: req.user._id,
receiver: key.userId,
workspace: workspaceId
}).save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload key to workspace'
});
}
await new Key({
encryptedKey: key.encryptedKey,
nonce: key.nonce,
sender: req.user._id,
receiver: key.userId,
workspace: workspaceId
}).save();
return res.status(200).send({
message: 'Successfully uploaded key to workspace'
@@ -52,25 +43,16 @@ export const uploadKey = async (req: Request, res: Response) => {
* @returns
*/
export const getLatestKey = async (req: Request, res: Response) => {
let latestKey;
try {
const { workspaceId } = req.params;
const { workspaceId } = req.params;
// get latest key
latestKey = await Key.find({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.limit(1)
.populate('sender', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get latest key'
});
}
// get latest key
const latestKey = await Key.find({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.limit(1)
.populate('sender', '+publicKey');
const resObj: any = {};
@@ -79,4 +61,4 @@ export const getLatestKey = async (req: Request, res: Response) => {
}
return res.status(200).send(resObj);
};
};

View File

@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import { Membership, MembershipOrg, User, Key } from '../../models';
import {
@@ -16,25 +15,16 @@ import { getSiteURL } from '../../config';
* @returns
*/
export const validateMembership = async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
const { workspaceId } = req.params;
// validate membership
const membership = await findMembership({
user: req.user._id,
workspace: workspaceId
});
// validate membership
const membership = await findMembership({
user: req.user._id,
workspace: workspaceId
});
if (!membership) {
throw new Error('Failed to validate membership');
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed workspace connection check'
});
}
if (!membership) {
throw new Error('Failed to validate membership');
}
return res.status(200).send({
message: 'Workspace membership confirmed'
@@ -48,48 +38,39 @@ export const validateMembership = async (req: Request, res: Response) => {
* @returns
*/
export const deleteMembership = async (req: Request, res: Response) => {
let deletedMembership;
try {
const { membershipId } = req.params;
const { membershipId } = req.params;
// check if membership to delete exists
const membershipToDelete = await Membership.findOne({
_id: membershipId
}).populate('user');
// check if membership to delete exists
const membershipToDelete = await Membership.findOne({
_id: membershipId
}).populate('user');
if (!membershipToDelete) {
throw new Error(
"Failed to delete workspace membership that doesn't exist"
);
}
if (!membershipToDelete) {
throw new Error(
"Failed to delete workspace membership that doesn't exist"
);
}
// check if user is a member and admin of the workspace
// whose membership we wish to delete
const membership = await Membership.findOne({
user: req.user._id,
workspace: membershipToDelete.workspace
});
// check if user is a member and admin of the workspace
// whose membership we wish to delete
const membership = await Membership.findOne({
user: req.user._id,
workspace: membershipToDelete.workspace
});
if (!membership) {
throw new Error('Failed to validate workspace membership');
}
if (!membership) {
throw new Error('Failed to validate workspace membership');
}
if (membership.role !== ADMIN) {
// user is not an admin member of the workspace
throw new Error('Insufficient role for deleting workspace membership');
}
if (membership.role !== ADMIN) {
// user is not an admin member of the workspace
throw new Error('Insufficient role for deleting workspace membership');
}
// delete workspace membership
deletedMembership = await deleteMember({
membershipId: membershipToDelete._id.toString()
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete membership'
});
}
// delete workspace membership
const deletedMembership = await deleteMember({
membershipId: membershipToDelete._id.toString()
});
return res.status(200).send({
deletedMembership
@@ -103,49 +84,40 @@ export const deleteMembership = async (req: Request, res: Response) => {
* @returns
*/
export const changeMembershipRole = async (req: Request, res: Response) => {
let membershipToChangeRole;
try {
const { membershipId } = req.params;
const { role } = req.body;
const { membershipId } = req.params;
const { role } = req.body;
if (![ADMIN, MEMBER].includes(role)) {
throw new Error('Failed to validate role');
}
if (![ADMIN, MEMBER].includes(role)) {
throw new Error('Failed to validate role');
}
// validate target membership
membershipToChangeRole = await findMembership({
_id: membershipId
});
// validate target membership
const membershipToChangeRole = await findMembership({
_id: membershipId
});
if (!membershipToChangeRole) {
throw new Error('Failed to find membership to change role');
}
if (!membershipToChangeRole) {
throw new Error('Failed to find membership to change role');
}
// check if user is a member and admin of target membership's
// workspace
const membership = await findMembership({
user: req.user._id,
workspace: membershipToChangeRole.workspace
});
// check if user is a member and admin of target membership's
// workspace
const membership = await findMembership({
user: req.user._id,
workspace: membershipToChangeRole.workspace
});
if (!membership) {
throw new Error('Failed to validate membership');
}
if (!membership) {
throw new Error('Failed to validate membership');
}
if (membership.role !== ADMIN) {
// user is not an admin member of the workspace
throw new Error('Insufficient role for changing member roles');
}
if (membership.role !== ADMIN) {
// user is not an admin member of the workspace
throw new Error('Insufficient role for changing member roles');
}
membershipToChangeRole.role = role;
await membershipToChangeRole.save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change membership role'
});
}
membershipToChangeRole.role = role;
await membershipToChangeRole.save();
return res.status(200).send({
membership: membershipToChangeRole
@@ -159,75 +131,66 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
* @returns
*/
export const inviteUserToWorkspace = async (req: Request, res: Response) => {
let invitee, latestKey;
try {
const { workspaceId } = req.params;
const { email }: { email: string } = req.body;
const { workspaceId } = req.params;
const { email }: { email: string } = req.body;
invitee = await User.findOne({
email
}).select('+publicKey');
const invitee = await User.findOne({
email
}).select('+publicKey');
if (!invitee || !invitee?.publicKey)
throw new Error('Failed to validate invitee');
if (!invitee || !invitee?.publicKey)
throw new Error('Failed to validate invitee');
// validate invitee's workspace membership - ensure member isn't
// already a member of the workspace
const inviteeMembership = await Membership.findOne({
user: invitee._id,
workspace: workspaceId
});
// validate invitee's workspace membership - ensure member isn't
// already a member of the workspace
const inviteeMembership = await Membership.findOne({
user: invitee._id,
workspace: workspaceId
});
if (inviteeMembership)
throw new Error('Failed to add existing member of workspace');
if (inviteeMembership)
throw new Error('Failed to add existing member of workspace');
// validate invitee's organization membership - ensure that only
// (accepted) organization members can be added to the workspace
const membershipOrg = await MembershipOrg.findOne({
user: invitee._id,
organization: req.membership.workspace.organization,
status: ACCEPTED
});
// validate invitee's organization membership - ensure that only
// (accepted) organization members can be added to the workspace
const membershipOrg = await MembershipOrg.findOne({
user: invitee._id,
organization: req.membership.workspace.organization,
status: ACCEPTED
});
if (!membershipOrg)
throw new Error("Failed to validate invitee's organization membership");
if (!membershipOrg)
throw new Error("Failed to validate invitee's organization membership");
// get latest key
latestKey = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.populate('sender', '+publicKey');
// get latest key
const latestKey = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.populate('sender', '+publicKey');
// create new workspace membership
const m = await new Membership({
user: invitee._id,
workspace: workspaceId,
role: MEMBER
}).save();
// create new workspace membership
const m = await new Membership({
user: invitee._id,
workspace: workspaceId,
role: MEMBER
}).save();
await sendMail({
template: 'workspaceInvitation.handlebars',
subjectLine: 'Infisical workspace invitation',
recipients: [invitee.email],
substitutions: {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
workspaceName: req.membership.workspace.name,
callback_url: (await getSiteURL()) + '/login'
}
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to invite user to workspace'
});
}
await sendMail({
template: 'workspaceInvitation.handlebars',
subjectLine: 'Infisical workspace invitation',
recipients: [invitee.email],
substitutions: {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
workspaceName: req.membership.workspace.name,
callback_url: (await getSiteURL()) + '/login'
}
});
return res.status(200).send({
invitee,
latestKey
});
};
};

View File

@@ -1,14 +1,15 @@
import { Types } from 'mongoose';
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { MembershipOrg, Organization, User } from '../../models';
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
import { createToken } from '../../helpers/auth';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services';
import { EELicenseService } from '../../ee/services';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
import { validateUserEmail } from '../../validation';
/**
* Delete organization membership with id [membershipOrgId] from organization
@@ -17,52 +18,43 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured
* @returns
*/
export const deleteMembershipOrg = async (req: Request, res: Response) => {
let membershipOrgToDelete;
try {
const { membershipOrgId } = req.params;
const { membershipOrgId } = req.params;
// check if organization membership to delete exists
membershipOrgToDelete = await MembershipOrg.findOne({
_id: membershipOrgId
}).populate('user');
// check if organization membership to delete exists
const membershipOrgToDelete = await MembershipOrg.findOne({
_id: membershipOrgId
}).populate('user');
if (!membershipOrgToDelete) {
throw new Error(
"Failed to delete organization membership that doesn't exist"
);
}
if (!membershipOrgToDelete) {
throw new Error(
"Failed to delete organization membership that doesn't exist"
);
}
// check if user is a member and admin of the organization
// whose membership we wish to delete
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: membershipOrgToDelete.organization
});
// check if user is a member and admin of the organization
// whose membership we wish to delete
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: membershipOrgToDelete.organization
});
if (!membershipOrg) {
throw new Error('Failed to validate organization membership');
}
if (!membershipOrg) {
throw new Error('Failed to validate organization membership');
}
if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) {
// user is not an admin member of the organization
throw new Error('Insufficient role for deleting organization membership');
}
if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) {
// user is not an admin member of the organization
throw new Error('Insufficient role for deleting organization membership');
}
// delete organization membership
const deletedMembershipOrg = await deleteMemberFromOrg({
membershipOrgId: membershipOrgToDelete._id.toString()
});
// delete organization membership
const deletedMembershipOrg = await deleteMemberFromOrg({
membershipOrgId: membershipOrgToDelete._id.toString()
});
await updateSubscriptionOrgQuantity({
organizationId: membershipOrg.organization.toString()
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization membership'
});
}
await updateSubscriptionOrgQuantity({
organizationId: membershipOrg.organization.toString()
});
return membershipOrgToDelete;
};
@@ -78,14 +70,6 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// [membershipOrgId]
let membershipToChangeRole;
// try {
// } catch (err) {
// Sentry.setUser({ email: req.user.email });
// Sentry.captureException(err);
// return res.status(400).send({
// message: 'Failed to change organization membership role'
// });
// }
return res.status(200).send({
membershipOrg: membershipToChangeRole
@@ -101,106 +85,114 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
*/
export const inviteUserToOrganization = async (req: Request, res: Response) => {
let invitee, inviteeMembershipOrg, completeInviteLink;
try {
const { organizationId, inviteeEmail } = req.body;
const host = req.headers.host;
const siteUrl = `${req.protocol}://${host}`;
const { organizationId, inviteeEmail } = req.body;
const host = req.headers.host;
const siteUrl = `${req.protocol}://${host}`;
// validate membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: organizationId
});
// validate membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: organizationId
});
if (!membershipOrg) {
throw new Error('Failed to validate organization membership');
}
if (!membershipOrg) {
throw new Error('Failed to validate organization membership');
}
const plan = await EELicenseService.getPlan(organizationId);
if (plan.memberLimit !== null) {
// case: limit imposed on number of members allowed
if (plan.membersUsed >= plan.memberLimit) {
// case: number of members used exceeds the number of members allowed
return res.status(400).send({
message: 'Failed to invite member due to member limit reached. Upgrade plan to invite more members.'
});
}
}
invitee = await User.findOne({
email: inviteeEmail
}).select('+publicKey');
invitee = await User.findOne({
email: inviteeEmail
}).select('+publicKey');
if (invitee) {
// case: invitee is an existing user
if (invitee) {
// case: invitee is an existing user
inviteeMembershipOrg = await MembershipOrg.findOne({
user: invitee._id,
organization: organizationId
});
inviteeMembershipOrg = await MembershipOrg.findOne({
user: invitee._id,
organization: organizationId
});
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
throw new Error(
'Failed to invite an existing member of the organization'
);
}
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
throw new Error(
'Failed to invite an existing member of the organization'
);
}
if (!inviteeMembershipOrg) {
await new MembershipOrg({
user: invitee,
inviteEmail: inviteeEmail,
organization: organizationId,
role: MEMBER,
status: INVITED
}).save();
}
} else {
// check if invitee has been invited before
inviteeMembershipOrg = await MembershipOrg.findOne({
inviteEmail: inviteeEmail,
organization: organizationId
});
if (!inviteeMembershipOrg) {
await new MembershipOrg({
user: invitee,
inviteEmail: inviteeEmail,
organization: organizationId,
role: MEMBER,
status: INVITED
}).save();
}
} else {
// check if invitee has been invited before
inviteeMembershipOrg = await MembershipOrg.findOne({
inviteEmail: inviteeEmail,
organization: organizationId
});
if (!inviteeMembershipOrg) {
// case: invitee has never been invited before
if (!inviteeMembershipOrg) {
// case: invitee has never been invited before
// validate that email is not disposable
validateUserEmail(inviteeEmail);
await new MembershipOrg({
inviteEmail: inviteeEmail,
organization: organizationId,
role: MEMBER,
status: INVITED
}).save();
}
}
await new MembershipOrg({
inviteEmail: inviteeEmail,
organization: organizationId,
role: MEMBER,
status: INVITED
}).save();
}
}
const organization = await Organization.findOne({ _id: organizationId });
const organization = await Organization.findOne({ _id: organizationId });
if (organization) {
if (organization) {
const token = await TokenService.createToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email: inviteeEmail,
organizationId: organization._id
});
const token = await TokenService.createToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email: inviteeEmail,
organizationId: organization._id
});
await sendMail({
template: 'organizationInvitation.handlebars',
subjectLine: 'Infisical organization invitation',
recipients: [inviteeEmail],
substitutions: {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
organizationName: organization.name,
email: inviteeEmail,
organizationId: organization._id.toString(),
token,
callback_url: (await getSiteURL()) + '/signupinvite'
}
});
await sendMail({
template: 'organizationInvitation.handlebars',
subjectLine: 'Infisical organization invitation',
recipients: [inviteeEmail],
substitutions: {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
organizationName: organization.name,
email: inviteeEmail,
organizationId: organization._id.toString(),
token,
callback_url: (await getSiteURL()) + '/signupinvite'
}
});
if (!(await getSmtpConfigured())) {
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`
}
}
if (!(await getSmtpConfigured())) {
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`
}
}
await updateSubscriptionOrgQuantity({ organizationId });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send organization invite'
});
}
await updateSubscriptionOrgQuantity({ organizationId });
return res.status(200).send({
message: `Sent an invite link to ${req.body.inviteeEmail}`,
@@ -216,70 +208,62 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
* @returns
*/
export const verifyUserToOrganization = async (req: Request, res: Response) => {
let user, token;
try {
const {
email,
organizationId,
code
} = req.body;
let user;
const {
email,
organizationId,
code
} = req.body;
user = await User.findOne({ email }).select('+publicKey');
user = await User.findOne({ email }).select('+publicKey');
const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email,
status: INVITED,
organization: new Types.ObjectId(organizationId)
});
const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email,
status: INVITED,
organization: new Types.ObjectId(organizationId)
});
if (!membershipOrg)
throw new Error('Failed to find any invitations for email');
if (!membershipOrg)
throw new Error('Failed to find any invitations for email');
await TokenService.validateToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email,
organizationId: membershipOrg.organization,
token: code
});
await TokenService.validateToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email,
organizationId: membershipOrg.organization,
token: code
});
if (user && user?.publicKey) {
// case: user has already completed account
// membership can be approved and redirected to login/dashboard
membershipOrg.status = ACCEPTED;
await membershipOrg.save();
await updateSubscriptionOrgQuantity({
organizationId
});
if (user && user?.publicKey) {
// case: user has already completed account
// membership can be approved and redirected to login/dashboard
membershipOrg.status = ACCEPTED;
await membershipOrg.save();
await updateSubscriptionOrgQuantity({
organizationId
});
return res.status(200).send({
message: 'Successfully verified email',
user,
});
}
return res.status(200).send({
message: 'Successfully verified email',
user,
});
}
if (!user) {
// initialize user account
user = await new User({
email
}).save();
}
if (!user) {
// initialize user account
user = await new User({
email
}).save();
}
// generate temporary signup token
token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed email magic link verification for organization invitation'
});
}
// generate temporary signup token
const token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret()
});
return res.status(200).send({
message: 'Successfully verified email',

View File

@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import Stripe from 'stripe';
import {
@@ -15,21 +14,12 @@ import _ from 'lodash';
import { getStripeSecretKey, getSiteURL } from '../../config';
export const getOrganizations = async (req: Request, res: Response) => {
let organizations;
try {
organizations = (
await MembershipOrg.find({
user: req.user._id,
status: ACCEPTED
}).populate('organization')
).map((m) => m.organization);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organizations'
});
}
const organizations = (
await MembershipOrg.find({
user: req.user._id,
status: ACCEPTED
}).populate('organization')
).map((m) => m.organization);
return res.status(200).send({
organizations
@@ -44,33 +34,24 @@ export const getOrganizations = async (req: Request, res: Response) => {
* @returns
*/
export const createOrganization = async (req: Request, res: Response) => {
let organization;
try {
const { organizationName } = req.body;
const { organizationName } = req.body;
if (organizationName.length < 1) {
throw new Error('Organization names must be at least 1-character long');
}
if (organizationName.length < 1) {
throw new Error('Organization names must be at least 1-character long');
}
// create organization and add user as member
organization = await create({
email: req.user.email,
name: organizationName
});
// create organization and add user as member
const organization = await create({
email: req.user.email,
name: organizationName
});
await addMembershipsOrg({
userIds: [req.user._id.toString()],
organizationId: organization._id.toString(),
roles: [OWNER],
statuses: [ACCEPTED]
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create organization'
});
}
await addMembershipsOrg({
userIds: [req.user._id.toString()],
organizationId: organization._id.toString(),
roles: [OWNER],
statuses: [ACCEPTED]
});
return res.status(200).send({
organization
@@ -84,17 +65,7 @@ export const createOrganization = async (req: Request, res: Response) => {
* @returns
*/
export const getOrganization = async (req: Request, res: Response) => {
let organization;
try {
organization = req.organization
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to find organization'
});
}
const organization = req.organization
return res.status(200).send({
organization
});
@@ -107,20 +78,11 @@ export const getOrganization = async (req: Request, res: Response) => {
* @returns
*/
export const getOrganizationMembers = async (req: Request, res: Response) => {
let users;
try {
const { organizationId } = req.params;
const { organizationId } = req.params;
users = await MembershipOrg.find({
organization: organizationId
}).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization members'
});
}
const users = await MembershipOrg.find({
organization: organizationId
}).populate('user', '+publicKey');
return res.status(200).send({
users
@@ -137,35 +99,26 @@ export const getOrganizationWorkspaces = async (
req: Request,
res: Response
) => {
let workspaces;
try {
const { organizationId } = req.params;
const { organizationId } = req.params;
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
'_id'
)
).map((w) => w._id.toString())
);
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
'_id'
)
).map((w) => w._id.toString())
);
workspaces = (
await Membership.find({
user: req.user._id
}).populate('workspace')
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get my workspaces'
});
}
const workspaces = (
await Membership.find({
user: req.user._id
}).populate('workspace')
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
return res.status(200).send({
workspaces
@@ -179,29 +132,20 @@ export const getOrganizationWorkspaces = async (
* @returns
*/
export const changeOrganizationName = async (req: Request, res: Response) => {
let organization;
try {
const { organizationId } = req.params;
const { name } = req.body;
const { organizationId } = req.params;
const { name } = req.body;
organization = await Organization.findOneAndUpdate(
{
_id: organizationId
},
{
name
},
{
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change organization name'
});
}
const organization = await Organization.findOneAndUpdate(
{
_id: organizationId
},
{
name
},
{
new: true
}
);
return res.status(200).send({
message: 'Successfully changed organization name',
@@ -219,20 +163,11 @@ export const getOrganizationIncidentContacts = async (
req: Request,
res: Response
) => {
let incidentContactsOrg;
try {
const { organizationId } = req.params;
const { organizationId } = req.params;
incidentContactsOrg = await IncidentContactOrg.find({
organization: organizationId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization incident contacts'
});
}
const incidentContactsOrg = await IncidentContactOrg.find({
organization: organizationId
});
return res.status(200).send({
incidentContactsOrg
@@ -249,23 +184,14 @@ export const addOrganizationIncidentContact = async (
req: Request,
res: Response
) => {
let incidentContactOrg;
try {
const { organizationId } = req.params;
const { email } = req.body;
const { organizationId } = req.params;
const { email } = req.body;
incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
{ email, organization: organizationId },
{ email, organization: organizationId },
{ upsert: true, new: true }
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to add incident contact for organization'
});
}
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
{ email, organization: organizationId },
{ email, organization: organizationId },
{ upsert: true, new: true }
);
return res.status(200).send({
incidentContactOrg
@@ -282,22 +208,13 @@ export const deleteOrganizationIncidentContact = async (
req: Request,
res: Response
) => {
let incidentContactOrg;
try {
const { organizationId } = req.params;
const { email } = req.body;
const { organizationId } = req.params;
const { email } = req.body;
incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
email,
organization: organizationId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization incident contact'
});
}
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
email,
organization: organizationId
});
return res.status(200).send({
message: 'Successfully deleted organization incident contact',
@@ -317,41 +234,33 @@ export const createOrganizationPortalSession = async (
res: Response
) => {
let session;
try {
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
// check if there is a payment method on file
const paymentMethods = await stripe.paymentMethods.list({
customer: req.organization.customerId,
type: 'card'
});
if (paymentMethods.data.length < 1) {
// case: no payment method on file
session = await stripe.checkout.sessions.create({
customer: req.organization.customerId,
mode: 'setup',
payment_method_types: ['card'],
success_url: (await getSiteURL()) + '/dashboard',
cancel_url: (await getSiteURL()) + '/dashboard'
});
} else {
session = await stripe.billingPortal.sessions.create({
customer: req.organization.customerId,
return_url: (await getSiteURL()) + '/dashboard'
});
}
// check if there is a payment method on file
const paymentMethods = await stripe.paymentMethods.list({
customer: req.organization.customerId,
type: 'card'
});
if (paymentMethods.data.length < 1) {
// case: no payment method on file
session = await stripe.checkout.sessions.create({
customer: req.organization.customerId,
mode: 'setup',
payment_method_types: ['card'],
success_url: (await getSiteURL()) + '/dashboard',
cancel_url: (await getSiteURL()) + '/dashboard'
});
} else {
session = await stripe.billingPortal.sessions.create({
customer: req.organization.customerId,
return_url: (await getSiteURL()) + '/dashboard'
});
}
return res.status(200).send({ url: session.url });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to redirect to organization billing portal'
});
}
return res.status(200).send({ url: session.url });
};
/**
@@ -364,22 +273,13 @@ export const getOrganizationSubscriptions = async (
req: Request,
res: Response
) => {
let subscriptions;
try {
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
subscriptions = await stripe.subscriptions.list({
customer: req.organization.customerId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization subscriptions'
});
}
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
const subscriptions = await stripe.subscriptions.list({
customer: req.organization.customerId
});
return res.status(200).send({
subscriptions
@@ -425,4 +325,4 @@ export const getOrganizationMembersAndTheirWorkspaces = async (
});
return res.json(userToWorkspaceIds);
};
};

View File

@@ -1,15 +1,25 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion';
import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
import { createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer';
import {
createToken,
sendMail,
clearTokens
} from '../../helpers';
import { TokenService } from '../../services';
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables';
import {
TOKEN_EMAIL_PASSWORD_RESET,
AUTH_MODE_JWT
} from '../../variables';
import { BadRequestError } from '../../utils/errors';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config';
import {
getSiteURL,
getJwtSignupLifetime,
getJwtSignupSecret,
getHttpsEnabled
} from '../../config';
/**
* Password reset step 1: Send email verification link to email [email]
@@ -20,43 +30,35 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../conf
*/
export const emailPasswordReset = async (req: Request, res: Response) => {
let email: string;
try {
email = req.body.email;
email = req.body.email;
const user = await User.findOne({ email }).select('+publicKey');
if (!user || !user?.publicKey) {
// case: user has already completed account
const user = await User.findOne({ email }).select('+publicKey');
if (!user || !user?.publicKey) {
// case: user has already completed account
return res.status(403).send({
error: 'Failed to send email verification for password reset'
});
}
const token = await TokenService.createToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email
});
await sendMail({
template: 'passwordReset.handlebars',
subjectLine: 'Infisical password reset',
recipients: [email],
substitutions: {
email,
token,
callback_url: (await getSiteURL()) + '/password-reset'
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send email for account recovery'
});
}
return res.status(200).send({
message:"If an account exists with this email, a password reset link has been sent"
});
}
const token = await TokenService.createToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email
});
await sendMail({
template: 'passwordReset.handlebars',
subjectLine: 'Infisical password reset',
recipients: [email],
substitutions: {
email,
token,
callback_url: (await getSiteURL()) + '/password-reset'
}
});
return res.status(200).send({
message: `Sent an email for account recovery to ${email}`
message:"If an account exists with this email, a password reset link has been sent"
});
}
@@ -67,40 +69,31 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
* @returns
*/
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
let user, token;
try {
const { email, code } = req.body;
const { email, code } = req.body;
user = await User.findOne({ email }).select('+publicKey');
if (!user || !user?.publicKey) {
// case: user doesn't exist with email [email] or
// hasn't even completed their account
return res.status(403).send({
error: 'Failed email verification for password reset'
});
}
await TokenService.validateToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email,
token: code
});
const user = await User.findOne({ email }).select('+publicKey');
if (!user || !user?.publicKey) {
// case: user doesn't exist with email [email] or
// hasn't even completed their account
return res.status(403).send({
error: 'Failed email verification for password reset'
});
}
await TokenService.validateToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email,
token: code
});
// generate temporary password-reset token
token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed email verification for password reset'
});
}
// generate temporary password-reset token
const token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret()
});
return res.status(200).send({
message: 'Successfully verified email',
@@ -117,44 +110,38 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
*/
export const srp1 = async (req: Request, res: Response) => {
// return salt, serverPublicKey as part of first step of SRP protocol
try {
const { clientPublicKey } = req.body;
const user = await User.findOne({
email: req.user.email
}).select('+salt +verifier');
const { clientPublicKey } = req.body;
const user = await User.findOne({
email: req.user.email
}).select('+salt +verifier');
if (!user) throw new Error('Failed to find user');
if (!user) throw new Error('Failed to find user');
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, {
email: req.user.email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false })
await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, {
email: req.user.email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false })
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
}
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to start change password process'
});
}
};
/**
* Change account SRP authentication information for user
@@ -165,80 +152,85 @@ export const srp1 = async (req: Request, res: Response) => {
* @returns
*/
export const changePassword = async (req: Request, res: Response) => {
try {
const {
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
} = req.body;
const {
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
} = req.body;
const user = await User.findOne({
email: req.user.email
}).select('+salt +verifier');
const user = await User.findOne({
email: req.user.email
}).select('+salt +verifier');
if (!user) throw new Error('Failed to find user');
if (!user) throw new Error('Failed to find user');
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
}
if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// change password
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// change password
await User.findByIdAndUpdate(
req.user._id.toString(),
{
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
},
{
new: true
}
);
await User.findByIdAndUpdate(
req.user._id.toString(),
{
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
},
{
new: true
}
);
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId)
}
return res.status(200).send({
message: 'Successfully changed password'
});
}
// clear httpOnly cookie
res.cookie('jid', '', {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: (await getHttpsEnabled()) as boolean
});
return res.status(400).send({
error: 'Failed to change password. Try again?'
});
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to change password. Try again?'
});
}
return res.status(200).send({
message: 'Successfully changed password'
});
}
return res.status(400).send({
error: 'Failed to change password. Try again?'
});
}
);
};
/**
@@ -252,69 +244,61 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
// requires verifying [clientProof] as part of second step of SRP protocol
// as initiated in /srp1
try {
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
req.body;
const user = await User.findOne({
email: req.user.email
}).select('+salt +verifier');
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
req.body;
const user = await User.findOne({
email: req.user.email
}).select('+salt +verifier');
if (!user) throw new Error('Failed to find user');
if (!user) throw new Error('Failed to find user');
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
}
if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(
loginSRPDetailFromDB.clientPublicKey
);
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(
loginSRPDetailFromDB.clientPublicKey
);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// create new or replace backup private key
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// create new or replace backup private key
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
{ user: req.user._id },
{
user: req.user._id,
encryptedPrivateKey,
iv,
tag,
salt,
verifier
},
{ upsert: true, new: true }
).select('+user, encryptedPrivateKey');
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
{ user: req.user._id },
{
user: req.user._id,
encryptedPrivateKey,
iv,
tag,
salt,
verifier
},
{ upsert: true, new: true }
).select('+user, encryptedPrivateKey');
// issue tokens
return res.status(200).send({
message: 'Successfully updated backup private key',
backupPrivateKey
});
}
// issue tokens
return res.status(200).send({
message: 'Successfully updated backup private key',
backupPrivateKey
});
}
return res.status(400).send({
message: 'Failed to update backup private key'
});
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update backup private key'
});
}
return res.status(400).send({
message: 'Failed to update backup private key'
});
}
);
};
/**
@@ -324,20 +308,11 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
* @returns
*/
export const getBackupPrivateKey = async (req: Request, res: Response) => {
let backupPrivateKey;
try {
backupPrivateKey = await BackupPrivateKey.findOne({
user: req.user._id
}).select('+encryptedPrivateKey +iv +tag');
const backupPrivateKey = await BackupPrivateKey.findOne({
user: req.user._id
}).select('+encryptedPrivateKey +iv +tag');
if (!backupPrivateKey) throw new Error('Failed to find backup private key');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get backup private key'
});
}
if (!backupPrivateKey) throw new Error('Failed to find backup private key');
return res.status(200).send({
backupPrivateKey
@@ -345,44 +320,36 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => {
}
export const resetPassword = async (req: Request, res: Response) => {
try {
const {
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
} = req.body;
const {
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
} = req.body;
await User.findByIdAndUpdate(
req.user._id.toString(),
{
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
},
{
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get backup private key'
});
}
await User.findByIdAndUpdate(
req.user._id.toString(),
{
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
},
{
new: true
}
);
return res.status(200).send({
message: 'Successfully reset password'
});
}
}

View File

@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Key, Secret } from '../../models';
import {
@@ -37,66 +36,56 @@ interface PushSecret {
*/
export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
const postHogClient = await TelemetryService.getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
try {
const postHogClient = await TelemetryService.getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
// sanitize secrets
secrets = secrets.filter(
(s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== ''
);
// sanitize secrets
secrets = secrets.filter(
(s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== ''
);
await push({
userId: req.user._id,
workspaceId,
environment,
secrets
});
await push({
userId: req.user._id,
workspaceId,
environment,
secrets
});
await pushKeys({
userId: req.user._id,
workspaceId,
keys
});
if (postHogClient) {
postHogClient.capture({
event: 'secrets pushed',
distinctId: req.user.email,
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
await pushKeys({
userId: req.user._id,
workspaceId,
keys
});
if (postHogClient) {
postHogClient.capture({
event: 'secrets pushed',
distinctId: req.user.email,
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload workspace secrets'
});
}
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
return res.status(200).send({
message: 'Successfully uploaded workspace secrets'
@@ -113,55 +102,48 @@ export const pushSecrets = async (req: Request, res: Response) => {
export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
let key;
try {
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
secrets = await pull({
userId: req.user._id.toString(),
workspaceId,
environment,
channel: channel ? channel : 'cli',
ipAddress: req.ip
});
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.populate('sender', '+publicKey');
if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets });
}
secrets = await pull({
userId: req.user._id.toString(),
workspaceId,
environment,
channel: channel ? channel : 'cli',
ipAddress: req.realIP
});
if (postHogClient) {
// capture secrets pushed event in production
postHogClient.capture({
distinctId: req.user.email,
event: 'secrets pulled',
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.populate('sender', '+publicKey');
if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets });
}
if (postHogClient) {
// capture secrets pushed event in production
postHogClient.capture({
distinctId: req.user.email,
event: 'secrets pulled',
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
@@ -182,54 +164,47 @@ export const pullSecrets = async (req: Request, res: Response) => {
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
let secrets;
let key;
try {
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
secrets = await pull({
userId: req.serviceToken.user._id.toString(),
workspaceId,
environment,
channel: 'cli',
ipAddress: req.ip
});
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
key = {
encryptedKey: req.serviceToken.encryptedKey,
nonce: req.serviceToken.nonce,
sender: {
publicKey: req.serviceToken.publicKey
},
receiver: req.serviceToken.user,
workspace: req.serviceToken.workspace
};
secrets = await pull({
userId: req.serviceToken.user._id.toString(),
workspaceId,
environment,
channel: 'cli',
ipAddress: req.realIP
});
if (postHogClient) {
// capture secrets pulled event in production
postHogClient.capture({
distinctId: req.serviceToken.user.email,
event: 'secrets pulled',
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
} catch (err) {
Sentry.setUser({ email: req.serviceToken.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
key = {
encryptedKey: req.serviceToken.encryptedKey,
nonce: req.serviceToken.nonce,
sender: {
publicKey: req.serviceToken.publicKey
},
receiver: req.serviceToken.user,
workspace: req.serviceToken.workspace
};
if (postHogClient) {
// capture secrets pulled event in production
postHogClient.capture({
distinctId: req.serviceToken.user.email,
event: 'secrets pulled',
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
@@ -237,4 +212,4 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
secrets: reformatPullSecrets({ secrets }),
key
});
};
};

View File

@@ -1,107 +1,218 @@
import { Request, Response } from 'express';
import { Secret } from '../../models';
import Folder from '../../models/folder';
import { BadRequestError } from '../../utils/errors';
import { ROOT_FOLDER_PATH, getFolderPath, getParentPath, normalizePath, validateFolderName } from '../../utils/folder';
import { ADMIN, MEMBER } from '../../variables';
import { validateMembership } from '../../helpers/membership';
import { Request, Response } from "express";
import { Secret } from "../../models";
import Folder from "../../models/folder";
import { BadRequestError } from "../../utils/errors";
import {
appendFolder,
deleteFolderById,
getAllFolderIds,
searchByFolderIdWithDir,
searchByFolderId,
validateFolderName,
generateFolderId,
getParentFromFolderId,
} from "../../services/FolderService";
import { ADMIN, MEMBER } from "../../variables";
import { validateMembership } from "../../helpers/membership";
import { FolderVersion } from "../../ee/models";
import { EESecretService } from "../../ee/services";
// TODO
// verify workspace id/environment
export const createFolder = async (req: Request, res: Response) => {
const { workspaceId, environment, folderName, parentFolderId } = req.body
const { workspaceId, environment, folderName, parentFolderId } = req.body;
if (!validateFolderName(folderName)) {
throw BadRequestError({ message: "Folder name cannot contain spaces. Only underscore and dashes" })
throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes",
});
}
if (parentFolderId) {
const parentFolder = await Folder.find({ environment: environment, workspace: workspaceId, id: parentFolderId });
if (!parentFolder) {
throw BadRequestError({ message: "The parent folder doesn't exist" })
}
}
let completePath = await getFolderPath(parentFolderId)
if (completePath == ROOT_FOLDER_PATH) {
completePath = ""
}
const currentFolderPath = completePath + "/" + folderName // construct new path with current folder to be created
const normalizedCurrentPath = normalizePath(currentFolderPath)
const normalizedParentPath = getParentPath(normalizedCurrentPath)
const existingFolder = await Folder.findOne({
name: folderName,
const folders = await Folder.findOne({
workspace: workspaceId,
environment: environment,
parent: parentFolderId,
path: normalizedCurrentPath
environment,
}).lean();
// space has no folders initialized
if (!folders) {
const id = generateFolderId();
const folder = new Folder({
workspace: workspaceId,
environment,
nodes: {
id: "root",
name: "root",
version: 1,
children: [{ id, name: folderName, children: [], version: 1 }],
},
});
await folder.save();
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: folder.nodes,
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
});
return res.json({ folder: { id, name: folderName } });
}
const folder = appendFolder(folders.nodes, { folderName, parentFolderId });
await Folder.findByIdAndUpdate(folders._id, folders);
const parentFolder = searchByFolderId(folders.nodes, parentFolderId);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder,
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId: parentFolderId,
});
if (existingFolder) {
return res.json(existingFolder)
return res.json({ folder });
};
export const updateFolderById = async (req: Request, res: Response) => {
const { folderId } = req.params;
const { name, workspaceId, environment } = req.body;
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const newFolder = new Folder({
name: folderName,
workspace: workspaceId,
environment: environment,
parent: parentFolderId,
path: normalizedCurrentPath,
parentPath: normalizedParentPath
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles: [ADMIN, MEMBER],
});
await newFolder.save();
const parentFolder = getParentFromFolderId(folders.nodes, folderId);
if (!parentFolder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const folder = parentFolder.children.find(({ id }) => id === folderId);
if (!folder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
return res.json(newFolder)
}
parentFolder.version += 1;
folder.name = name;
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder,
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId: parentFolder.id,
});
return res.json({
message: "Successfully updated folder",
folder: { name: folder.name, id: folder.id },
});
};
export const deleteFolder = async (req: Request, res: Response) => {
const { folderId } = req.params
const queue: any[] = [folderId];
const { folderId } = req.params;
const { workspaceId, environment } = req.body;
const folder = await Folder.findById(folderId);
if (!folder) {
throw BadRequestError({ message: "The folder doesn't exist" })
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId: folder.workspace as any,
acceptedRoles: [ADMIN, MEMBER]
workspaceId,
acceptedRoles: [ADMIN, MEMBER],
});
while (queue.length > 0) {
const currentFolderId = queue.shift();
const delOp = deleteFolderById(folders.nodes, folderId);
if (!delOp) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const { deletedNode: delFolder, parent: parentFolder } = delOp;
const childFolders = await Folder.find({ parent: currentFolderId });
for (const childFolder of childFolders) {
queue.push(childFolder._id);
}
parentFolder.version += 1;
const delFolderIds = getAllFolderIds(delFolder);
await Secret.deleteMany({ folder: currentFolderId });
await Folder.deleteOne({ _id: currentFolderId });
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder,
});
await folderVersion.save();
if (delFolderIds.length) {
await Secret.deleteMany({
folder: { $in: delFolderIds.map(({ id }) => id) },
workspace: workspaceId,
environment,
});
}
res.send()
}
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId: parentFolder.id,
});
res.send({ message: "successfully deleted folders", folders: delFolderIds });
};
// TODO: validate workspace
export const getFolderById = async (req: Request, res: Response) => {
const { folderId } = req.params
export const getFolders = async (req: Request, res: Response) => {
const { workspaceId, environment, parentFolderId } = req.query as {
workspaceId: string;
environment: string;
parentFolderId?: string;
};
const folder = await Folder.findById(folderId);
if (!folder) {
throw BadRequestError({ message: "The folder doesn't exist" })
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
res.send({ folders: [], dir: [] });
return;
}
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId: folder.workspace as any,
acceptedRoles: [ADMIN, MEMBER]
workspaceId,
acceptedRoles: [ADMIN, MEMBER],
});
res.send({ folder })
}
if (!parentFolderId) {
const rootFolders = folders.nodes.children.map(({ id, name }) => ({
id,
name,
}));
res.send({ folders: rootFolders });
return;
}
const folderBySearch = searchByFolderIdWithDir(folders.nodes, parentFolderId);
if (!folderBySearch) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const { folder, dir } = folderBySearch;
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir,
});
};

View File

@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { User } from '../../models';
import {
sendEmailVerification,
@@ -8,6 +7,7 @@ import {
import { createToken } from '../../helpers/auth';
import { BadRequestError } from '../../utils/errors';
import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
import { validateUserEmail } from '../../validation';
/**
* Signup step 1: Initialize account for user under email [email] and send a verification code
@@ -18,28 +18,23 @@ import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpC
*/
export const beginEmailSignup = async (req: Request, res: Response) => {
let email: string;
try {
email = req.body.email;
email = req.body.email;
// validate that email is not disposable
validateUserEmail(email);
const user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) {
// case: user has already completed account
const user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) {
// case: user has already completed account
return res.status(403).send({
error: 'Failed to send email verification code for complete account'
});
}
return res.status(403).send({
error: 'Failed to send email verification code for complete account'
});
}
// send send verification email
await sendEmailVerification({ email });
// send send verification email
await sendEmailVerification({ email });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to send email verification code'
});
}
return res.status(200).send({
message: `Sent an email verification code to ${email}`
});
@@ -54,55 +49,47 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
*/
export const verifyEmailSignup = async (req: Request, res: Response) => {
let user, token;
try {
const { email, code } = req.body;
const { email, code } = req.body;
// initialize user account
user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) {
// case: user has already completed account
return res.status(403).send({
error: 'Failed email verification for complete user'
});
}
// initialize user account
user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) {
// case: user has already completed account
return res.status(403).send({
error: 'Failed email verification for complete user'
});
}
if (await getInviteOnlySignup()) {
// Only one user can create an account without being invited. The rest need to be invited in order to make an account
const userCount = await User.countDocuments({})
if (userCount != 0) {
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
}
}
if (await getInviteOnlySignup()) {
// Only one user can create an account without being invited. The rest need to be invited in order to make an account
const userCount = await User.countDocuments({})
if (userCount != 0) {
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
}
}
// verify email
if (await getSmtpConfigured()) {
await checkEmailVerification({
email,
code
});
}
// verify email
if (await getSmtpConfigured()) {
await checkEmailVerification({
email,
code
});
}
if (!user) {
user = await new User({
email
}).save();
}
if (!user) {
user = await new User({
email
}).save();
}
// generate temporary signup token
token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed email verification'
});
}
// generate temporary signup token
token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret()
});
return res.status(200).send({
message: 'Successfuly verified email',

View File

@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import Stripe from 'stripe';
import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
@@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
* @returns
*/
export const handleWebhook = async (req: Request, res: Response) => {
let event;
try {
// check request for valid stripe signature
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
// check request for valid stripe signature
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
const sig = req.headers['stripe-signature'] as string;
event = stripe.webhooks.constructEvent(
req.body,
sig,
await getStripeWebhookSecret()
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to process webhook'
});
}
const sig = req.headers['stripe-signature'] as string;
const event = stripe.webhooks.constructEvent(
req.body,
sig,
await getStripeWebhookSecret()
);
switch (event.type) {
case '':

View File

@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { UserAction } from '../../models';
/**
@@ -11,28 +10,19 @@ import { UserAction } from '../../models';
export const addUserAction = async (req: Request, res: Response) => {
// add/record new action [action] for user with id [req.user._id]
let userAction;
try {
const { action } = req.body;
const { action } = req.body;
userAction = await UserAction.findOneAndUpdate(
{
user: req.user._id,
action
},
{ user: req.user._id, action },
{
new: true,
upsert: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to record user action'
});
}
const userAction = await UserAction.findOneAndUpdate(
{
user: req.user._id,
action
},
{ user: req.user._id, action },
{
new: true,
upsert: true
}
);
return res.status(200).send({
message: 'Successfully recorded user action',
@@ -48,21 +38,12 @@ export const addUserAction = async (req: Request, res: Response) => {
*/
export const getUserAction = async (req: Request, res: Response) => {
// get user action [action] for user with id [req.user._id]
let userAction;
try {
const action: string = req.query.action as string;
const action: string = req.query.action as string;
userAction = await UserAction.findOne({
user: req.user._id,
action
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get user action'
});
}
const userAction = await UserAction.findOne({
user: req.user._id,
action
});
return res.status(200).send({
userAction

View File

@@ -1,5 +1,4 @@
import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import {
Workspace,
Membership,
@@ -14,6 +13,7 @@ import {
createWorkspace as create,
deleteWorkspace as deleteWork,
} from "../../helpers/workspace";
import { EELicenseService } from '../../ee/services';
import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables";
@@ -24,27 +24,18 @@ import { ADMIN } from "../../variables";
* @returns
*/
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
let publicKeys;
try {
const { workspaceId } = req.params;
const { workspaceId } = req.params;
publicKeys = (
await Membership.find({
workspace: workspaceId,
}).populate<{ user: IUser }>("user", "publicKey")
).map((member) => {
return {
publicKey: member.user.publicKey,
userId: member.user._id,
};
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace member public keys",
});
}
const publicKeys = (
await Membership.find({
workspace: workspaceId,
}).populate<{ user: IUser }>("user", "publicKey")
).map((member) => {
return {
publicKey: member.user.publicKey,
userId: member.user._id,
};
});
return res.status(200).send({
publicKeys,
@@ -58,20 +49,11 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
let users;
try {
const { workspaceId } = req.params;
const { workspaceId } = req.params;
users = await Membership.find({
workspace: workspaceId,
}).populate("user", "+publicKey");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace members",
});
}
const users = await Membership.find({
workspace: workspaceId,
}).populate("user", "+publicKey");
return res.status(200).send({
users,
@@ -85,20 +67,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspaces = async (req: Request, res: Response) => {
let workspaces;
try {
workspaces = (
await Membership.find({
user: req.user._id,
}).populate("workspace")
).map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspaces",
});
}
const workspaces = (
await Membership.find({
user: req.user._id,
}).populate("workspace")
).map((m) => m.workspace);
return res.status(200).send({
workspaces,
@@ -112,20 +85,11 @@ export const getWorkspaces = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspace = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceId } = req.params;
const { workspaceId } = req.params;
workspace = await Workspace.findOne({
_id: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace",
});
}
const workspace = await Workspace.findOne({
_id: workspaceId,
});
return res.status(200).send({
workspace,
@@ -140,43 +104,46 @@ export const getWorkspace = async (req: Request, res: Response) => {
* @returns
*/
export const createWorkspace = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceName, organizationId } = req.body;
const { workspaceName, organizationId } = req.body;
// validate organization membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: organizationId,
});
// validate organization membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: organizationId,
});
if (!membershipOrg) {
throw new Error("Failed to validate organization membership");
}
if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long");
}
// create workspace and add user as member
workspace = await create({
name: workspaceName,
organizationId,
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN],
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to create workspace",
});
if (!membershipOrg) {
throw new Error("Failed to validate organization membership");
}
const plan = await EELicenseService.getPlan(organizationId);
if (plan.workspaceLimit !== null) {
// case: limit imposed on number of workspaces allowed
if (plan.workspacesUsed >= plan.workspaceLimit) {
// case: number of workspaces used exceeds the number of workspaces allowed
return res.status(400).send({
message: 'Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces.'
});
}
}
if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long");
}
// create workspace and add user as member
const workspace = await create({
name: workspaceName,
organizationId,
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN],
});
return res.status(200).send({
workspace,
});
@@ -189,20 +156,12 @@ export const createWorkspace = async (req: Request, res: Response) => {
* @returns
*/
export const deleteWorkspace = async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
const { workspaceId } = req.params;
// delete workspace
await deleteWork({
id: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete workspace",
});
}
// delete workspace
await deleteWork({
id: workspaceId,
});
return res.status(200).send({
message: "Successfully deleted workspace",
@@ -216,29 +175,20 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
* @returns
*/
export const changeWorkspaceName = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceId } = req.params;
const { name } = req.body;
const { workspaceId } = req.params;
const { name } = req.body;
workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId,
},
{
name,
},
{
new: true,
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to change workspace name",
});
}
const workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId,
},
{
name,
},
{
new: true,
}
);
return res.status(200).send({
message: "Successfully changed workspace name",
@@ -253,20 +203,11 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
let integrations;
try {
const { workspaceId } = req.params;
const { workspaceId } = req.params;
integrations = await Integration.find({
workspace: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace integrations",
});
}
const integrations = await Integration.find({
workspace: workspaceId,
});
return res.status(200).send({
integrations,
@@ -283,20 +224,11 @@ export const getWorkspaceIntegrationAuthorizations = async (
req: Request,
res: Response
) => {
let authorizations;
try {
const { workspaceId } = req.params;
const { workspaceId } = req.params;
authorizations = await IntegrationAuth.find({
workspace: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace integration authorizations",
});
}
const authorizations = await IntegrationAuth.find({
workspace: workspaceId,
});
return res.status(200).send({
authorizations,
@@ -313,21 +245,12 @@ export const getWorkspaceServiceTokens = async (
req: Request,
res: Response
) => {
let serviceTokens;
try {
const { workspaceId } = req.params;
// ?? FIX.
serviceTokens = await ServiceToken.find({
user: req.user._id,
workspace: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace service tokens",
});
}
const { workspaceId } = req.params;
// ?? FIX.
const serviceTokens = await ServiceToken.find({
user: req.user._id,
workspace: workspaceId,
});
return res.status(200).send({
serviceTokens,

View File

@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
@@ -14,18 +13,9 @@ import { getSaltRounds } from '../../config';
* @returns
*/
export const getAPIKeyData = async (req: Request, res: Response) => {
let apiKeyData;
try {
apiKeyData = await APIKeyData.find({
user: req.user._id
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get API key data'
});
}
const apiKeyData = await APIKeyData.find({
user: req.user._id
});
return res.status(200).send({
apiKeyData
@@ -38,39 +28,30 @@ export const getAPIKeyData = async (req: Request, res: Response) => {
* @param res
*/
export const createAPIKeyData = async (req: Request, res: Response) => {
let apiKey, apiKeyData;
try {
const { name, expiresIn } = req.body;
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
apiKeyData = await new APIKeyData({
name,
lastUsed: new Date(),
expiresAt,
user: req.user._id,
secretHash
}).save();
// return api key data without sensitive data
apiKeyData = await APIKeyData.findById(apiKeyData._id);
if (!apiKeyData) throw new Error('Failed to find API key data');
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to API key data'
});
}
const { name, expiresIn } = req.body;
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
let apiKeyData = await new APIKeyData({
name,
lastUsed: new Date(),
expiresAt,
user: req.user._id,
secretHash
}).save();
// return api key data without sensitive data
// FIX: fix this any
apiKeyData = await APIKeyData.findById(apiKeyData._id) as any
if (!apiKeyData) throw new Error('Failed to find API key data');
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
return res.status(200).send({
apiKey,
apiKeyData
@@ -84,21 +65,10 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
* @returns
*/
export const deleteAPIKeyData = async (req: Request, res: Response) => {
let apiKeyData;
try {
const { apiKeyDataId } = req.params;
apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete API key data'
});
}
const { apiKeyDataId } = req.params;
const apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
return res.status(200).send({
apiKeyData
});
}
}

View File

@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models';
@@ -35,47 +34,40 @@ declare module 'jsonwebtoken' {
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
try {
const {
email,
clientPublicKey
}: { email: string; clientPublicKey: string } = req.body;
const {
email,
clientPublicKey
}: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({
email
}).select('+salt +verifier');
const user = await User.findOne({
email
}).select('+salt +verifier');
if (!user) throw new Error('Failed to find user');
if (!user) throw new Error('Failed to find user');
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: email }, {
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false });
await LoginSRPDetail.findOneAndReplace({ email: email }, {
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false });
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
};
/**
@@ -86,149 +78,143 @@ export const login1 = async (req: Request, res: Response) => {
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
try {
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
const { email, clientProof } = req.body;
const user = await User.findOne({
email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
const { email, clientProof } = req.body;
const user = await User.findOne({
email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
if (!user) throw new Error('Failed to find user');
if (!user) throw new Error('Failed to find user');
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"))
}
if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"))
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
// generate temporary MFA token
const token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtMfaLifetime(),
secret: await getJwtMfaSecret()
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: 'emailMfa.handlebars',
subjectLine: 'Infisical MFA code',
recipients: [email],
substitutions: {
code
}
});
return res.status(200).send({
mfaEnabled: true,
token
});
}
await checkUserDevice({
user,
ip: req.ip,
userAgent: req.headers['user-agent'] ?? ''
// generate temporary MFA token
const token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtMfaLifetime(),
secret: await getJwtMfaSecret()
});
// issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// case: user does not have MFA enablgged
// return (access) token in response
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
}
if (
user?.protectedKey &&
user?.protectedKeyIV &&
user?.protectedKeyTag
) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV
response.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
// send MFA code [code] to [email]
await sendMail({
template: 'emailMfa.handlebars',
subjectLine: 'Infisical MFA code',
recipients: [email],
substitutions: {
code
}
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
return res.status(200).send({
mfaEnabled: true,
token
});
return res.status(200).send(response);
}
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
// case: user does not have MFA enabled
// return (access) token in response
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
}
if (
user?.protectedKey &&
user?.protectedKeyIV &&
user?.protectedKeyTag
) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV
response.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
return res.status(200).send(response);
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
);
};
/**
@@ -237,30 +223,22 @@ export const login2 = async (req: Request, res: Response) => {
* @param res
*/
export const sendMfaToken = async (req: Request, res: Response) => {
try {
const { email } = req.body;
const { email } = req.body;
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: 'emailMfa.handlebars',
subjectLine: 'Infisical MFA code',
recipients: [email],
substitutions: {
code
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send MFA code'
});
}
// send MFA code [code] to [email]
await sendMail({
template: 'emailMfa.handlebars',
subjectLine: 'Infisical MFA code',
recipients: [email],
substitutions: {
code
}
});
return res.status(200).send({
message: 'Successfully sent new MFA code'
@@ -284,18 +262,24 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
const user = await User.findOne({
email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
if (!user) throw new Error('Failed to find user');
await LoginSRPDetail.deleteOne({ userId: user.id })
await checkUserDevice({
user,
ip: req.ip,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() });
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
@@ -353,7 +337,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
ipAddress: req.realIP
});
return res.status(200).send(resObj);

View File

@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
Secret,
ServiceToken,
@@ -9,7 +8,8 @@ import {
Membership,
} from '../../models';
import { SecretVersion } from '../../ee/models';
import { BadRequestError } from '../../utils/errors';
import { EELicenseService } from '../../ee/services';
import { BadRequestError, WorkspaceNotFoundError } from '../../utils/errors';
import _ from 'lodash';
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
@@ -23,32 +23,43 @@ export const createWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body;
try {
const workspace = await Workspace.findById(workspaceId).exec();
if (
!workspace ||
workspace?.environments.find(
({ name, slug }) => slug === environmentSlug || environmentName === name
)
) {
throw new Error('Failed to create workspace environment');
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.'
});
}
workspace?.environments.push({
name: environmentName,
slug: environmentSlug.toLowerCase(),
});
await workspace.save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create new workspace environment',
});
}
if (
!workspace ||
workspace?.environments.find(
({ name, slug }) => slug === environmentSlug || environmentName === name
)
) {
throw new Error('Failed to create workspace environment');
}
workspace?.environments.push({
name: environmentName,
slug: environmentSlug.toLowerCase(),
});
await workspace.save();
await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId);
return res.status(200).send({
message: 'Successfully created new environment',
workspace: workspaceId,
@@ -72,75 +83,67 @@ export const renameWorkspaceEnvironment = async (
) => {
const { workspaceId } = req.params;
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
try {
// user should pass both new slug and env name
if (!environmentSlug || !environmentName) {
throw new Error('Invalid environment given.');
}
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error('Failed to create workspace environment');
}
const isEnvExist = workspace.environments.some(
({ name, slug }) =>
slug !== oldEnvironmentSlug &&
(name === environmentName || slug === environmentSlug)
);
if (isEnvExist) {
throw new Error('Invalid environment given');
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === oldEnvironmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments[envIndex].name = environmentName;
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
await workspace.save();
await Secret.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretVersion.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceToken.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceTokenData.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Membership.updateMany(
{
workspace: workspaceId,
"deniedPermissions.environmentSlug": oldEnvironmentSlug
},
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
)
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update workspace environment',
});
// user should pass both new slug and env name
if (!environmentSlug || !environmentName) {
throw new Error('Invalid environment given.');
}
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error('Failed to create workspace environment');
}
const isEnvExist = workspace.environments.some(
({ name, slug }) =>
slug !== oldEnvironmentSlug &&
(name === environmentName || slug === environmentSlug)
);
if (isEnvExist) {
throw new Error('Invalid environment given');
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === oldEnvironmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments[envIndex].name = environmentName;
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
await workspace.save();
await Secret.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretVersion.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceToken.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceTokenData.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Membership.updateMany(
{
workspace: workspaceId,
"deniedPermissions.environmentSlug": oldEnvironmentSlug
},
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
)
return res.status(200).send({
message: 'Successfully update environment',
workspace: workspaceId,
@@ -163,57 +166,50 @@ export const deleteWorkspaceEnvironment = async (
) => {
const { workspaceId } = req.params;
const { environmentSlug } = req.body;
try {
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error('Failed to create workspace environment');
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === environmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments.splice(envIndex, 1);
await workspace.save();
// clean up
await Secret.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await SecretVersion.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceToken.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceTokenData.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Membership.updateMany(
{ workspace: workspaceId },
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
)
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace environment',
});
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error('Failed to create workspace environment');
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === environmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments.splice(envIndex, 1);
await workspace.save();
// clean up
await Secret.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await SecretVersion.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceToken.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceTokenData.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Membership.updateMany(
{ workspace: workspaceId },
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
);
await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId);
return res.status(200).send({
message: 'Successfully deleted environment',
workspace: workspaceId,

View File

@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
MembershipOrg,
@@ -49,20 +48,11 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
}
}
*/
let memberships;
try {
const { organizationId } = req.params;
const { organizationId } = req.params;
memberships = await MembershipOrg.find({
const memberships = await MembershipOrg.find({
organization: organizationId
}).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization memberships'
});
}
return res.status(200).send({
memberships
@@ -128,26 +118,17 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
}
}
*/
let membership;
try {
const { membershipId } = req.params;
const { role } = req.body;
membership = await MembershipOrg.findByIdAndUpdate(
membershipId,
{
role
}, {
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update organization membership'
});
}
const { membershipId } = req.params;
const { role } = req.body;
const membership = await MembershipOrg.findByIdAndUpdate(
membershipId,
{
role
}, {
new: true
}
);
return res.status(200).send({
membership
@@ -197,25 +178,16 @@ export const deleteOrganizationMembership = async (req: Request, res: Response)
}
}
*/
let membership;
try {
const { membershipId } = req.params;
// delete organization membership
membership = await deleteMembershipOrg({
membershipOrgId: membershipId
});
const { membershipId } = req.params;
// delete organization membership
const membership = await deleteMembershipOrg({
membershipOrgId: membershipId
});
await updateSubscriptionOrgQuantity({
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString()
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization membership'
});
}
return res.status(200).send({
membership
@@ -303,4 +275,4 @@ export const getOrganizationServiceAccounts = async (req: Request, res: Response
return res.status(200).send({
serviceAccounts
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +1,27 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import { Request, Response } from "express";
import crypto from "crypto";
import bcrypt from "bcrypt";
import { User, ServiceAccount, ServiceTokenData } from "../../models";
import { userHasWorkspaceAccess } from "../../ee/helpers/checkMembershipPermissions";
import {
User,
ServiceAccount,
ServiceTokenData
} from '../../models';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
import {
PERMISSION_READ_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN
} from '../../variables';
import { getSaltRounds } from '../../config';
import { BadRequestError } from '../../utils/errors';
PERMISSION_READ_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
} 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
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getServiceTokenData = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Return Infisical Token data'
#swagger.description = 'Return Infisical Token data'
@@ -37,111 +34,135 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
"application/json": {
"schema": {
"type": "object",
"properties": {
"properties": {
"serviceTokenData": {
"type": "object",
$ref: "#/components/schemas/ServiceTokenData",
"description": "Details of service token"
}
}
}
}
}
}
}
*/
if (!(req.authData.authPayload instanceof ServiceTokenData)) throw BadRequestError({
message: 'Failed accepted client validation for service token data'
if (!(req.authData.authPayload instanceof ServiceTokenData))
throw BadRequestError({
message: "Failed accepted client validation for service token data",
});
const serviceTokenData = await ServiceTokenData
.findById(req.authData.authPayload._id)
.select('+encryptedKey +iv +tag')
.populate('user');
const serviceTokenData = await ServiceTokenData.findById(
req.authData.authPayload._id
)
.select("+encryptedKey +iv +tag")
.populate("user");
return res.status(200).json(serviceTokenData);
}
return res.status(200).json(serviceTokenData);
};
/**
* Create new service token data for workspace with id [workspaceId] and
* environment [environment].
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData;
let serviceTokenData;
const {
name,
workspaceId,
environment,
encryptedKey,
iv,
tag,
expiresIn,
permissions
} = req.body;
const {
name,
workspaceId,
environment,
encryptedKey,
iv,
tag,
expiresIn,
secretPath,
permissions,
} = req.body;
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
});
let expiresAt;
if (expiresIn) {
expiresAt = new Date()
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (folder == undefined) {
throw BadRequestError({ message: "Path for service token does not exist" })
}
}
let user, serviceAccount;
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
user = req.authData.authPayload._id;
}
const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
if (req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && req.authData.authPayload instanceof ServiceAccount) {
serviceAccount = req.authData.authPayload._id;
}
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
environment,
user,
serviceAccount,
lastUsed: new Date(),
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
permissions
}).save();
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
let user, serviceAccount;
if (!serviceTokenData) throw new Error('Failed to find service token data');
if (
req.authData.authMode === AUTH_MODE_JWT &&
req.authData.authPayload instanceof User
) {
user = req.authData.authPayload._id;
}
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
if (
req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
req.authData.authPayload instanceof ServiceAccount
) {
serviceAccount = req.authData.authPayload._id;
}
return res.status(200).send({
serviceToken,
serviceTokenData
});
}
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
environment,
user,
serviceAccount,
lastUsed: new Date(),
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
secretPath,
permissions,
}).save();
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (!serviceTokenData) throw new Error("Failed to find service token data");
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
return res.status(200).send({
serviceToken,
serviceTokenData,
});
};
/**
* Delete service token data with id [serviceTokenDataId].
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const deleteServiceTokenData = async (req: Request, res: Response) => {
const { serviceTokenDataId } = req.params;
const { serviceTokenDataId } = req.params;
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(
serviceTokenDataId
);
return res.status(200).send({
serviceTokenData
});
}
return res.status(200).send({
serviceTokenData,
});
};

View File

@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { User, MembershipOrg } from '../../models';
import { completeAccount } from '../../helpers/user';
import {
@@ -20,136 +19,130 @@ import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
*/
export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token, refreshToken;
try {
const {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
organizationName
}: {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
organizationName: string;
} = req.body;
const {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
organizationName
}: {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
organizationName: string;
} = req.body;
// get user
user = await User.findOne({ email });
// get user
user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: 'Failed to complete account for complete user'
});
}
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: 'Failed to complete account for complete user'
});
}
// complete setting up user's account
user = await completeAccount({
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
// complete setting up user's account
user = await completeAccount({
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
if (!user)
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
if (!user)
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
// initialize default organization and workspace
await initializeDefaultOrg({
organizationName,
user
});
// initialize default organization and workspace
await initializeDefaultOrg({
organizationName,
user
});
// update organization membership statuses that are
// invited to completed with user attached
const membershipsToUpdate = await MembershipOrg.find({
inviteEmail: email,
status: INVITED
});
membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString()
});
});
// update organization membership statuses that are
// invited to completed with user attached
const membershipsToUpdate = await MembershipOrg.find({
inviteEmail: email,
status: INVITED
});
membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString()
});
});
// update organization membership statuses that are
// invited to completed with user attached
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED
},
{
user,
status: ACCEPTED
}
);
// update organization membership statuses that are
// invited to completed with user attached
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED
},
{
user,
status: ACCEPTED
}
);
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id.toString()
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
token = tokens.token;
token = tokens.token;
// sending a welcome email to new users
if (await getLoopsApiKey()) {
await standardRequest.post("https://app.loops.so/api/v1/events/send", {
"email": email,
"eventName": "Sign Up",
"firstName": firstName,
"lastName": lastName
}, {
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + (await getLoopsApiKey())
},
});
}
// sending a welcome email to new users
if (await getLoopsApiKey()) {
await standardRequest.post("https://app.loops.so/api/v1/events/send", {
"email": email,
"eventName": "Sign Up",
"firstName": firstName,
"lastName": lastName
}, {
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + (await getLoopsApiKey())
},
});
}
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to complete account setup'
});
}
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
return res.status(200).send({
message: 'Successfully set up account',
@@ -167,109 +160,103 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
*/
export const completeAccountInvite = async (req: Request, res: Response) => {
let user, token, refreshToken;
try {
const {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
} = req.body;
const {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
} = req.body;
// get user
user = await User.findOne({ email });
// get user
user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: 'Failed to complete account for complete user'
});
}
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: 'Failed to complete account for complete user'
});
}
const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email,
status: INVITED
});
const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email,
status: INVITED
});
if (!membershipOrg) throw new Error('Failed to find invitations for email');
if (!membershipOrg) throw new Error('Failed to find invitations for email');
// complete setting up user's account
user = await completeAccount({
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
// complete setting up user's account
user = await completeAccount({
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
if (!user)
throw new Error('Failed to complete account for non-existent user');
// update organization membership statuses that are
// invited to completed with user attached
const membershipsToUpdate = await MembershipOrg.find({
inviteEmail: email,
status: INVITED
});
membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString()
});
});
if (!user)
throw new Error('Failed to complete account for non-existent user');
// update organization membership statuses that are
// invited to completed with user attached
const membershipsToUpdate = await MembershipOrg.find({
inviteEmail: email,
status: INVITED
});
membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString()
});
});
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED
},
{
user,
status: ACCEPTED
}
);
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED
},
{
user,
status: ACCEPTED
}
);
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id.toString()
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
token = tokens.token;
token = tokens.token;
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to complete account setup'
});
}
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
return res.status(200).send({
message: 'Successfully set up account',
user,
token
});
};
};

View File

@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Membership, Secret,
@@ -69,4 +68,4 @@ export const getWorkspaceTags = async (req: Request, res: Response) => {
return res.json({
workspaceTags
})
}
}

View File

@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
User,
MembershipOrg
@@ -37,18 +36,9 @@ export const getMe = async (req: Request, res: Response) => {
}
}
*/
let user;
try {
user = await User
.findById(req.user._id)
.select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get current user'
});
}
const user = await User
.findById(req.user._id)
.select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
return res.status(200).send({
user
@@ -64,29 +54,20 @@ export const getMe = async (req: Request, res: Response) => {
* @returns
*/
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
let user;
try {
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
req.user.isMfaEnabled = isMfaEnabled;
if (isMfaEnabled) {
// TODO: adapt this route/controller
// to work for different forms of MFA
req.user.mfaMethods = ['email'];
} else {
req.user.mfaMethods = [];
}
await req.user.save();
user = req.user;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to update current user's MFA status"
});
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
req.user.isMfaEnabled = isMfaEnabled;
if (isMfaEnabled) {
// TODO: adapt this route/controller
// to work for different forms of MFA
req.user.mfaMethods = ['email'];
} else {
req.user.mfaMethods = [];
}
await req.user.save();
const user = req.user;
return res.status(200).send({
user
@@ -126,22 +107,13 @@ export const getMyOrganizations = async (req: Request, res: Response) => {
}
}
*/
let organizations;
try {
organizations = (
await MembershipOrg.find({
user: req.user._id
}).populate('organization')
).map((m) => m.organization);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get current user's organizations"
});
}
const organizations = (
await MembershipOrg.find({
user: req.user._id
}).populate('organization')
).map((m) => m.organization);
return res.status(200).send({
organizations
});
}
}

View File

@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Workspace,
@@ -47,66 +46,57 @@ interface V2PushSecret {
*/
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
try {
const postHogClient = await TelemetryService.getPostHogClient();
let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
const postHogClient = await TelemetryService.getPostHogClient();
let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
// sanitize secrets
secrets = secrets.filter(
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
);
// sanitize secrets
secrets = secrets.filter(
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
);
await push({
userId: req.user._id,
workspaceId,
environment,
secrets,
channel: channel ? channel : 'cli',
ipAddress: req.ip
});
await push({
userId: req.user._id,
workspaceId,
environment,
secrets,
channel: channel ? channel : 'cli',
ipAddress: req.realIP
});
await pushKeys({
userId: req.user._id,
workspaceId,
keys
});
await pushKeys({
userId: req.user._id,
workspaceId,
keys
});
if (postHogClient) {
postHogClient.capture({
event: 'secrets pushed',
distinctId: req.user.email,
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
if (postHogClient) {
postHogClient.capture({
event: 'secrets pushed',
distinctId: req.user.email,
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload workspace secrets'
});
}
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
return res.status(200).send({
message: 'Successfully uploaded workspace secrets'
@@ -122,56 +112,48 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
*/
export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
try {
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
let userId;
if (req.user) {
userId = req.user._id.toString();
} else if (req.serviceTokenData) {
userId = req.serviceTokenData.user.toString();
}
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
let userId;
if (req.user) {
userId = req.user._id.toString();
} else if (req.serviceTokenData) {
userId = req.serviceTokenData.user.toString();
}
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
secrets = await pull({
userId,
workspaceId,
environment,
channel: channel ? channel : 'cli',
ipAddress: req.ip
});
secrets = await pull({
userId,
workspaceId,
environment,
channel: channel ? channel : 'cli',
ipAddress: req.realIP
});
if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets });
}
if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets });
}
if (postHogClient) {
// capture secrets pushed event in production
postHogClient.capture({
distinctId: req.user.email,
event: 'secrets pulled',
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
if (postHogClient) {
// capture secrets pushed event in production
postHogClient.capture({
distinctId: req.user.email,
event: 'secrets pulled',
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
return res.status(200).send({
secrets
@@ -208,22 +190,14 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
}
*/
let key;
try {
const { workspaceId } = req.params;
const { workspaceId } = req.params;
key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
}).populate('sender', '+publicKey');
key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
}).populate('sender', '+publicKey');
if (!key) throw new Error('Failed to find workspace key');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace key'
});
}
if (!key) throw new Error('Failed to find workspace key');
return res.status(200).json(key);
}
@@ -231,23 +205,13 @@ export const getWorkspaceServiceTokenData = async (
req: Request,
res: Response
) => {
let serviceTokenData;
try {
const { workspaceId } = req.params;
const { workspaceId } = req.params;
serviceTokenData = await ServiceTokenData
.find({
workspace: workspaceId
})
.select('+encryptedKey +iv +tag');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace service token data'
});
}
const serviceTokenData = await ServiceTokenData
.find({
workspace: workspaceId
})
.select('+encryptedKey +iv +tag');
return res.status(200).send({
serviceTokenData
@@ -294,20 +258,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
}
}
*/
let memberships;
try {
const { workspaceId } = req.params;
const { workspaceId } = req.params;
memberships = await Membership.find({
workspace: workspaceId
}).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace memberships'
});
}
const memberships = await Membership.find({
workspace: workspaceId
}).populate('user', '+publicKey');
return res.status(200).send({
memberships
@@ -374,29 +329,20 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
}
}
*/
let membership;
try {
const {
membershipId
} = req.params;
const { role } = req.body;
membership = await Membership.findByIdAndUpdate(
membershipId,
{
role
}, {
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update workspace membership'
});
}
const {
membershipId
} = req.params;
const { role } = req.body;
const membership = await Membership.findByIdAndUpdate(
membershipId,
{
role
}, {
new: true
}
);
return res.status(200).send({
membership
});
@@ -445,27 +391,18 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
}
}
*/
let membership;
try {
const {
membershipId
} = req.params;
membership = await Membership.findByIdAndDelete(membershipId);
if (!membership) throw new Error('Failed to delete workspace membership');
await Key.deleteMany({
receiver: membership.user,
workspace: membership.workspace
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace membership'
});
}
const {
membershipId
} = req.params;
const membership = await Membership.findByIdAndDelete(membershipId);
if (!membership) throw new Error('Failed to delete workspace membership');
await Key.deleteMany({
receiver: membership.user,
workspace: membership.workspace
});
return res.status(200).send({
membership
@@ -479,32 +416,23 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
* @returns
*/
export const toggleAutoCapitalization = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceId } = req.params;
const { autoCapitalization } = req.body;
const { workspaceId } = req.params;
const { autoCapitalization } = req.body;
workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId
},
{
autoCapitalization
},
{
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change autoCapitalization setting'
});
}
const workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId
},
{
autoCapitalization
},
{
new: true
}
);
return res.status(200).send({
message: 'Successfully changed autoCapitalization setting',
workspace
});
};
};

View File

@@ -0,0 +1,264 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models';
import { issueAuthTokens, createToken, validateProviderAuthToken } from '../../helpers/auth';
import { checkUserDevice } from '../../helpers/user';
import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services';
import { EELogService } from '../../ee/services';
import { BadRequestError, InternalServerError } from '../../utils/errors';
import {
TOKEN_EMAIL_MFA,
ACTION_LOGIN
} from '../../variables';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getJwtMfaLifetime,
getJwtMfaSecret,
getHttpsEnabled,
} from '../../config';
import { AuthProvider } from '../../models/user';
declare module 'jsonwebtoken' {
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
userId: string;
email: string;
authProvider: AuthProvider;
isUserCompleted: boolean,
}
}
/**
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req
* @param res
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
try {
const {
email,
providerAuthToken,
clientPublicKey
}: {
email: string;
clientPublicKey: string,
providerAuthToken?: string;
} = req.body;
const user = await User.findOne({
email,
}).select('+salt +verifier');
if (!user) throw new Error('Failed to find user');
if (user.authProvider) {
await validateProviderAuthToken({
email,
user,
providerAuthToken,
})
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({
email: email
}, {
email,
userId: user.id,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false });
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
};
/**
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
* private key
* @param req
* @param res
* @returns
*/
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;
const user = await User.findOne({
email,
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
if (!user) throw new Error('Failed to find user');
if (user.authProvider) {
await validateProviderAuthToken({
email,
user,
providerAuthToken,
})
}
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"))
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
// generate temporary MFA token
const token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtMfaLifetime(),
secret: await getJwtMfaSecret()
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: 'emailMfa.handlebars',
subjectLine: 'Infisical MFA code',
recipients: [user.email],
substitutions: {
code
}
});
return res.status(200).send({
mfaEnabled: true,
token
});
}
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
// case: user does not have MFA enablgged
// return (access) token in response
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
}
if (
user?.protectedKey &&
user?.protectedKeyIV &&
user?.protectedKeyTag
) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV
response.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.realIP
});
return res.status(200).send(response);
}
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
};

View File

@@ -1,7 +1,11 @@
import * as secretsController from './secretsController';
import * as workspacesController from './workspacesController';
import * as authController from './authController';
import * as signupController from './signupController';
export {
authController,
secretsController,
workspacesController
}
signupController,
workspacesController,
}

View File

@@ -1,183 +1,420 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import {
SecretService,
TelemetryService,
EventService
} from '../../services';
import { eventPushSecrets } from '../../events';
import { getAuthDataPayloadIdObj } from '../../utils/auth';
import { BadRequestError } from '../../utils/errors';
import { Request, Response } from "express";
import { Types } from "mongoose";
import { SecretService, EventService } from "../../services";
import { eventPushSecrets } from "../../events";
import { BotService } from "../../services";
import { repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
/**
* Get secrets for workspace with id [workspaceId] and environment
* [environment]
* @param req
* @param res
* Return secrets for workspace with id [workspaceId] and environment
* [environment] in plaintext
* @param req
* @param res
*/
export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
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;
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
authData: req.authData
});
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
authData: req.authData,
});
return res.status(200).send({
secrets
});
}
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
secrets: secrets.map((secret) => {
const rep = repackageSecretToRaw({
secret,
key
});
return rep;
})
});
};
/**
* Get secret with name [secretName]
* @param req
* @param res
* Return secret with name [secretName] in plaintext
* @param req
* @param res
*/
export const getSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const type = req.query.type as 'shared' | 'personal' | undefined;
export const getSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const type = req.query.type as "shared" | "personal" | undefined;
const secret = await SecretService.getSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData
});
return res.status(200).send({
secret
});
}
const secret = await SecretService.getSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
secretPath,
authData: req.authData,
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
secret: repackageSecretToRaw({
secret,
key
})
});
};
/**
* Create secret with name [secretName]
* @param req
* Create secret with name [secretName] in plaintext
* @param req
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
} = req.body;
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
...((secretCommentCiphertext && secretCommentIV && secretCommentTag) ? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
} : {})
});
export const createSecretRaw = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretValue,
secretComment,
secretPath = "/"
} = req.body;
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
});
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({
secret: secretWithoutBlindIndex
});
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretName,
key
});
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretValue,
key
});
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretComment,
key
});
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretPath,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({
secret: repackageSecretToRaw({
secret: secretWithoutBlindIndex,
key
})
});
}
/**
* Update secret with name [secretName]
* @param req
* @param res
* @param res
*/
export const updateSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretValueCiphertext,
secretValueIV,
secretValueTag
} = req.body;
export const updateSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretValue,
secretPath = "/",
} = req.body;
const secret = await SecretService.updateSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData,
secretValueCiphertext,
secretValueIV,
secretValueTag
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
secret
});
}
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretValue,
key
});
const secret = await SecretService.updateSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretPath,
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
return res.status(200).send({
secret: repackageSecretToRaw({
secret,
key
})
});
};
/**
* Delete secret with name [secretName]
* @param req
* @param res
* @param req
* @param res
*/
export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretPath = "/"
} = req.body;
const { secret } = await SecretService.deleteSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData,
secretPath,
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
secret: repackageSecretToRaw({
secret,
key
})
});
};
/**
* Get secrets for workspace with id [workspaceId] and environment
* [environment]
* @param req
* @param res
*/
export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
authData: req.authData,
});
return res.status(200).send({
secrets,
});
};
/**
* Return secret with name [secretName]
* @param req
* @param res
*/
export const getSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const type = req.query.type as "shared" | "personal" | undefined;
const secret = await SecretService.getSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
secretPath,
authData: req.authData,
});
return res.status(200).send({
secret,
});
};
/**
* Create secret with name [secretName]
* @param req
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretPath = "/",
} = req.body;
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({
secret: secretWithoutBlindIndex,
});
};
/**
* Update secret with name [secretName]
* @param req
* @param res
*/
export const updateSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath = "/",
} = req.body;
const secret = await SecretService.updateSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath,
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
return res.status(200).send({
secret,
});
};
/**
* Delete secret with name [secretName]
* @param req
* @param res
*/
export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type
} = req.body;
const { secret, secrets } = await SecretService.deleteSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData
});
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretPath = "/"
} = req.body;
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
const { secret } = await SecretService.deleteSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData,
secretPath,
});
return res.status(200).send({
secret
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
return res.status(200).send({
secret,
});
};

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Action, SecretVersion } from '../../models';
import { ActionNotFoundError } from '../../../utils/errors';
@@ -28,4 +27,4 @@ export const getAction = async (req: Request, res: Response) => {
return res.status(200).send({
action
});
}
}

View File

@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import { EELicenseService } from '../../services';
import { getLicenseServerUrl } from '../../../config';
@@ -12,23 +11,18 @@ import { licenseServerKeyRequest } from '../../../config/request';
* @returns
*/
export const getCloudProducts = async (req: Request, res: Response) => {
try {
const billingCycle = req.query['billing-cycle'] as string;
const billingCycle = req.query['billing-cycle'] as string;
if (EELicenseService.instanceType === 'cloud') {
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
);
if (EELicenseService.instanceType === 'cloud') {
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
);
return res.status(200).send(data);
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(200).send(data);
}
return res.status(200).send({
head: [],
rows: []
});
}
}

View File

@@ -8,8 +8,9 @@ import { EELicenseService } from '../../services';
*/
export const getOrganizationPlan = async (req: Request, res: Response) => {
const { organizationId } = req.params;
const workspaceId = req.query.workspaceId as string;
const plan = await EELicenseService.getOrganizationPlan(organizationId);
const plan = await EELicenseService.getPlan(organizationId, workspaceId);
return res.status(200).send({
plan,

View File

@@ -1,16 +1,15 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Secret } from '../../../models';
import { SecretVersion } from '../../models';
import { EESecretService } from '../../services';
import { Request, Response } from "express";
import { Secret } from "../../../models";
import { SecretVersion } from "../../models";
import { EESecretService } from "../../services";
/**
* Return secret versions for secret with id [secretId]
* @param req
* @param res
* @param req
* @param res
*/
export const getSecretVersions = async (req: Request, res: Response) => {
/*
export const getSecretVersions = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return secret versions'
#swagger.description = 'Return secret versions'
@@ -55,41 +54,34 @@ import { EESecretService } from '../../services';
}
}
*/
let secretVersions;
try {
const { secretId } = req.params;
const { secretId, workspaceId, environment, folderId } = req.params;
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
secretVersions = await SecretVersion.find({
secret: secretId
})
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get secret versions'
});
}
return res.status(200).send({
secretVersions
});
}
const secretVersions = await SecretVersion.find({
secret: secretId,
workspace: workspaceId,
environment,
folder: folderId,
})
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
return res.status(200).send({
secretVersions,
});
};
/**
* Roll back secret with id [secretId] to version [version]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const rollbackSecretVersion = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Roll back secret to a version.'
#swagger.description = 'Roll back secret to a version.'
@@ -137,97 +129,92 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
}
}
*/
let secret;
try {
const { secretId } = req.params;
const { version } = req.body;
// validate secret version
const oldSecretVersion = await SecretVersion.findOne({
secret: secretId,
version
}).select('+secretBlindIndex')
if (!oldSecretVersion) throw new Error('Failed to find secret version');
const {
workspace,
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
keyEncoding
} = oldSecretVersion;
// update secret
secret = await Secret.findByIdAndUpdate(
secretId,
{
$inc: {
version: 1
},
workspace,
type,
user,
environment,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
keyEncoding
},
{
new: true
}
);
if (!secret) throw new Error('Failed to find and update secret');
const { secretId } = req.params;
const { version } = req.body;
// add new secret version
await new SecretVersion({
secret: secretId,
version: secret.version,
workspace,
type,
user,
environment,
isDeleted: false,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
keyEncoding
}).save();
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: secret.workspace
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to roll back secret version'
});
}
return res.status(200).send({
secret
});
}
// validate secret version
const oldSecretVersion = await SecretVersion.findOne({
secret: secretId,
version,
}).select("+secretBlindIndex");
if (!oldSecretVersion) throw new Error("Failed to find secret version");
const {
workspace,
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
folder,
keyEncoding,
} = oldSecretVersion;
// update secret
const secret = await Secret.findByIdAndUpdate(
secretId,
{
$inc: {
version: 1,
},
workspace,
type,
user,
environment,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
folderId: folder,
algorithm,
keyEncoding,
},
{
new: true,
}
);
if (!secret) throw new Error("Failed to find and update secret");
// add new secret version
await new SecretVersion({
secret: secretId,
version: secret.version,
workspace,
type,
user,
environment,
isDeleted: false,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
folder,
algorithm,
keyEncoding,
}).save();
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: secret.workspace,
environment,
folderId: folder,
});
return res.status(200).send({
secret,
});
};

View File

@@ -1,33 +1,45 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { SecretSnapshot } from '../../models';
import { Request, Response } from "express";
import {
ISecretVersion,
SecretSnapshot,
TFolderRootVersionSchema,
} from "../../models";
/**
* Return secret snapshot with id [secretSnapshotId]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getSecretSnapshot = async (req: Request, res: Response) => {
let secretSnapshot;
try {
const { secretSnapshotId } = req.params;
const { secretSnapshotId } = req.params;
secretSnapshot = await SecretSnapshot
.findById(secretSnapshotId)
.populate('secretVersions');
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
.lean()
.populate<{ secretVersions: ISecretVersion[] }>({
path: 'secretVersions',
populate: {
path: 'tags',
model: 'Tag'
}
})
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
const folderId = secretSnapshot.folderId;
// to show only the folder required secrets
secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter(
({ folder }) => folder === folderId
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get secret snapshot'
});
}
return res.status(200).send({
secretSnapshot
});
}
secretSnapshot.folderVersion =
secretSnapshot?.folderVersion?.nodes?.children?.map(({ id, name }) => ({
id,
name,
})) as any;
return res.status(200).send({
secretSnapshot,
});
};

View File

@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import Stripe from 'stripe';
import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
@@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
* @returns
*/
export const handleWebhook = async (req: Request, res: Response) => {
let event;
try {
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
// check request for valid stripe signature
const sig = req.headers['stripe-signature'] as string;
event = stripe.webhooks.constructEvent(
req.body,
sig,
await getStripeWebhookSecret()
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to process webhook'
});
}
// check request for valid stripe signature
const sig = req.headers['stripe-signature'] as string;
const event = stripe.webhooks.constructEvent(
req.body,
sig,
await getStripeWebhookSecret()
);
switch (event.type) {
case '':

View File

@@ -1,25 +1,29 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Request, Response } from "express";
import { PipelineStage, Types } from "mongoose";
import { Secret } from "../../../models";
import {
Secret
} from '../../../models';
import {
SecretSnapshot,
Log,
SecretVersion,
ISecretVersion
} from '../../models';
import { EESecretService } from '../../services';
import { getLatestSecretVersionIds } from '../../helpers/secretVersion';
SecretSnapshot,
Log,
SecretVersion,
ISecretVersion,
FolderVersion,
TFolderRootVersionSchema,
} from "../../models";
import { EESecretService } from "../../services";
import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
import Folder, { TFolderSchema } from "../../../models/folder";
import { searchByFolderId } from "../../../services/FolderService";
/**
* Return secret snapshots for workspace with id [workspaceId]
* @param req
* @param res
* @param req
* @param res
*/
export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => {
/*
export const getWorkspaceSecretSnapshots = async (
req: Request,
res: Response
) => {
/*
#swagger.summary = 'Return project secret snapshot ids'
#swagger.description = 'Return project secret snapshots ids'
@@ -64,66 +68,60 @@ import { getLatestSecretVersionIds } from '../../helpers/secretVersion';
}
}
*/
let secretSnapshots;
try {
const { workspaceId } = req.params;
const { workspaceId } = req.params;
const { environment, folderId } = req.query;
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId
})
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get secret snapshots'
});
}
return res.status(200).send({
secretSnapshots
});
}
const secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId,
environment,
folderId: folderId || "root",
})
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
return res.status(200).send({
secretSnapshots,
});
};
/**
* Return count of secret snapshots for workspace with id [workspaceId]
* @param req
* @param res
* @param req
* @param res
*/
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
let count;
try {
const { workspaceId } = req.params;
count = await SecretSnapshot.countDocuments({
workspace: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to count number of secret snapshots'
});
}
return res.status(200).send({
count
});
}
export const getWorkspaceSecretSnapshotsCount = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const { environment, folderId } = req.query;
const count = await SecretSnapshot.countDocuments({
workspace: workspaceId,
environment,
folderId: folderId || "root",
});
return res.status(200).send({
count,
});
};
/**
* Rollback secret snapshot with id [secretSnapshotId] to version [version]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => {
/*
export const rollbackWorkspaceSecretSnapshot = async (
req: Request,
res: Response
) => {
/*
#swagger.summary = 'Roll back project secrets to those captured in a secret snapshot version.'
#swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.'
@@ -173,168 +171,329 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
}
}
*/
let secrets;
try {
const { workspaceId } = req.params;
const { version } = req.body;
// validate secret snapshot
const secretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId,
version
}).populate<{ secretVersions: ISecretVersion[]}>({
path: 'secretVersions',
select: '+secretBlindIndex'
});
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
// TODO: fix any
const oldSecretVersionsObj: any = secretSnapshot.secretVersions
.reduce((accumulator, s) => ({
...accumulator,
[`${s.secret.toString()}`]: s
}), {});
const latestSecretVersionIds = await getLatestSecretVersionIds({
secretIds: secretSnapshot.secretVersions.map((sv) => sv.secret)
});
// TODO: fix any
const latestSecretVersions: any = (await SecretVersion.find({
_id: {
$in: latestSecretVersionIds.map((s) => s.versionId)
}
}, 'secret version'))
.reduce((accumulator, s) => ({
...accumulator,
[`${s.secret.toString()}`]: s
}), {});
// delete existing secrets
await Secret.deleteMany({
workspace: workspaceId
});
const { workspaceId } = req.params;
const { version, environment, folderId = "root" } = req.body;
// add secrets
secrets = await Secret.insertMany(
secretSnapshot.secretVersions.map((sv) => {
const secretId = sv.secret;
const {
workspace,
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
createdAt
} = oldSecretVersionsObj[secretId.toString()];
return ({
_id: secretId,
version: latestSecretVersions[secretId.toString()].version + 1,
workspace,
type,
user,
environment,
secretBlindIndex: secretBlindIndex ?? undefined,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext: '',
secretCommentIV: '',
secretCommentTag: '',
createdAt
});
})
);
// add secret versions
const secretV = await SecretVersion.insertMany(
secrets.map(({
_id,
version,
workspace,
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}) => ({
_id: new Types.ObjectId(),
secret: _id,
version,
workspace,
type,
user,
environment,
isDeleted: false,
secretBlindIndex: secretBlindIndex ?? undefined,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}))
);
// update secret versions of restored secrets as not deleted
await SecretVersion.updateMany({
secret: {
$in: secretSnapshot.secretVersions.map((sv) => sv.secret)
}
}, {
isDeleted: false
});
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId)
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to roll back secret snapshot'
});
// validate secret snapshot
const secretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId,
version,
environment,
folderId: folderId,
})
.populate<{ secretVersions: ISecretVersion[] }>({
path: "secretVersions",
select: "+secretBlindIndex",
})
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
const snapshotFolderTree = secretSnapshot.folderVersion;
const latestFolderTree = await Folder.findOne({
workspace: workspaceId,
environment,
});
const latestFolderVersion = await FolderVersion.findOne({
environment,
workspace: workspaceId,
"nodes.id": folderId,
}).sort({ "nodes.version": -1 });
const oldSecretVersionsObj: Record<string, ISecretVersion> = {};
const secretIds: Types.ObjectId[] = [];
const folderIds: string[] = [folderId];
secretSnapshot.secretVersions.forEach((snapSecVer) => {
oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
secretIds.push(snapSecVer.secret);
});
// the parent node from current latest one
// this will be modified according to the snapshot and latest snapshots
const newFolderTree =
latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId);
if (newFolderTree) {
newFolderTree.children = snapshotFolderTree?.nodes?.children || [];
const queue = [newFolderTree];
// a bfs algorithm in which we take the latest snapshots of all the folders in a level
while (queue.length) {
const groupByFolderId: Record<string, TFolderSchema> = {};
// the original queue is popped out completely to get what ever in a level
// subqueue is filled with all the children thus next level folders
// subQueue will then be transfered to the oriinal queue
const subQueue: TFolderSchema[] = [];
// get everything inside a level
while (queue.length) {
const folder = queue.pop() as TFolderSchema;
folder.children.forEach((el) => {
folderIds.push(el.id); // push ids and data into queu
subQueue.push(el);
// to modify the original tree very fast we keep a reference object
// key with folder id and pointing to the various nodes
groupByFolderId[el.id] = el;
});
}
// get latest snapshots of all the folder
const matchWsFoldersPipeline = {
$match: {
workspace: new Types.ObjectId(workspaceId),
environment,
folderId: {
$in: Object.keys(groupByFolderId),
},
},
};
const sortByFolderIdAndVersion: PipelineStage = {
$sort: { folderId: 1, version: -1 },
};
const pickLatestVersionOfEachFolder = {
$group: {
_id: "$folderId",
latestVersion: { $first: "$version" },
doc: {
$first: "$$ROOT",
},
},
};
const populateSecVersion = {
$lookup: {
from: SecretVersion.collection.name,
localField: "doc.secretVersions",
foreignField: "_id",
as: "doc.secretVersions",
},
};
const populateFolderVersion = {
$lookup: {
from: FolderVersion.collection.name,
localField: "doc.folderVersion",
foreignField: "_id",
as: "doc.folderVersion",
},
};
const unwindFolderVerField = {
$unwind: {
path: "$doc.folderVersion",
preserveNullAndEmptyArrays: true,
},
};
const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> =
await SecretSnapshot.aggregate([
matchWsFoldersPipeline,
sortByFolderIdAndVersion,
pickLatestVersionOfEachFolder,
populateSecVersion,
populateFolderVersion,
unwindFolderVerField,
]);
// recursive snapshotting each level
latestSnapshotsByFolders.forEach((snap) => {
// mutate the folder tree to update the nodes to the latest version tree
// we are reconstructing the folder tree by latest snapshots here
if (groupByFolderId[snap.doc.folderId]) {
groupByFolderId[snap.doc.folderId].children =
snap.doc?.folderVersion?.nodes?.children || [];
}
// push all children of next level snapshots
if (snap.doc.folderVersion?.nodes?.children) {
queue.push(...snap.doc.folderVersion.nodes.children);
}
snap.doc.secretVersions.forEach((snapSecVer) => {
// record all the secrets
oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
secretIds.push(snapSecVer.secret);
});
});
queue.push(...subQueue);
}
return res.status(200).send({
secrets
});
}
}
// TODO: fix any
const latestSecretVersionIds = await getLatestSecretVersionIds({
secretIds,
});
// TODO: fix any
const latestSecretVersions: any = (
await SecretVersion.find(
{
_id: {
$in: latestSecretVersionIds.map((s) => s.versionId),
},
},
"secret version"
)
).reduce(
(accumulator, s) => ({
...accumulator,
[`${s.secret.toString()}`]: s,
}),
{}
);
const secDelQuery: Record<string, unknown> = {
workspace: workspaceId,
environment,
// undefined means root thus collect all secrets
};
if (folderId !== "root" && folderIds.length)
secDelQuery.folder = { $in: folderIds };
// delete existing secrets
await Secret.deleteMany(secDelQuery);
await Folder.deleteOne({
workspace: workspaceId,
environment,
});
// add secrets
const secrets = await Secret.insertMany(
Object.keys(oldSecretVersionsObj).map((sv) => {
const {
secret: secretId,
workspace,
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
createdAt,
algorithm,
keyEncoding,
folder: secFolderId,
} = oldSecretVersionsObj[sv];
return {
_id: secretId,
version: latestSecretVersions[secretId.toString()].version + 1,
workspace,
type,
user,
environment,
secretBlindIndex: secretBlindIndex ?? undefined,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext: "",
secretCommentIV: "",
secretCommentTag: "",
createdAt,
algorithm,
keyEncoding,
folder: secFolderId,
};
})
);
// add secret versions
const secretV = await SecretVersion.insertMany(
secrets.map(
({
_id,
version,
workspace,
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
keyEncoding,
folder: secFolderId,
}) => ({
_id: new Types.ObjectId(),
secret: _id,
version,
workspace,
type,
user,
environment,
isDeleted: false,
secretBlindIndex: secretBlindIndex ?? undefined,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
keyEncoding,
folder: secFolderId,
})
)
);
if (newFolderTree && latestFolderTree) {
// save the updated folder tree to the present one
newFolderTree.version = (latestFolderVersion?.nodes?.version || 0) + 1;
latestFolderTree._id = new Types.ObjectId();
latestFolderTree.isNew = true;
await latestFolderTree.save();
// create new folder version
const newFolderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: newFolderTree,
});
await newFolderVersion.save();
}
// update secret versions of restored secrets as not deleted
await SecretVersion.updateMany(
{
secret: {
$in: Object.keys(oldSecretVersionsObj).map(
(sv) => oldSecretVersionsObj[sv].secret
),
},
},
{
isDeleted: false,
}
);
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId,
});
return res.status(200).send({
secrets,
});
};
/**
* Return (audit) logs for workspace with id [workspaceId]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getWorkspaceLogs = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Return project (audit) logs'
#swagger.description = 'Return project (audit) logs'
@@ -400,43 +559,32 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
}
}
*/
let logs
try {
const { workspaceId } = req.params;
const { workspaceId } = req.params;
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
const sortBy: string = req.query.sortBy as string;
const userId: string = req.query.userId as string;
const actionNames: string = req.query.actionNames as string;
logs = await Log.find({
workspace: workspaceId,
...( userId ? { user: userId } : {}),
...(
actionNames
? {
actionNames: {
$in: actionNames.split(',')
}
} : {}
)
})
.sort({ createdAt: sortBy === 'recent' ? -1 : 1 })
.skip(offset)
.limit(limit)
.populate('actions')
.populate('user serviceAccount serviceTokenData');
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
const sortBy: string = req.query.sortBy as string;
const userId: string = req.query.userId as string;
const actionNames: string = req.query.actionNames as string;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace logs'
});
}
return res.status(200).send({
logs
});
}
const logs = await Log.find({
workspace: workspaceId,
...(userId ? { user: userId } : {}),
...(actionNames
? {
actionNames: {
$in: actionNames.split(","),
},
}
: {}),
})
.sort({ createdAt: sortBy === "recent" ? -1 : 1 })
.skip(offset)
.limit(limit)
.populate("actions")
.populate("user serviceAccount serviceTokenData");
return res.status(200).send({
logs,
});
};

View File

@@ -1,6 +1,11 @@
import { Types } from "mongoose";
import { Secret, ISecret } from "../../models";
import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models";
import {
SecretSnapshot,
SecretVersion,
ISecretVersion,
FolderVersion,
} from "../models";
/**
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
@@ -12,22 +17,31 @@ import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models";
*/
const takeSecretSnapshotHelper = async ({
workspaceId,
environment,
folderId = "root",
}: {
workspaceId: Types.ObjectId;
environment: string;
folderId?: string;
}) => {
// get all folder ids
const secretIds = (
await Secret.find(
{
workspace: workspaceId,
environment,
folder: folderId,
},
"_id"
)
).lean()
).map((s) => s._id);
const latestSecretVersions = (
await SecretVersion.aggregate([
{
$match: {
environment,
workspace: new Types.ObjectId(workspaceId),
secret: {
$in: secretIds,
},
@@ -45,6 +59,11 @@ const takeSecretSnapshotHelper = async ({
},
]).exec()
).map((s) => s.versionId);
const latestFolderVersion = await FolderVersion.findOne({
environment,
workspace: workspaceId,
"nodes.id": folderId,
}).sort({ "nodes.version": -1 });
const latestSecretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId,
@@ -52,8 +71,11 @@ const takeSecretSnapshotHelper = async ({
const secretSnapshot = await new SecretSnapshot({
workspace: workspaceId,
environment,
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
secretVersions: latestSecretVersions,
folderId,
folderVersion: latestFolderVersion,
}).save();
return secretSnapshot;
@@ -96,5 +118,5 @@ const markDeletedSecretVersionsHelper = async ({
export {
takeSecretSnapshotHelper,
addSecretVersionsHelper,
markDeletedSecretVersionsHelper
markDeletedSecretVersionsHelper,
};

View File

@@ -1,92 +1,82 @@
import { Types } from 'mongoose';
import { SecretVersion } from '../models';
import { Types } from "mongoose";
import { SecretVersion } from "../models";
/**
* Return latest secret versions for secrets with ids [secretIds]
* @param {Object} obj
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
* @returns
* @returns
*/
const getLatestSecretVersionIds = async ({
secretIds
secretIds,
}: {
secretIds: Types.ObjectId[];
secretIds: Types.ObjectId[];
}) => {
interface LatestSecretVersionId {
_id: Types.ObjectId;
version: number;
versionId: Types.ObjectId;
}
const latestSecretVersionIds = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
}
}
const latestSecretVersionIds = await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds,
},
{
$group: {
_id: '$secret',
version: { $max: '$version' },
versionId: { $max: '$_id' } // id of latest secret version
}
},
{
$sort: { version: -1 }
}
])
.exec());
return latestSecretVersionIds;
}
},
},
{
$group: {
_id: "$secret",
version: { $max: "$version" },
versionId: { $max: "$_id" }, // id of latest secret version
},
},
{
$sort: { version: -1 },
},
]).exec();
return latestSecretVersionIds;
};
/**
* Return latest [n] secret versions for secrets with ids [secretIds]
* @param {Object} obj
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
* @param {Number} obj.n - number of latest secret versions to return for each secret
* @returns
* @returns
*/
const getLatestNSecretSecretVersionIds = async ({
secretIds,
n
secretIds,
n,
}: {
secretIds: Types.ObjectId[];
n: number;
secretIds: Types.ObjectId[];
n: number;
}) => {
// TODO: optimize query
const latestNSecretVersions = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds,
},
},
// TODO: optimize query
const latestNSecretVersions = await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds,
},
{
$sort: { version: -1 },
},
{
$group: {
_id: "$secret",
versions: { $push: "$$ROOT" },
},
},
{
$project: {
_id: 0,
secret: "$_id",
versions: { $slice: ["$versions", n] },
},
}
]));
return latestNSecretVersions;
}
},
},
{
$sort: { version: -1 },
},
{
$group: {
_id: "$secret",
versions: { $push: "$$ROOT" },
},
},
{
$project: {
_id: 0,
secret: "$_id",
versions: { $slice: ["$versions", n] },
},
},
]);
export {
getLatestSecretVersionIds,
getLatestNSecretSecretVersionIds
}
return latestNSecretVersions;
};
export { getLatestSecretVersionIds, getLatestNSecretSecretVersionIds };

View File

@@ -0,0 +1,60 @@
import { model, Schema, Types } from "mongoose";
export type TFolderRootVersionSchema = {
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
nodes: TFolderVersionSchema;
};
export type TFolderVersionSchema = {
id: string;
name: string;
version: number;
children: TFolderVersionSchema[];
};
const folderVersionSchema = new Schema<TFolderVersionSchema>({
id: {
required: true,
type: String,
default: "root",
},
name: {
required: true,
type: String,
default: "root",
},
version: {
required: true,
type: Number,
default: 1,
},
});
folderVersionSchema.add({ children: [folderVersionSchema] });
const folderRootVersionSchema = new Schema<TFolderRootVersionSchema>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
},
environment: {
type: String,
required: true,
},
nodes: folderVersionSchema,
},
{
timestamps: true,
}
);
const FolderVersion = model<TFolderRootVersionSchema>(
"FolderVersion",
folderRootVersionSchema
);
export default FolderVersion;

View File

@@ -1,15 +1,18 @@
import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot';
import SecretVersion, { ISecretVersion } from './secretVersion';
import Log, { ILog } from './log';
import Action, { IAction } from './action';
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,
Log,
ILog,
Action,
IAction
}
SecretSnapshot,
ISecretSnapshot,
SecretVersion,
ISecretVersion,
FolderVersion,
TFolderRootVersionSchema,
Log,
ILog,
Action,
IAction,
};

View File

@@ -1,33 +1,54 @@
import { Schema, model, Types } from 'mongoose';
import { Schema, model, Types } from "mongoose";
export interface ISecretSnapshot {
workspace: Types.ObjectId;
version: number;
secretVersions: Types.ObjectId[];
workspace: Types.ObjectId;
environment: string;
folderId: string | "root";
version: number;
secretVersions: Types.ObjectId[];
folderVersion: Types.ObjectId;
}
const secretSnapshotSchema = new Schema<ISecretSnapshot>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
version: {
type: Number,
required: true
},
secretVersions: [{
type: Schema.Types.ObjectId,
ref: 'SecretVersion',
required: true
}]
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
},
{
timestamps: true
}
environment: {
type: String,
required: true,
},
folderId: {
type: String,
default: "root",
},
version: {
type: Number,
default: 1,
required: true,
},
secretVersions: [
{
type: Schema.Types.ObjectId,
ref: "SecretVersion",
required: true,
},
],
folderVersion: {
type: Schema.Types.ObjectId,
ref: "FolderVersion",
},
},
{
timestamps: true,
}
);
const SecretSnapshot = model<ISecretSnapshot>('SecretSnapshot', secretSnapshotSchema);
const SecretSnapshot = model<ISecretSnapshot>(
"SecretSnapshot",
secretSnapshotSchema
);
export default SecretSnapshot;
export default SecretSnapshot;

View File

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

View File

@@ -5,7 +5,7 @@ import {
requireOrganizationAuth,
validateRequest
} from '../../../middleware';
import { param, body } from 'express-validator';
import { param, body, query } from 'express-validator';
import { organizationsController } from '../../controllers/v1';
import {
OWNER, ADMIN, MEMBER, ACCEPTED
@@ -21,6 +21,7 @@ router.get(
acceptedStatuses: [ACCEPTED]
}),
param('organizationId').exists().trim(),
query('workspaceId').optional().isString(),
validateRequest,
organizationsController.getOrganizationPlan
);

View File

@@ -1,76 +1,82 @@
import express from 'express';
import express from "express";
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
validateRequest
} from '../../../middleware';
import { param, query, body } from 'express-validator';
import { ADMIN, MEMBER } from '../../../variables';
import { workspaceController } from '../../controllers/v1';
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from "../../../middleware";
import { param, query, body } from "express-validator";
import { ADMIN, MEMBER } from "../../../variables";
import { workspaceController } from "../../controllers/v1";
router.get(
'/:workspaceId/secret-snapshots',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
query('offset').exists().isInt(),
query('limit').exists().isInt(),
validateRequest,
workspaceController.getWorkspaceSecretSnapshots
"/:workspaceId/secret-snapshots",
requireAuth({
acceptedAuthModes: ["jwt", "apiKey"],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
param("workspaceId").exists().trim(),
query("environment").isString().exists().trim(),
query("folderId").default("root").isString().trim(),
query("offset").exists().isInt(),
query("limit").exists().isInt(),
validateRequest,
workspaceController.getWorkspaceSecretSnapshots
);
router.get(
'/:workspaceId/secret-snapshots/count',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspaceSecretSnapshotsCount
"/:workspaceId/secret-snapshots/count",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
param("workspaceId").exists().trim(),
query("environment").isString().exists().trim(),
query("folderId").default("root").isString().trim(),
validateRequest,
workspaceController.getWorkspaceSecretSnapshotsCount
);
router.post(
'/:workspaceId/secret-snapshots/rollback',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
body('version').exists().isInt(),
validateRequest,
workspaceController.rollbackWorkspaceSecretSnapshot
"/:workspaceId/secret-snapshots/rollback",
requireAuth({
acceptedAuthModes: ["jwt", "apiKey"],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
param("workspaceId").exists().trim(),
body("environment").isString().exists().trim(),
query("folderId").default("root").isString().exists().trim(),
body("version").exists().isInt(),
validateRequest,
workspaceController.rollbackWorkspaceSecretSnapshot
);
router.get(
'/:workspaceId/logs',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
query('offset').exists().isInt(),
query('limit').exists().isInt(),
query('sortBy'),
query('userId'),
query('actionNames'),
validateRequest,
workspaceController.getWorkspaceLogs
"/:workspaceId/logs",
requireAuth({
acceptedAuthModes: ["jwt", "apiKey"],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
param("workspaceId").exists().trim(),
query("offset").exists().isInt(),
query("limit").exists().isInt(),
query("sortBy"),
query("userId"),
query("actionNames"),
validateRequest,
workspaceController.getWorkspaceLogs
);
export default router;
export default router;

View File

@@ -1,5 +1,5 @@
import NodeCache from 'node-cache';
import * as Sentry from '@sentry/node';
import NodeCache from 'node-cache';
import {
getLicenseKey,
getLicenseServerKey,
@@ -22,6 +22,8 @@ interface FeatureSet {
workspacesUsed: number;
memberLimit: number | null;
membersUsed: number;
environmentLimit: number | null;
environmentsUsed: number;
secretVersioning: boolean;
pitRecovery: boolean;
rbac: boolean;
@@ -50,6 +52,8 @@ class EELicenseService {
workspacesUsed: 0,
memberLimit: null,
membersUsed: 0,
environmentLimit: null,
environmentsUsed: 0,
secretVersioning: true,
pitRecovery: true,
rbac: true,
@@ -67,10 +71,10 @@ class EELicenseService {
});
}
public async getOrganizationPlan(organizationId: string): Promise<FeatureSet> {
public async getPlan(organizationId: string, workspaceId?: string): Promise<FeatureSet> {
try {
if (this.instanceType === 'cloud') {
const cachedPlan = this.localFeatureSet.get<FeatureSet>(organizationId);
const cachedPlan = this.localFeatureSet.get<FeatureSet>(`${organizationId}-${workspaceId ?? ''}`);
if (cachedPlan) {
return cachedPlan;
}
@@ -78,12 +82,16 @@ class EELicenseService {
const organization = await Organization.findById(organizationId);
if (!organization) throw OrganizationNotFoundError();
const { data: { currentPlan } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`
);
let url = `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`;
if (workspaceId) {
url += `?workspaceId=${workspaceId}`;
}
const { data: { currentPlan } } = await licenseServerKeyRequest.get(url);
// cache fetched plan for organization
this.localFeatureSet.set(organizationId, currentPlan);
this.localFeatureSet.set(`${organizationId}-${workspaceId ?? ''}`, currentPlan);
return currentPlan;
}
@@ -93,6 +101,13 @@ class EELicenseService {
return this.globalFeatureSet;
}
public async refreshPlan(organizationId: string, workspaceId?: string) {
if (this.instanceType === 'cloud') {
this.localFeatureSet.del(`${organizationId}-${workspaceId ?? ''}`);
await this.getPlan(organizationId, workspaceId);
}
}
public async initGlobalFeatureSet() {
const licenseServerKey = await getLicenseServerKey();
@@ -135,4 +150,4 @@ class EELicenseService {
}
}
export default new EELicenseService();
export default new EELicenseService();

View File

@@ -1,9 +1,9 @@
import { Types } from 'mongoose';
import { ISecretVersion } from '../models';
import {
takeSecretSnapshotHelper,
addSecretVersionsHelper,
markDeletedSecretVersionsHelper
import {
takeSecretSnapshotHelper,
addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
} from '../helpers/secret';
import EELicenseService from './EELicenseService';
@@ -11,58 +11,65 @@ import EELicenseService from './EELicenseService';
* Class to handle Enterprise Edition secret actions
*/
class EESecretService {
/**
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
* [workspaceId] under a new snapshot with incremented version under the
* SecretSnapshot collection.
* Requires a valid license key [licenseKey]
* @param {Object} obj
* @param {String} obj.workspaceId
* @returns {SecretSnapshot} secretSnapshot - new secret snpashot
*/
static async takeSecretSnapshot({
workspaceId
}: {
workspaceId: Types.ObjectId;
}) {
if (!EELicenseService.isLicenseValid) return;
return await takeSecretSnapshotHelper({ workspaceId });
}
/**
* Add secret versions [secretVersions] to the SecretVersion collection.
* @param {Object} obj
* @param {Object[]} obj.secretVersions
* @returns {SecretVersion[]} newSecretVersions - new secret versions
*/
static async addSecretVersions({
secretVersions
}: {
secretVersions: ISecretVersion[];
}) {
if (!EELicenseService.isLicenseValid) return;
return await addSecretVersionsHelper({
secretVersions
});
}
/**
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
* [workspaceId] under a new snapshot with incremented version under the
* SecretSnapshot collection.
* Requires a valid license key [licenseKey]
* @param {Object} obj
* @param {String} obj.workspaceId
* @returns {SecretSnapshot} secretSnapshot - new secret snpashot
*/
static async takeSecretSnapshot({
workspaceId,
environment,
folderId,
}: {
workspaceId: Types.ObjectId;
environment: string;
folderId?: string;
}) {
if (!EELicenseService.isLicenseValid) return;
return await takeSecretSnapshotHelper({
workspaceId,
environment,
folderId,
});
}
/**
* Mark secret versions associated with secrets with ids [secretIds]
* as deleted.
* @param {Object} obj
* @param {ObjectId[]} obj.secretIds - secret ids
*/
static async markDeletedSecretVersions({
secretIds
}: {
secretIds: Types.ObjectId[];
}) {
if (!EELicenseService.isLicenseValid) return;
await markDeletedSecretVersionsHelper({
secretIds
});
}
/**
* Add secret versions [secretVersions] to the SecretVersion collection.
* @param {Object} obj
* @param {Object[]} obj.secretVersions
* @returns {SecretVersion[]} newSecretVersions - new secret versions
*/
static async addSecretVersions({
secretVersions,
}: {
secretVersions: ISecretVersion[];
}) {
if (!EELicenseService.isLicenseValid) return;
return await addSecretVersionsHelper({
secretVersions,
});
}
/**
* Mark secret versions associated with secrets with ids [secretIds]
* as deleted.
* @param {Object} obj
* @param {ObjectId[]} obj.secretIds - secret ids
*/
static async markDeletedSecretVersions({
secretIds,
}: {
secretIds: Types.ObjectId[];
}) {
if (!EELicenseService.isLicenseValid) return;
await markDeletedSecretVersionsHelper({
secretIds,
});
}
}
export default EESecretService;
export default EESecretService;

View File

@@ -6,7 +6,9 @@ import {
User,
ServiceTokenData,
ServiceAccount,
APIKeyData
APIKeyData,
TokenVersion,
ITokenVersion
} from '../models';
import {
AccountNotFoundError,
@@ -19,6 +21,7 @@ import {
import {
getJwtAuthLifetime,
getJwtAuthSecret,
getJwtProviderAuthSecret,
getJwtRefreshLifetime,
getJwtRefreshSecret
} from '../config';
@@ -34,7 +37,7 @@ import {
* @param {Object} obj
* @param {Object} obj.headers - HTTP request headers object
*/
const validateAuthMode = ({
export const validateAuthMode = ({
headers,
acceptedAuthModes
}: {
@@ -43,7 +46,7 @@ const validateAuthMode = ({
}) => {
const apiKey = headers['x-api-key'];
const authHeader = headers['authorization'];
let authMode, authTokenValue;
if (apiKey === undefined && authHeader === undefined) {
// case: no auth or X-API-KEY header present
@@ -96,7 +99,7 @@ const validateAuthMode = ({
* @param {String} obj.authTokenValue - JWT token value
* @returns {User} user - user corresponding to JWT token
*/
const getAuthUserPayload = async ({
export const getAuthUserPayload = async ({
authTokenValue
}: {
authTokenValue: string;
@@ -106,14 +109,32 @@ const getAuthUserPayload = async ({
);
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
_id: new Types.ObjectId(decodedToken.userId)
}).select('+publicKey +accessVersion');
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
if (!user) throw AccountNotFoundError({ message: 'Failed to find user' });
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate user with partially set up account' });
return user;
const tokenVersion = await TokenVersion.findOneAndUpdate({
_id: new Types.ObjectId(decodedToken.tokenVersionId),
user: user._id
}, {
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'
});
return ({
user,
tokenVersionId: tokenVersion._id
});
}
/**
@@ -122,7 +143,7 @@ const getAuthUserPayload = async ({
* @param {String} obj.authTokenValue - service token value
* @returns {ServiceTokenData} serviceTokenData - service token data
*/
const getAuthSTDPayload = async ({
export const getAuthSTDPayload = async ({
authTokenValue
}: {
authTokenValue: string;
@@ -168,7 +189,7 @@ const getAuthSTDPayload = async ({
* @param {String} obj.authTokenValue - service account access token value
* @returns {ServiceAccount} serviceAccount
*/
const getAuthSAAKPayload = async ({
export const getAuthSAAKPayload = async ({
authTokenValue
}: {
authTokenValue: string;
@@ -197,7 +218,7 @@ const getAuthSAAKPayload = async ({
* @param {String} obj.authTokenValue - API key value
* @returns {APIKeyData} apiKeyData - API key data
*/
const getAuthAPIKeyPayload = async ({
export const getAuthAPIKeyPayload = async ({
authTokenValue
}: {
authTokenValue: string;
@@ -254,12 +275,43 @@ const getAuthAPIKeyPayload = async ({
* @return {String} obj.token - issued JWT token
* @return {String} obj.refreshToken - issued refresh token
*/
const issueAuthTokens = async ({ userId }: { userId: string }) => {
export const issueAuthTokens = async ({
userId,
ip,
userAgent
}: {
userId: Types.ObjectId;
ip: string;
userAgent: string;
}) => {
let tokenVersion: ITokenVersion | null;
// continue with (session) token version matching existing ip and user agent
tokenVersion = await TokenVersion.findOne({
user: userId,
ip,
userAgent
});
if (!tokenVersion) {
// case: no existing ip and user agent exists
// -> create new (session) token version for ip and user agent
tokenVersion = await new TokenVersion({
user: userId,
refreshVersion: 0,
accessVersion: 0,
ip,
userAgent,
lastUsed: new Date()
}).save();
}
// issue tokens
const token = createToken({
payload: {
userId
userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.accessVersion
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret()
@@ -267,7 +319,9 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
const refreshToken = createToken({
payload: {
userId
userId,
tokenVersionId: tokenVersion._id.toString(),
refreshVersion: tokenVersion.refreshVersion
},
expiresIn: await getJwtRefreshLifetime(),
secret: await getJwtRefreshSecret()
@@ -284,13 +338,15 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
* @param {Object} obj
* @param {String} obj.userId - id of user whose tokens are cleared.
*/
const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
export const clearTokens = async (tokenVersionId: Types.ObjectId): Promise<void> => {
// increment refreshVersion on user by 1
User.findOneAndUpdate({
_id: userId
await TokenVersion.findOneAndUpdate({
_id: tokenVersionId
}, {
$inc: {
refreshVersion: 1
refreshVersion: 1,
accessVersion: 1
}
});
};
@@ -303,7 +359,7 @@ const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
* @param {String} obj.secret - (JWT) secret such as [JWT_AUTH_SECRET]
* @param {String} obj.expiresIn - string describing time span such as '10h' or '7d'
*/
const createToken = ({
export const createToken = ({
payload,
expiresIn,
secret
@@ -317,13 +373,27 @@ const createToken = ({
});
};
export {
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
getAuthSAAKPayload,
getAuthAPIKeyPayload,
createToken,
issueAuthTokens,
clearTokens
};
export const validateProviderAuthToken = async ({
email,
user,
providerAuthToken,
}: {
email: string;
user: IUser,
providerAuthToken?: string;
}) => {
if (!providerAuthToken) {
throw new Error('Invalid authentication request.');
}
const decodedToken = <jwt.ProviderAuthJwtPayload>(
jwt.verify(providerAuthToken, await getJwtProviderAuthSecret())
);
if (
decodedToken.authProvider !== user.authProvider ||
decodedToken.email !== email
) {
throw new Error('Invalid authentication credentials.')
}
}

View File

@@ -31,7 +31,7 @@ import { InternalServerError } from "../utils/errors";
* @param {String} obj.name - name of bot
* @param {String} obj.workspaceId - id of workspace that bot belongs to
*/
const createBot = async ({
export const createBot = async ({
name,
workspaceId,
}: {
@@ -86,6 +86,18 @@ const createBot = async ({
});
};
/**
* Return whether or not workspace with id [workspaceId] is end-to-end encrypted
* @param {Types.ObjectId} workspaceId - id of workspace to check
*/
export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
const botKey = await BotKey.exists({
workspace: workspaceId
});
return botKey ? false : true;
}
/**
* Return decrypted secrets for workspace with id [workspaceId]
* and [environment] using bot
@@ -93,7 +105,7 @@ const createBot = async ({
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment
*/
const getSecretsHelper = async ({
export const getSecretsBotHelper = async ({
workspaceId,
environment,
}: {
@@ -101,7 +113,7 @@ const getSecretsHelper = async ({
environment: string;
}) => {
const content = {} as any;
const key = await getKey({ workspaceId: workspaceId.toString() });
const key = await getKey({ workspaceId: workspaceId });
const secrets = await Secret.find({
workspace: workspaceId,
environment,
@@ -136,7 +148,7 @@ const getSecretsHelper = async ({
* @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key
*/
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
@@ -194,14 +206,14 @@ const getKey = async ({ workspaceId }: { workspaceId: string }) => {
* @param {String} obj1.workspaceId - id of workspace
* @param {String} obj1.plaintext - plaintext to encrypt
*/
const encryptSymmetricHelper = async ({
export const encryptSymmetricHelper = async ({
workspaceId,
plaintext,
}: {
workspaceId: Types.ObjectId;
plaintext: string;
}) => {
const key = await getKey({ workspaceId: workspaceId.toString() });
const key = await getKey({ workspaceId: workspaceId });
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext,
key,
@@ -222,7 +234,7 @@ const encryptSymmetricHelper = async ({
* @param {String} obj.iv - iv
* @param {String} obj.tag - tag
*/
const decryptSymmetricHelper = async ({
export const decryptSymmetricHelper = async ({
workspaceId,
ciphertext,
iv,
@@ -233,7 +245,7 @@ const decryptSymmetricHelper = async ({
iv: string;
tag: string;
}) => {
const key = await getKey({ workspaceId: workspaceId.toString() });
const key = await getKey({ workspaceId: workspaceId });
const plaintext = decryptSymmetric128BitHexKeyUTF8({
ciphertext,
iv,
@@ -242,11 +254,4 @@ const decryptSymmetricHelper = async ({
});
return plaintext;
};
export {
createBot,
getSecretsHelper,
encryptSymmetricHelper,
decryptSymmetricHelper
};
};

View File

@@ -7,7 +7,7 @@ import { getLogger } from '../utils/logger';
* @param {String} obj.mongoURL - mongo connection string
* @returns
*/
const initDatabaseHelper = async ({
export const initDatabaseHelper = async ({
mongoURL
}: {
mongoURL: string;
@@ -30,7 +30,7 @@ const initDatabaseHelper = async ({
/**
* Close database conection
*/
const closeDatabaseHelper = async () => {
export const closeDatabaseHelper = async () => {
return Promise.all([
new Promise((resolve) => {
if (mongoose.connection && mongoose.connection.readyState == 1) {
@@ -41,9 +41,4 @@ const closeDatabaseHelper = async () => {
}
})
]);
}
export {
initDatabaseHelper,
closeDatabaseHelper
}

View File

@@ -18,7 +18,7 @@ interface Event {
* @param {String} obj.event.workspaceId - id of workspace that event is part of
* @param {Object} obj.event.payload - payload of event (depends on event)
*/
const handleEventHelper = async ({ event }: { event: Event }) => {
export const handleEventHelper = async ({ event }: { event: Event }) => {
const { workspaceId, environment } = event;
// TODO: moduralize bot check into separate function
@@ -37,6 +37,4 @@ const handleEventHelper = async ({ event }: { event: Event }) => {
});
break;
}
};
export { handleEventHelper };
};

View File

@@ -0,0 +1,17 @@
export * from './auth';
export * from './bot';
export * from './database';
export * from './event';
export * from './integration';
export * from './key';
export * from './membership';
export * from './membershipOrg';
export * from './nodemailer';
export * from './organization';
export * from './rateLimiter';
export * from './secret';
export * from './secrets';
export * from './signup';
export * from './token';
export * from './user';
export * from './workspace';

View File

@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Bot,
@@ -16,7 +15,6 @@ import {
import {
UnauthorizedRequestError,
} from '../utils/errors';
import RequestError from '../utils/requestError';
interface Update {
workspace: string;
@@ -37,7 +35,7 @@ interface Update {
* @param {String} obj.code - code
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/
const handleOAuthExchangeHelper = async ({
export const handleOAuthExchangeHelper = async ({
workspaceId,
integration,
code,
@@ -48,66 +46,59 @@ const handleOAuthExchangeHelper = async ({
code: string;
environment: string;
}) => {
let integrationAuth;
try {
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true
});
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
// exchange code for access and refresh tokens
const res = await exchangeCode({
integration,
code
});
const update: Update = {
workspace: workspaceId,
integration
}
switch (integration) {
case INTEGRATION_VERCEL:
update.teamId = res.teamId;
break;
case INTEGRATION_NETLIFY:
update.accountId = res.accountId;
break;
}
const integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: workspaceId,
integration
}, update, {
new: true,
upsert: true
});
if (res.refreshToken) {
// case: refresh token returned from exchange
// set integration auth refresh token
await setIntegrationAuthRefreshHelper({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: res.refreshToken
});
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
// exchange code for access and refresh tokens
const res = await exchangeCode({
integration,
code
}
if (res.accessToken) {
// case: access token returned from exchange
// set integration auth access token
await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt
});
const update: Update = {
workspace: workspaceId,
integration
}
switch (integration) {
case INTEGRATION_VERCEL:
update.teamId = res.teamId;
break;
case INTEGRATION_NETLIFY:
update.accountId = res.accountId;
break;
}
integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: workspaceId,
integration
}, update, {
new: true,
upsert: true
});
if (res.refreshToken) {
// case: refresh token returned from exchange
// set integration auth refresh token
await setIntegrationAuthRefreshHelper({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: res.refreshToken
});
}
if (res.accessToken) {
// case: access token returned from exchange
// set integration auth access token
await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to handle OAuth2 code-token exchange')
}
return integrationAuth;
@@ -118,54 +109,47 @@ const handleOAuthExchangeHelper = async ({
* @param {Object} obj
* @param {Object} obj.workspaceId - id of workspace
*/
const syncIntegrationsHelper = async ({
export const syncIntegrationsHelper = async ({
workspaceId,
environment
}: {
workspaceId: Types.ObjectId;
environment?: string;
}) => {
let integrations;
try {
integrations = await Integration.find({
workspace: workspaceId,
...(environment ? {
environment
} : {}),
isActive: true,
app: { $ne: null }
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
});
// 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
});
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
});
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
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to integrations');
// sync secrets to integration
await syncSecrets({
integration,
integrationAuth,
secrets,
accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken
});
}
}
@@ -177,31 +161,19 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} refreshToken - decrypted refresh token
*/
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let refreshToken;
try {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('+refreshCiphertext +refreshIV +refreshTag');
export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('+refreshCiphertext +refreshIV +refreshTag');
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
if(err instanceof RequestError)
throw err
else
throw new Error('Failed to get integration refresh token');
}
const refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string
});
return refreshToken;
}
@@ -214,53 +186,43 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token
*/
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let accessId;
let accessToken;
try {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string
});
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string
});
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
// there is a access token expiration date
// and refresh token to exchange with the OAuth2 server
if (integrationAuth.accessExpiresAt < new Date()) {
// access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
accessToken = await exchangeRefresh({
integrationAuth,
refreshToken
});
}
}
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
// there is a access token expiration date
// and refresh token to exchange with the OAuth2 server
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
accessId = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessIdCiphertext as string,
iv: integrationAuth.accessIdIV as string,
tag: integrationAuth.accessIdTag as string
if (integrationAuth.accessExpiresAt < new Date()) {
// access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
accessToken = await exchangeRefresh({
integrationAuth,
refreshToken
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
if(err instanceof RequestError)
throw err
else
throw new Error('Failed to get integration access token');
}
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
accessId = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessIdCiphertext as string,
iv: integrationAuth.accessIdIV as string,
tag: integrationAuth.accessIdTag as string
});
}
return ({
@@ -277,7 +239,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.refreshToken - refresh token
*/
const setIntegrationAuthRefreshHelper = async ({
export const setIntegrationAuthRefreshHelper = async ({
integrationAuthId,
refreshToken
}: {
@@ -285,34 +247,27 @@ const setIntegrationAuthRefreshHelper = async ({
refreshToken: string;
}) => {
let integrationAuth;
try {
integrationAuth = await IntegrationAuth
.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth');
const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: refreshToken
});
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv,
refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to set integration auth refresh token');
}
let integrationAuth = await IntegrationAuth
.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth');
const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: refreshToken
});
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv,
refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
return integrationAuth;
}
@@ -326,7 +281,7 @@ const setIntegrationAuthRefreshHelper = async ({
* @param {String} obj.accessToken - access token
* @param {Date} obj.accessExpiresAt - expiration date of access token
*/
const setIntegrationAuthAccessHelper = async ({
export const setIntegrationAuthAccessHelper = async ({
integrationAuthId,
accessId,
accessToken,
@@ -337,54 +292,38 @@ const setIntegrationAuthAccessHelper = async ({
accessToken: string;
accessExpiresAt: Date | undefined;
}) => {
let integrationAuth;
try {
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth');
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth');
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessId
});
}
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to save integration auth access token');
plaintext: accessId
});
}
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
return integrationAuth;
}
export {
handleOAuthExchangeHelper,
syncIntegrationsHelper,
getIntegrationAuthRefreshHelper,
getIntegrationAuthAccessHelper,
setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper
}
}

View File

@@ -17,7 +17,7 @@ interface Key {
* @param {String} obj.keys.nonce - nonce for encryption
* @param {String} obj.keys.userId - id of receiver user
*/
const pushKeys = async ({
export const pushKeys = async ({
userId,
workspaceId,
keys
@@ -50,6 +50,4 @@ const pushKeys = async ({
workspace: workspaceId
}))
);
};
export { pushKeys };
};

View File

@@ -1,13 +1,6 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Membership,
Key
} from '../models';
import {
MembershipNotFoundError,
BadRequestError
} from '../utils/errors';
import { Types } from "mongoose";
import { Membership, Key } from "../models";
import { MembershipNotFoundError, BadRequestError } from "../utils/errors";
/**
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
@@ -17,50 +10,45 @@ import {
* @param {String} obj.workspaceId - id of workspace
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
*/
const validateMembership = async ({
export const validateMembership = async ({
userId,
workspaceId,
acceptedRoles,
}: {
userId: Types.ObjectId;
workspaceId: Types.ObjectId;
acceptedRoles?: Array<'admin' | 'member'>;
userId: Types.ObjectId | string;
workspaceId: Types.ObjectId | string;
acceptedRoles?: Array<"admin" | "member">;
}) => {
const membership = await Membership.findOne({
user: userId,
workspace: workspaceId
}).populate("workspace");
if (!membership) {
throw MembershipNotFoundError({ message: 'Failed to find workspace membership' });
}
if (acceptedRoles) {
if (!acceptedRoles.includes(membership.role)) {
throw BadRequestError({ message: 'Failed authorization for membership role' });
}
}
return membership;
}
const membership = await Membership.findOne({
user: userId,
workspace: workspaceId,
}).populate("workspace");
if (!membership) {
throw MembershipNotFoundError({
message: "Failed to find workspace membership",
});
}
if (acceptedRoles) {
if (!acceptedRoles.includes(membership.role)) {
throw BadRequestError({
message: "Failed authorization for membership role",
});
}
}
return membership;
};
/**
* Return membership matching criteria specified in query [queryObj]
* @param {Object} queryObj - query object
* @return {Object} membership - membership
*/
const findMembership = async (queryObj: any) => {
let membership;
try {
membership = await Membership.findOne(queryObj);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to find membership');
}
return membership;
export const findMembership = async (queryObj: any) => {
const membership = await Membership.findOne(queryObj);
return membership;
};
/**
@@ -71,40 +59,33 @@ const findMembership = async (queryObj: any) => {
* @param {String} obj.workspaceId - id of workspace.
* @param {String[]} obj.roles - roles of users.
*/
const addMemberships = async ({
export const addMemberships = async ({
userIds,
workspaceId,
roles
}: {
userIds: string[];
workspaceId: string;
roles: string[];
userIds: string[];
workspaceId: string;
roles: string[];
}): Promise<void> => {
try {
const operations = userIds.map((userId, idx) => {
return {
updateOne: {
filter: {
user: userId,
workspace: workspaceId,
role: roles[idx]
},
update: {
user: userId,
workspace: workspaceId,
role: roles[idx]
},
upsert: true
}
};
});
await Membership.bulkWrite(operations as any);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to add users to workspace');
}
const operations = userIds.map((userId, idx) => {
return {
updateOne: {
filter: {
user: userId,
workspace: workspaceId,
role: roles[idx],
},
update: {
user: userId,
workspace: workspaceId,
role: roles[idx],
},
upsert: true,
},
};
});
await Membership.bulkWrite(operations as any);
};
/**
@@ -112,33 +93,19 @@ const addMemberships = async ({
* @param {Object} obj
* @param {String} obj.membershipId - id of membership to delete
*/
const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
let deletedMembership;
try {
deletedMembership = await Membership.findOneAndDelete({
_id: membershipId
});
export const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
const deletedMembership = await Membership.findOneAndDelete({
_id: membershipId
});
// delete keys associated with the membership
if (deletedMembership?.user) {
// case: membership had a registered user
await Key.deleteMany({
receiver: deletedMembership.user,
workspace: deletedMembership.workspace
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete membership');
}
// delete keys associated with the membership
if (deletedMembership?.user) {
// case: membership had a registered user
await Key.deleteMany({
receiver: deletedMembership.user,
workspace: deletedMembership.workspace,
});
}
return deletedMembership;
};
export {
validateMembership,
addMemberships,
findMembership,
deleteMembership
};

View File

@@ -18,7 +18,7 @@ import {
* @param {Types.ObjectId} obj.organizationId
* @param {String[]} obj.acceptedRoles
*/
const validateMembershipOrg = async ({
export const validateMembershipOrg = async ({
userId,
organizationId,
acceptedRoles,
@@ -59,7 +59,7 @@ const validateMembershipOrg = async ({
* @param {Object} queryObj - query object
* @return {Object} membershipOrg - membership
*/
const findMembershipOrg = (queryObj: any) => {
export const findMembershipOrg = (queryObj: any) => {
const membershipOrg = MembershipOrg.findOne(queryObj);
return membershipOrg;
};
@@ -72,7 +72,7 @@ const findMembershipOrg = (queryObj: any) => {
* @param {String} obj.organizationId - id of organization.
* @param {String[]} obj.roles - roles of users.
*/
const addMembershipsOrg = async ({
export const addMembershipsOrg = async ({
userIds,
organizationId,
roles,
@@ -111,7 +111,7 @@ const addMembershipsOrg = async ({
* @param {Object} obj
* @param {String} obj.membershipOrgId - id of organization membership to delete
*/
const deleteMembershipOrg = async ({
export const deleteMembershipOrg = async ({
membershipOrgId
}: {
membershipOrgId: string;
@@ -148,11 +148,4 @@ const deleteMembershipOrg = async ({
}
return deletedMembershipOrg;
};
export {
validateMembershipOrg,
findMembershipOrg,
addMembershipsOrg,
deleteMembershipOrg
};
};

View File

@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import fs from 'fs';
import path from 'path';
import handlebars from 'handlebars';
@@ -14,7 +13,7 @@ let smtpTransporter: nodemailer.Transporter;
* @param {String[]} obj.recipients - email addresses of people to send email to
* @param {Object} obj.substitutions - object containing template substitutions
*/
const sendMail = async ({
export const sendMail = async ({
template,
subjectLine,
recipients,
@@ -26,29 +25,22 @@ const sendMail = async ({
substitutions: any;
}) => {
if (await getSmtpConfigured()) {
try {
const html = fs.readFileSync(
path.resolve(__dirname, '../templates/' + template),
'utf8'
);
const temp = handlebars.compile(html);
const htmlToSend = temp(substitutions);
const html = fs.readFileSync(
path.resolve(__dirname, '../templates/' + template),
'utf8'
);
const temp = handlebars.compile(html);
const htmlToSend = temp(substitutions);
await smtpTransporter.sendMail({
from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`,
to: recipients.join(', '),
subject: subjectLine,
html: htmlToSend
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
await smtpTransporter.sendMail({
from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`,
to: recipients.join(', '),
subject: subjectLine,
html: htmlToSend
});
}
};
const setTransporter = (transporter: nodemailer.Transporter) => {
export const setTransporter = (transporter: nodemailer.Transporter) => {
smtpTransporter = transporter;
};
export { sendMail, setTransporter };
};

View File

@@ -28,7 +28,7 @@ import {
* @param {String} obj.email - POC email that will receive invoice info
* @param {Object} organization - new organization
*/
const createOrganization = async ({
export const createOrganization = async ({
name,
email,
}: {
@@ -70,7 +70,7 @@ const createOrganization = async ({
* @return {Object} obj.stripeSubscription - new stripe subscription
* @return {Subscription} obj.subscription - new subscription
*/
const initSubscriptionOrg = async ({
export const initSubscriptionOrg = async ({
organizationId,
}: {
organizationId: Types.ObjectId;
@@ -125,7 +125,7 @@ const initSubscriptionOrg = async ({
* @param {Object} obj
* @param {Number} obj.organizationId - id of subscription's organization
*/
const updateSubscriptionOrgQuantity = async ({
export const updateSubscriptionOrgQuantity = async ({
organizationId,
}: {
organizationId: string;
@@ -153,28 +153,24 @@ const updateSubscriptionOrgQuantity = async ({
EELicenseService.localFeatureSet.del(organizationId);
}
if (EELicenseService.instanceType === 'enterprise-self-hosted') {
// instance of Infisical is an enterprise self-hosted instance
const usedSeats = await MembershipOrg.countDocuments({
status: ACCEPTED
});
await licenseKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license/v1/license`,
{
usedSeats
}
);
}
}
return stripeSubscription;
};
if (EELicenseService.instanceType === 'enterprise-self-hosted') {
// instance of Infisical is an enterprise self-hosted instance
const usedSeats = await MembershipOrg.countDocuments({
status: ACCEPTED
});
export {
createOrganization,
initSubscriptionOrg,
updateSubscriptionOrgQuantity
};
await licenseKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license/v1/license`,
{
usedSeats
}
);
}
await EELicenseService.refreshPlan(organizationId);
return stripeSubscription;
};

View File

@@ -1,7 +1,14 @@
import rateLimit from 'express-rate-limit';
// const MongoStore = require('rate-limit-mongo');
// 120 requests per minute
const apiLimiter = rateLimit({
// 200 per minute
export const apiLimiter = rateLimit({
// store: new MongoStore({
// uri: process.env.MONGO_URL,
// expireTimeMs: 1000 * 60,
// collectionName: "expressRateRecords-apiLimiter",
// errorHandler: console.error.bind(null, 'rate-limit-mongo')
// }),
windowMs: 60 * 1000,
max: 240,
standardHeaders: true,
@@ -10,42 +17,48 @@ const apiLimiter = rateLimit({
return request.path === '/healthcheck' || request.path === '/api/status'
},
keyGenerator: (req, res) => {
return req.clientIp
return req.realIP
}
});
// 10 requests per minute
// 50 requests per 1 hours
const authLimit = rateLimit({
// store: new MongoStore({
// uri: process.env.MONGO_URL,
// expireTimeMs: 1000 * 60 * 60,
// errorHandler: console.error.bind(null, 'rate-limit-mongo'),
// collectionName: "expressRateRecords-authLimit",
// }),
windowMs: 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => {
return req.clientIp
return req.realIP
}
});
// 10 requests per hour
const passwordLimiter = rateLimit({
// 5 requests per 1 hour
export const passwordLimiter = rateLimit({
// store: new MongoStore({
// uri: process.env.MONGO_URL,
// expireTimeMs: 1000 * 60 * 60,
// errorHandler: console.error.bind(null, 'rate-limit-mongo'),
// collectionName: "expressRateRecords-passwordLimiter",
// }),
windowMs: 60 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => {
return req.clientIp
return req.realIP
}
});
const authLimiter = (req: any, res: any, next: any) => {
export const authLimiter = (req: any, res: any, next: any) => {
if (process.env.NODE_ENV === 'production') {
authLimit(req, res, next);
} else {
next();
}
};
export {
apiLimiter,
authLimiter,
passwordLimiter
};
};

View File

@@ -1,5 +1,5 @@
import { Types } from "mongoose";
import { Secret, ISecret, Membership } from "../models";
import { Secret, ISecret } from "../models";
import { EESecretService, EELogService } from "../ee/services";
import { IAction, SecretVersion } from "../ee/models";
import {
@@ -12,8 +12,6 @@ import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
} from "../variables";
import _ from "lodash";
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
interface V1PushSecret {
ciphertextKey: string;
@@ -62,7 +60,7 @@ interface Update {
* @param {String} obj.environment - environment for secrets
* @param {Object[]} obj.secrets - secrets to push
*/
const v1PushSecrets = async ({
export const v1PushSecrets = async ({
userId,
workspaceId,
environment,
@@ -197,7 +195,7 @@ const v1PushSecrets = async ({
secretValueTag: newSecret.tagValue,
secretValueHash: newSecret.hashValue,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
keyEncoding: ENCODING_SCHEME_UTF8,
});
}),
});
@@ -230,7 +228,7 @@ const v1PushSecrets = async ({
secretCommentTag: s.tagComment,
secretCommentHash: s.hashComment,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
keyEncoding: ENCODING_SCHEME_UTF8,
};
if (toAdd[idx].type === "personal") {
@@ -261,7 +259,7 @@ const v1PushSecrets = async ({
secretValueTag,
secretValueHash,
algorithm,
keyEncoding
keyEncoding,
}) =>
new SecretVersion({
secret: _id,
@@ -280,7 +278,7 @@ const v1PushSecrets = async ({
secretValueTag,
secretValueHash,
algorithm,
keyEncoding
keyEncoding,
})
),
});
@@ -289,6 +287,7 @@ const v1PushSecrets = async ({
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
});
};
@@ -305,7 +304,7 @@ const v1PushSecrets = async ({
* @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets
*/
const v2PushSecrets = async ({
export const v2PushSecrets = async ({
userId,
workspaceId,
environment,
@@ -491,7 +490,7 @@ const v2PushSecrets = async ({
secret: secretDocument._id,
isDeleted: false,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
keyEncoding: ENCODING_SCHEME_UTF8,
});
}),
});
@@ -508,6 +507,7 @@ const v2PushSecrets = async ({
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
});
// (EE) create (audit) log
@@ -530,7 +530,7 @@ const v2PushSecrets = async ({
* @param {String} obj.workspaceId - id of workspace to pull from
* @param {String} obj.environment - environment for secrets
*/
const getSecrets = async ({
export const getSecrets = async ({
userId,
workspaceId,
environment,
@@ -570,7 +570,7 @@ const getSecrets = async ({
* @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets
*/
const pullSecrets = async ({
export const pullSecrets = async ({
userId,
workspaceId,
environment,
@@ -614,7 +614,7 @@ const pullSecrets = async ({
* @param {Object} obj
* @param {Object} obj.secrets
*/
const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
export const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
const reformatedSecrets = secrets.map((s) => ({
_id: s._id,
workspace: s.workspace,
@@ -644,6 +644,4 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
}));
return reformatedSecrets;
};
export { v1PushSecrets, v2PushSecrets, pullSecrets, reformatPullSecrets };
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { IUser } from '../models';
import { createOrganization } from './organization';
import { addMembershipsOrg } from './membershipOrg';
@@ -14,29 +13,21 @@ import { TOKEN_EMAIL_CONFIRMATION } from '../variables';
* @param {String} obj.email - email
* @returns {Boolean} success - whether or not operation was successful
*/
const sendEmailVerification = async ({ email }: { email: string }) => {
try {
const token = await TokenService.createToken({
type: TOKEN_EMAIL_CONFIRMATION,
email
});
export const sendEmailVerification = async ({ email }: { email: string }) => {
const token = await TokenService.createToken({
type: TOKEN_EMAIL_CONFIRMATION,
email
});
// send mail
await sendMail({
template: 'emailVerification.handlebars',
subjectLine: 'Infisical confirmation code',
recipients: [email],
substitutions: {
code: token
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(
"Ouch. We weren't able to send your email verification code"
);
}
// send mail
await sendMail({
template: 'emailVerification.handlebars',
subjectLine: 'Infisical confirmation code',
recipients: [email],
substitutions: {
code: token
}
});
};
/**
@@ -45,24 +36,18 @@ const sendEmailVerification = async ({ email }: { email: string }) => {
* @param {String} obj.email - emai
* @param {String} obj.code - code that was sent to [email]
*/
const checkEmailVerification = async ({
export const checkEmailVerification = async ({
email,
code
}: {
email: string;
code: string;
}) => {
try {
await TokenService.validateToken({
type: TOKEN_EMAIL_CONFIRMATION,
email,
token: code
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Oops. We weren't able to verify");
}
await TokenService.validateToken({
type: TOKEN_EMAIL_CONFIRMATION,
email,
token: code
});
};
/**
@@ -72,7 +57,7 @@ const checkEmailVerification = async ({
* @param {String} obj.organizationName - name of organization to initialize
* @param {IUser} obj.user - user who we are initializing for
*/
const initializeDefaultOrg = async ({
export const initializeDefaultOrg = async ({
organizationName,
user
}: {
@@ -96,6 +81,4 @@ const initializeDefaultOrg = async ({
} catch (err) {
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
}
};
export { sendEmailVerification, checkEmailVerification, initializeDefaultOrg };
};

View File

@@ -20,7 +20,7 @@ import { getSaltRounds } from "../config";
* @param {Types.ObjectId} obj.organizationId
* @returns {String} token - the created token
*/
const createTokenHelper = async ({
export const createTokenHelper = async ({
type,
email,
phoneNumber,
@@ -121,7 +121,7 @@ const createTokenHelper = async ({
* @param {String} obj.email - email associated with the token
* @param {String} obj.token - value of the token
*/
const validateTokenHelper = async ({
export const validateTokenHelper = async ({
type,
email,
phoneNumber,
@@ -212,6 +212,4 @@ const validateTokenHelper = async ({
// case: token is valid
await TokenData.findByIdAndDelete(tokenData._id);
};
export { createTokenHelper, validateTokenHelper };
};

View File

@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import {
Workspace,
Bot,
@@ -7,6 +6,7 @@ import {
Secret
} from '../models';
import { createBot } from '../helpers/bot';
import { EELicenseService } from '../ee/services';
import { SecretService } from '../services';
/**
@@ -16,38 +16,32 @@ import { SecretService } from '../services';
* @param {String} organizationId - id of organization to create workspace in
* @param {Object} workspace - new workspace
*/
const createWorkspace = async ({
export const createWorkspace = async ({
name,
organizationId
}: {
name: string;
organizationId: string;
}) => {
let workspace;
try {
// create workspace
workspace = await new Workspace({
name,
organization: organizationId,
autoCapitalization: true
}).save();
// initialize bot for workspace
await createBot({
name: 'Infisical Bot',
workspaceId: workspace._id
});
// initialize blind index salt for workspace
await SecretService.createSecretBlindIndexData({
workspaceId: workspace._id
});
// create workspace
const workspace = await new Workspace({
name,
organization: organizationId,
autoCapitalization: true
}).save();
// initialize bot for workspace
await createBot({
name: 'Infisical Bot',
workspaceId: workspace._id
});
// initialize blind index salt for workspace
await SecretService.createSecretBlindIndexData({
workspaceId: workspace._id
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create workspace');
}
await EELicenseService.refreshPlan(organizationId);
return workspace;
};
@@ -58,29 +52,18 @@ const createWorkspace = async ({
* @param {Object} obj
* @param {String} obj.id - id of workspace to delete
*/
const deleteWorkspace = async ({ id }: { id: string }) => {
try {
await Workspace.deleteOne({ _id: id });
await Bot.deleteOne({
workspace: id
});
await Membership.deleteMany({
workspace: id
});
await Secret.deleteMany({
workspace: id
});
await Key.deleteMany({
workspace: id
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete workspace');
}
};
export {
createWorkspace,
deleteWorkspace
export const deleteWorkspace = async ({ id }: { id: string }) => {
await Workspace.deleteOne({ _id: id });
await Bot.deleteOne({
workspace: id
});
await Membership.deleteMany({
workspace: id
});
await Secret.deleteMany({
workspace: id
});
await Key.deleteMany({
workspace: id
});
};

View File

@@ -1,175 +1,185 @@
import dotenv from 'dotenv';
import dotenv from "dotenv";
dotenv.config();
import express from 'express';
import express from "express";
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('express-async-errors');
import helmet from 'helmet';
import cors from 'cors';
import { DatabaseService } from './services';
import { EELicenseService } from './ee/services';
import { setUpHealthEndpoint } from './services/health';
import cookieParser from 'cookie-parser';
import swaggerUi = require('swagger-ui-express');
import helmet from "helmet";
import cors from "cors";
import { DatabaseService } from "./services";
import { EELicenseService } from "./ee/services";
import { setUpHealthEndpoint } from "./services/health";
import cookieParser from "cookie-parser";
import swaggerUi = require("swagger-ui-express");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require('../spec.json');
const swaggerFile = require("../spec.json");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const requestIp = require('request-ip');
import { apiLimiter } from './helpers/rateLimiter';
import { apiLimiter } from "./helpers/rateLimiter";
import {
workspace as eeWorkspaceRouter,
secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
action as eeActionRouter,
organizations as eeOrganizationsRouter,
cloudProducts as eeCloudProductsRouter
} from './ee/routes/v1';
workspace as eeWorkspaceRouter,
secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
action as eeActionRouter,
organizations as eeOrganizationsRouter,
cloudProducts as eeCloudProductsRouter,
} from "./ee/routes/v1";
import {
signup as v1SignupRouter,
auth as v1AuthRouter,
bot as v1BotRouter,
organization as v1OrganizationRouter,
workspace as v1WorkspaceRouter,
membershipOrg as v1MembershipOrgRouter,
membership as v1MembershipRouter,
key as v1KeyRouter,
inviteOrg as v1InviteOrgRouter,
user as v1UserRouter,
userAction as v1UserActionRouter,
secret as v1SecretRouter,
serviceToken as v1ServiceTokenRouter,
password as v1PasswordRouter,
stripe as v1StripeRouter,
integration as v1IntegrationRouter,
integrationAuth as v1IntegrationAuthRouter,
secretsFolder as v1SecretsFolder
} from './routes/v1';
signup as v1SignupRouter,
auth as v1AuthRouter,
bot as v1BotRouter,
organization as v1OrganizationRouter,
workspace as v1WorkspaceRouter,
membershipOrg as v1MembershipOrgRouter,
membership as v1MembershipRouter,
key as v1KeyRouter,
inviteOrg as v1InviteOrgRouter,
user as v1UserRouter,
userAction as v1UserActionRouter,
secret as v1SecretRouter,
serviceToken as v1ServiceTokenRouter,
password as v1PasswordRouter,
stripe as v1StripeRouter,
integration as v1IntegrationRouter,
integrationAuth as v1IntegrationAuthRouter,
secretsFolder as v1SecretsFolder,
} from "./routes/v1";
import {
signup as v2SignupRouter,
auth as v2AuthRouter,
users as v2UsersRouter,
organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
serviceAccounts as v2ServiceAccountsRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from './routes/v2';
signup as v2SignupRouter,
auth as v2AuthRouter,
users as v2UsersRouter,
organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
serviceAccounts as v2ServiceAccountsRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from "./routes/v2";
import {
secrets as v3SecretsRouter,
workspaces as v3WorkspacesRouter
} from './routes/v3';
import { healthCheck } from './routes/status';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
import { requestErrorHandler } from './middleware/requestErrorHandler';
import {
getNodeEnv,
getPort,
getSiteURL
} from './config';
import { setup } from './utils/setup';
auth as v3AuthRouter,
secrets as v3SecretsRouter,
signup as v3SignupRouter,
workspaces as v3WorkspacesRouter,
} from "./routes/v3";
import { healthCheck } from "./routes/status";
import { getLogger } from "./utils/logger";
import { RouteNotFoundError } from "./utils/errors";
import { requestErrorHandler } from "./middleware/requestErrorHandler";
import { getNodeEnv, getPort, getSiteURL } from "./config";
import { setup } from "./utils/setup";
const main = async () => {
await setup();
await setup();
await EELicenseService.initGlobalFeatureSet();
await EELicenseService.initGlobalFeatureSet();
const app = express();
app.enable('trust proxy');
app.use(express.json());
app.use(cookieParser());
app.use(
cors({
credentials: true,
origin: await getSiteURL()
})
const app = express();
app.enable("trust proxy");
app.use(express.json());
app.use(cookieParser());
app.use(
cors({
credentials: true,
origin: await getSiteURL(),
})
);
if ((await getNodeEnv()) === "production") {
// enable app-wide rate-limiting + helmet security
// in production
app.disable("x-powered-by");
app.use(apiLimiter);
app.use(helmet());
}
app.use((req, res, next) => {
// default to IP address provided by Cloudflare
const cfIp = req.headers['cf-connecting-ip'];
req.realIP = Array.isArray(cfIp) ? cfIp[0] : (cfIp as string) || req.ip;
next();
});
// (EE) routes
app.use("/api/v1/secret", eeSecretRouter);
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
app.use("/api/v1/workspace", eeWorkspaceRouter);
app.use("/api/v1/action", eeActionRouter);
app.use("/api/v1/organizations", eeOrganizationsRouter);
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
// v1 routes (default)
app.use("/api/v1/signup", v1SignupRouter);
app.use("/api/v1/auth", v1AuthRouter);
app.use("/api/v1/bot", v1BotRouter);
app.use("/api/v1/user", v1UserRouter);
app.use("/api/v1/user-action", v1UserActionRouter);
app.use("/api/v1/organization", v1OrganizationRouter);
app.use("/api/v1/workspace", v1WorkspaceRouter);
app.use("/api/v1/membership-org", v1MembershipOrgRouter);
app.use("/api/v1/membership", v1MembershipRouter);
app.use("/api/v1/key", v1KeyRouter);
app.use("/api/v1/invite-org", v1InviteOrgRouter);
app.use("/api/v1/secret", v1SecretRouter); // deprecate
app.use("/api/v1/service-token", v1ServiceTokenRouter); // deprecate
app.use("/api/v1/password", v1PasswordRouter);
app.use("/api/v1/stripe", v1StripeRouter);
app.use("/api/v1/integration", v1IntegrationRouter);
app.use("/api/v1/integration-auth", v1IntegrationAuthRouter);
app.use("/api/v1/folders", v1SecretsFolder);
// v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter);
app.use("/api/v2/auth", v2AuthRouter);
app.use("/api/v2/users", v2UsersRouter);
app.use("/api/v2/organizations", v2OrganizationsRouter);
app.use("/api/v2/workspace", v2EnvironmentRouter);
app.use("/api/v2/workspace", v2TagsRouter);
app.use("/api/v2/workspace", v2WorkspaceRouter);
app.use("/api/v2/secret", v2SecretRouter); // deprecate
app.use("/api/v2/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets
app.use("/api/v2/service-token", v2ServiceTokenDataRouter);
app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
app.use("/api/v2/api-key", v2APIKeyDataRouter);
// v3 routes (experimental)
app.use("/api/v3/auth", v3AuthRouter);
app.use("/api/v3/secrets", v3SecretsRouter);
app.use("/api/v3/workspaces", v3WorkspacesRouter);
app.use("/api/v3/signup", v3SignupRouter);
// api docs
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile));
// server status
app.use("/api", healthCheck);
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next) => {
if (res.headersSent) return next();
next(
RouteNotFoundError({
message: `The requested source '(${req.method})${req.url}' was not found`,
})
);
});
app.use(requestIp.mw());
app.use(requestErrorHandler);
if ((await getNodeEnv()) === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
app.disable('x-powered-by');
app.use(apiLimiter);
app.use(helmet());
}
const server = app.listen(await getPort(), async () => {
(await getLogger("backend-main")).info(
`Server started listening at port ${await getPort()}`
);
});
// (EE) routes
app.use('/api/v1/secret', eeSecretRouter);
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
app.use('/api/v1/workspace', eeWorkspaceRouter);
app.use('/api/v1/action', eeActionRouter);
app.use('/api/v1/organizations', eeOrganizationsRouter);
app.use('/api/v1/cloud-products', eeCloudProductsRouter);
// await createTestUserForDevelopment();
setUpHealthEndpoint(server);
// v1 routes (default)
app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/bot', v1BotRouter);
app.use('/api/v1/user', v1UserRouter);
app.use('/api/v1/user-action', v1UserActionRouter);
app.use('/api/v1/organization', v1OrganizationRouter);
app.use('/api/v1/workspace', v1WorkspaceRouter);
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter); // deprecate
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
app.use('/api/v1/folder', v1SecretsFolder)
server.on("close", async () => {
await DatabaseService.closeDatabase();
});
// v2 routes (improvements)
app.use('/api/v2/signup', v2SignupRouter);
app.use('/api/v2/auth', v2AuthRouter);
app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecate
app.use('/api/v2/secrets', v2SecretsRouter); // note: in the process of moving to v3/secrets
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new
app.use('/api/v2/api-key', v2APIKeyDataRouter);
return server;
};
// v3 routes (experimental)
app.use('/api/v3/secrets', v3SecretsRouter);
app.use('/api/v3/workspaces', v3WorkspacesRouter);
// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
// server status
app.use('/api', healthCheck)
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next) => {
if (res.headersSent) return next();
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
})
app.use(requestErrorHandler)
const server = app.listen(await getPort(), async () => {
(await getLogger("backend-main")).info(`Server started listening at port ${await getPort()}`)
});
// await createTestUserForDevelopment();
setUpHealthEndpoint(server);
server.on('close', async () => {
await DatabaseService.closeDatabase();
})
return server;
}
export default main();
export default main();

View File

@@ -16,6 +16,7 @@ import {
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL,
@@ -26,6 +27,7 @@ import {
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL,
INTEGRATION_CHECKLY_API_URL
} from "../variables";
interface App {
@@ -120,6 +122,11 @@ const getApps = async ({
accessToken,
});
break;
case INTEGRATION_CHECKLY:
apps = await getAppsCheckly({
accessToken,
});
break;
}
return apps;
@@ -601,4 +608,32 @@ const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => {
return apps;
};
/**
* Return list of projects for the Checkly integration
* @param {Object} obj
* @param {String} obj.accessToken - api key for the Checkly API
* @returns {Object[]} apps - Сheckly accounts
* @returns {String} apps.name - name of Checkly account
*/
const getAppsCheckly = async ({ accessToken }: { accessToken: string }) => {
const { data } = await standardRequest.get(
`${INTEGRATION_CHECKLY_API_URL}/v1/accounts`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
const apps = data.map((a: any) => {
return {
name: a.name,
appId: a.id,
};
});
return apps;
};
export { getApps };

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
import { Types } from 'mongoose';
import {
IUser,
IServiceAccount,
@@ -10,4 +11,5 @@ export interface AuthData {
authChannel: string;
authIP: string;
authUserAgent: string;
tokenVersionId?: Types.ObjectId;
}

View File

@@ -1,52 +1,57 @@
import { Types } from 'mongoose';
import { AuthData } from '../../middleware';
import { Types } from "mongoose";
import { AuthData } from "../../middleware";
export interface CreateSecretParams {
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type: 'shared' | 'personal';
authData: AuthData;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type: "shared" | "personal";
authData: AuthData;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretPath: string;
}
export interface GetSecretsParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
workspaceId: Types.ObjectId;
environment: string;
secretPath: string;
authData: AuthData;
}
export interface GetSecretParams {
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type?: 'shared' | 'personal';
authData: AuthData;
secretName: string;
workspaceId: Types.ObjectId;
secretPath: string;
environment: string;
type?: "shared" | "personal";
authData: AuthData;
}
export interface UpdateSecretParams {
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type: 'shared' | 'personal',
authData: AuthData
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type: "shared" | "personal";
authData: AuthData;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretPath: string;
}
export interface DeleteSecretParams {
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type: 'shared' | 'personal';
authData: AuthData;
}
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type: "shared" | "personal";
authData: AuthData;
secretPath: string;
}

View File

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

View File

@@ -42,10 +42,9 @@ const requireAuth = ({
acceptedAuthModes: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// validate auth token against accepted auth modes [acceptedAuthModes]
// and return token type [authTokenType] and value [authTokenValue]
const { authMode, authTokenValue } = validateAuthMode({
headers: req.headers,
acceptedAuthModes
@@ -72,27 +71,30 @@ const requireAuth = ({
req.user = authPayload;
break;
default:
authPayload = await getAuthUserPayload({
const { user, tokenVersionId } = await getAuthUserPayload({
authTokenValue
});
req.user = authPayload;
authPayload = user;
req.user = user;
req.tokenVersionId = tokenVersionId;
break;
}
req.requestData = {
...req.params,
...req.query,
...req.body,
}
req.authData = {
authMode,
authPayload, // User, ServiceAccount, ServiceTokenData
authChannel: getChannelFromUserAgent(req.headers['user-agent']),
authIP: req.ip,
authUserAgent: req.headers['user-agent'] ?? 'other'
authIP: req.realIP,
authUserAgent: req.headers['user-agent'] ?? 'other',
tokenVersionId: req.tokenVersionId
}
return next();
}
}

View File

@@ -16,13 +16,15 @@ const requireWorkspaceAuth = ({
locationWorkspaceId,
locationEnvironment = undefined,
requiredPermissions = [],
requireBlindIndicesEnabled = false
requireBlindIndicesEnabled = false,
requireE2EEOff = false
}: {
acceptedRoles: Array<'admin' | 'member'>;
locationWorkspaceId: req;
locationEnvironment?: req | undefined;
requiredPermissions?: string[];
requireBlindIndicesEnabled?: boolean;
requireE2EEOff?: boolean;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const workspaceId = req[locationWorkspaceId]?.workspaceId;
@@ -35,7 +37,8 @@ const requireWorkspaceAuth = ({
environment,
acceptedRoles,
requiredPermissions,
requireBlindIndicesEnabled
requireBlindIndicesEnabled,
requireE2EEOff
});
if (membership) {

View File

@@ -1,36 +1,56 @@
import { Schema, Types, model } from 'mongoose';
import { Schema, model, Types } from "mongoose";
const folderSchema = new Schema({
export type TFolderRootSchema = {
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
nodes: TFolderSchema;
};
export type TFolderSchema = {
id: string;
name: string;
version: number;
children: TFolderSchema[];
};
const folderSchema = new Schema<TFolderSchema>({
id: {
required: true,
type: String,
},
version: {
required: true,
type: Number,
default: 1,
},
name: {
type: String,
required: true,
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true,
},
environment: {
type: String,
required: true,
default: "root",
},
parent: {
type: Schema.Types.ObjectId,
ref: 'Folder',
required: false, // optional for root folders
},
path: {
type: String,
required: true
},
parentPath: {
type: String,
required: true,
},
}, {
timestamps: true
});
const Folder = model('Folder', folderSchema);
folderSchema.add({ children: [folderSchema] });
export default Folder;
const folderRootSchema = new Schema<TFolderRootSchema>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
},
environment: {
type: String,
required: true,
},
nodes: folderSchema,
},
{
timestamps: true,
}
);
const Folder = model<TFolderRootSchema>("Folder", folderRootSchema);
export default Folder;

View File

@@ -16,14 +16,16 @@ import ServiceAccountKey, { IServiceAccountKey } from './serviceAccountKey'; //
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from './serviceAccountOrganizationPermission'; // new
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from './serviceAccountWorkspacePermission'; // new
import TokenData, { ITokenData } from './tokenData';
import User, { IUser } from './user';
import User,{ AuthProvider, IUser } from './user';
import UserAction, { IUserAction } from './userAction';
import Workspace, { IWorkspace } from './workspace';
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
import APIKeyData, { IAPIKeyData } from './apiKeyData';
import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail';
import TokenVersion, { ITokenVersion } from './tokenVersion';
export {
AuthProvider,
BackupPrivateKey,
IBackupPrivateKey,
Bot,
@@ -71,5 +73,7 @@ export {
APIKeyData,
IAPIKeyData,
LoginSRPDetail,
ILoginSRPDetail
ILoginSRPDetail,
TokenVersion,
ITokenVersion
};

View File

@@ -13,7 +13,9 @@ import {
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT
} from "../variables";
export interface IIntegration {
@@ -45,7 +47,9 @@ export interface IIntegration {
| 'flyio'
| 'circleci'
| 'travisci'
| 'supabase';
| 'supabase'
| 'checkly'
| 'hashicorp-vault';
integrationAuth: Types.ObjectId;
}
@@ -130,7 +134,9 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT
],
required: true,
},

View File

@@ -14,6 +14,7 @@ import {
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_HASHICORP_VAULT,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
@@ -22,9 +23,11 @@ import {
export interface IIntegrationAuth extends Document {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager';
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager' | 'checkly';
teamId: string;
accountId: string;
url: string;
namespace: string;
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
@@ -62,7 +65,8 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE
INTEGRATION_SUPABASE,
INTEGRATION_HASHICORP_VAULT
],
required: true,
},
@@ -70,6 +74,14 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
// vercel-specific integration param
type: String,
},
url: {
// for any self-hosted integrations (e.g. self-hosted hashicorp-vault)
type: String
},
namespace: {
// hashicorp-vault-specific integration param
type: String
},
accountId: {
// netlify-specific integration param
type: String,

View File

@@ -5,6 +5,7 @@ export interface ILoginSRPDetail {
clientPublicKey: string;
email: string;
serverBInt: mongoose.Schema.Types.Buffer;
userId: string;
expireAt: Date;
}
@@ -16,7 +17,6 @@ const loginSRPDetailSchema = new Schema<ILoginSRPDetail>(
},
email: {
type: String,
required: true,
unique: true
},
serverBInt: { type: mongoose.Schema.Types.Buffer },

View File

@@ -1,154 +1,144 @@
import { Schema, model, Types } from 'mongoose';
import { Schema, model, Types } from "mongoose";
import {
SECRET_SHARED,
SECRET_PERSONAL,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../variables';
import { ROOT_FOLDER_PATH } from '../utils/folder';
SECRET_SHARED,
SECRET_PERSONAL,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64,
} from "../variables";
export interface ISecret {
_id: Types.ObjectId;
version: number;
workspace: Types.ObjectId;
type: string;
user: Types.ObjectId;
environment: string;
secretBlindIndex?: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
algorithm: 'aes-256-gcm';
keyEncoding: 'utf8' | 'base64';
tags?: string[];
path?: string;
folder?: Types.ObjectId;
_id: Types.ObjectId;
version: number;
workspace: Types.ObjectId;
type: string;
user: Types.ObjectId;
environment: string;
secretBlindIndex?: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
algorithm: "aes-256-gcm";
keyEncoding: "utf8" | "base64";
tags?: string[];
folder?: string;
}
const secretSchema = new Schema<ISecret>(
{
version: {
type: Number,
required: true,
default: 1
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
type: {
type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true
},
user: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: 'User'
},
tags: {
ref: 'Tag',
type: [Schema.Types.ObjectId],
default: []
},
environment: {
type: String,
required: true
},
secretBlindIndex: {
type: String,
select: false
},
secretKeyCiphertext: {
type: String,
required: true
},
secretKeyIV: {
type: String, // symmetric
required: true
},
secretKeyTag: {
type: String, // symmetric
required: true
},
secretKeyHash: {
type: String
},
secretValueCiphertext: {
type: String,
required: true
},
secretValueIV: {
type: String, // symmetric
required: true
},
secretValueTag: {
type: String, // symmetric
required: true
},
secretValueHash: {
type: String
},
secretCommentCiphertext: {
type: String,
required: false
},
secretCommentIV: {
type: String, // symmetric
required: false
},
secretCommentTag: {
type: String, // symmetric
required: false
},
secretCommentHash: {
type: String,
required: false
},
algorithm: { // the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
],
required: true
},
// the full path to the secret in relation to folders
path: {
type: String,
required: false,
default: ROOT_FOLDER_PATH
},
folder: {
type: Schema.Types.ObjectId,
ref: 'Folder',
required: false,
},
},
{
timestamps: true
}
{
version: {
type: Number,
required: true,
default: 1,
},
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
},
type: {
type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true,
},
user: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: "User",
},
tags: {
ref: "Tag",
type: [Schema.Types.ObjectId],
default: [],
},
environment: {
type: String,
required: true,
},
secretBlindIndex: {
type: String,
select: false,
},
secretKeyCiphertext: {
type: String,
required: true,
},
secretKeyIV: {
type: String, // symmetric
required: true,
},
secretKeyTag: {
type: String, // symmetric
required: true,
},
secretKeyHash: {
type: String,
},
secretValueCiphertext: {
type: String,
required: true,
},
secretValueIV: {
type: String, // symmetric
required: true,
},
secretValueTag: {
type: String, // symmetric
required: true,
},
secretValueHash: {
type: String,
},
secretCommentCiphertext: {
type: String,
required: false,
},
secretCommentIV: {
type: String, // symmetric
required: false,
},
secretCommentTag: {
type: String, // symmetric
required: false,
},
secretCommentHash: {
type: String,
required: false,
},
algorithm: {
// the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
default: ALGORITHM_AES_256_GCM,
},
keyEncoding: {
type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true,
default: ENCODING_SCHEME_UTF8,
},
folder: {
type: String,
default: "root",
},
},
{
timestamps: true,
}
);
secretSchema.index({ tags: 1 }, { background: true });
secretSchema.index({ tags: 1 }, { background: true })
const Secret = model<ISecret>('Secret', secretSchema);
const Secret = model<ISecret>("Secret", secretSchema);
export default Secret;

View File

@@ -1,79 +1,88 @@
import { Schema, model, Types, Document } from 'mongoose';
import { Schema, model, Types, Document } from "mongoose";
export interface IServiceTokenData extends Document {
_id: Types.ObjectId;
name: string;
workspace: Types.ObjectId;
environment: string;
user: Types.ObjectId;
serviceAccount: Types.ObjectId;
lastUsed: Date;
expiresAt: Date;
secretHash: string;
encryptedKey: string;
iv: string;
tag: string;
permissions: string[];
_id: Types.ObjectId;
name: string;
workspace: Types.ObjectId;
environment: string;
user: Types.ObjectId;
serviceAccount: Types.ObjectId;
lastUsed: Date;
expiresAt: Date;
secretHash: string;
encryptedKey: string;
iv: string;
tag: string;
secretPath: string;
permissions: string[];
}
const serviceTokenDataSchema = new Schema<IServiceTokenData>(
{
name: {
type: String,
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
environment: {
type: String,
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
lastUsed: {
type: Date
},
expiresAt: {
type: Date
},
secretHash: {
type: String,
required: true,
select: false
},
encryptedKey: {
type: String,
select: false
},
iv: {
type: String,
select: false
},
tag: {
type: String,
select: false
},
permissions: {
type: [String],
enum: ['read', 'write'],
default: ['read']
}
},
{
timestamps: true
}
{
name: {
type: String,
required: true,
},
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
},
environment: {
type: String,
required: true,
},
user: {
type: Schema.Types.ObjectId,
ref: "User",
required: true,
},
serviceAccount: {
type: Schema.Types.ObjectId,
ref: "ServiceAccount",
},
lastUsed: {
type: Date,
},
expiresAt: {
type: Date,
},
secretHash: {
type: String,
required: true,
select: false,
},
encryptedKey: {
type: String,
select: false,
},
iv: {
type: String,
select: false,
},
tag: {
type: String,
select: false,
},
permissions: {
type: [String],
enum: ["read", "write"],
default: ["read"],
},
secretPath: {
type: String,
default: "/",
required: true,
},
},
{
timestamps: true,
}
);
const ServiceTokenData = model<IServiceTokenData>(
"ServiceTokenData",
serviceTokenDataSchema
);
const ServiceTokenData = model<IServiceTokenData>('ServiceTokenData', serviceTokenDataSchema);
export default ServiceTokenData;

View File

@@ -0,0 +1,47 @@
import { Schema, model, Types, Document } from 'mongoose';
export interface ITokenVersion extends Document {
user: Types.ObjectId;
ip: string;
userAgent: string;
refreshVersion: number;
accessVersion: number;
lastUsed: Date;
}
const tokenVersionSchema = new Schema<ITokenVersion>(
{
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
ip: {
type: String,
required: true
},
userAgent: {
type: String,
required: true
},
refreshVersion: {
type: Number,
required: true
},
accessVersion: {
type: Number,
required: true
},
lastUsed: {
type: Date,
required: true
}
},
{
timestamps: true
}
);
const TokenVersion = model<ITokenVersion>('TokenVersion', tokenVersionSchema);
export default TokenVersion;

View File

@@ -1,7 +1,13 @@
import { Schema, model, Types, Document } from 'mongoose';
export enum AuthProvider {
GOOGLE = 'google',
}
export interface IUser extends Document {
_id: Types.ObjectId;
authId?: string;
authProvider?: AuthProvider;
email: string;
firstName?: string;
lastName?: string;
@@ -15,7 +21,6 @@ export interface IUser extends Document {
tag?: string;
salt?: string;
verifier?: string;
refreshVersion?: number;
isMfaEnabled: boolean;
mfaMethods: boolean;
devices: {
@@ -26,9 +31,17 @@ export interface IUser extends Document {
const userSchema = new Schema<IUser>(
{
authId: {
type: String,
},
authProvider: {
type: String,
enum: AuthProvider,
},
email: {
type: String,
required: true
required: true,
unique: true,
},
firstName: {
type: String
@@ -77,11 +90,6 @@ const userSchema = new Schema<IUser>(
type: String,
select: false
},
refreshVersion: {
type: Number,
default: 0,
select: false
},
isMfaEnabled: {
type: Boolean,
default: false
@@ -94,7 +102,8 @@ const userSchema = new Schema<IUser>(
ip: String,
userAgent: String
}],
default: []
default: [],
select: false
}
},
{

View File

@@ -37,10 +37,6 @@ const workspaceSchema = new Schema<IWorkspace>({
name: "Development",
slug: "dev"
},
{
name: "Test",
slug: "test"
},
{
name: "Staging",
slug: "staging"

View File

@@ -1,6 +1,7 @@
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';
@@ -27,20 +28,50 @@ router.post( // deprecated (moved to api/v2/auth/login2)
);
router.post(
'/logout',
'/logout',
authLimiter,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
}),
authController.logout
);
router.post(
'/checkAuth',
'/checkAuth',
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
}),
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,
authController.getCommonPasswords
);
router.delete(
'/sessions',
authLimiter,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
authController.revokeAllSessions
);
export default router;

View File

@@ -15,7 +15,7 @@ import password from './password';
import stripe from './stripe';
import integration from './integration';
import integrationAuth from './integrationAuth';
import secretsFolder from './secretsFolder'
import secretsFolder from './secretsFolder';
export {
signup,

View File

@@ -15,7 +15,7 @@ import {
import { body, param } from 'express-validator';
import { integrationController } from '../../controllers/v1';
router.post( // new: add new integration for integration auth
router.post(
'/',
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]

View File

@@ -57,6 +57,8 @@ router.post(
body('workspaceId').exists().trim().notEmpty(),
body('accessId').trim(),
body('accessToken').exists().trim().notEmpty(),
body('url').trim(),
body('namespace').trim(),
body('integration').exists().trim().notEmpty(),
validateRequest,
requireAuth({

View File

@@ -1,50 +1,70 @@
import express, { Request, Response } from 'express';
import express from "express";
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
validateRequest
} from '../../middleware';
import { body, param } from 'express-validator';
import { createFolder, deleteFolder, getFolderById } from '../../controllers/v1/secretsFolderController';
import { ADMIN, MEMBER } from '../../variables';
validateRequest,
} from "../../middleware";
import { body, param, query } from "express-validator";
import {
createFolder,
deleteFolder,
getFolders,
updateFolderById,
} from "../../controllers/v1/secretsFolderController";
import { ADMIN, MEMBER } from "../../variables";
router.post(
'/',
"/",
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ["jwt"],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body'
locationWorkspaceId: "body",
}),
body('workspaceId').exists(),
body('environment').exists(),
body('folderName').exists(),
body('parentFolderId'),
body("workspaceId").exists(),
body("environment").exists(),
body("folderName").exists(),
body("parentFolderId"),
validateRequest,
createFolder
);
router.delete(
'/:folderId',
router.patch(
"/:folderId",
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ["jwt"],
}),
param('folderId').exists(),
body("workspaceId").exists(),
body("environment").exists(),
param("folderId").not().isIn(["root"]).exists(),
validateRequest,
updateFolderById
);
router.delete(
"/:folderId",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
body("workspaceId").exists(),
body("environment").exists(),
param("folderId").not().isIn(["root"]).exists(),
validateRequest,
deleteFolder
);
router.get(
'/:folderId',
"/",
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ["jwt"],
}),
param('folderId').exists(),
query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(),
query("parentFolderId").optional().isString().trim(),
validateRequest,
getFolderById
getFolders
);
export default router;
export default router;

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