Compare commits

..

1256 Commits

Author SHA1 Message Date
5128466233 update go.mod after Infisical/go-keyring update 2023-09-25 15:28:13 -07:00
e11abb619a use v1.0.2 of internal keyring in cli 2023-09-25 15:10:59 -07:00
f51e9ba8ff add back role migration 2023-09-25 11:51:25 -07:00
a255af6ad8 Merge pull request #1022 from Infisical/debug-vercel-integration
Patch Vercel integration for custom preview branches
2023-09-23 11:42:38 +01:00
30da2e50b1 Patch Vercel integration for custom preview branches 2023-09-23 11:38:49 +01:00
7f9bd93382 Merge pull request #1004 from vwbusguy/bugfix/no-auto-capitalization-support
Fix no Auto-Capitalization for secrets get/set. Fixes #1003
2023-09-22 13:28:15 -07:00
e81ea314e1 update go minor version 2023-09-22 13:26:32 -07:00
f19aca2904 fix zod type for ToggleAutoCapitalizationV2 2023-09-22 13:10:11 -07:00
763bdabd60 Merge pull request #998 from Infisical/qovery-integration
Added Qovery integration
2023-09-22 12:44:06 -07:00
7ec708b71d Merge pull request #1019 from akhilmhdh/fix/audit-log
feat: made audit log options back
2023-09-22 12:24:42 -07:00
3c6c1891a8 feat: made audit log options back 2023-09-22 22:07:48 +05:30
01d3d84b40 Merge pull request #1017 from Infisical/debug-self-hosted-gitlab
Patch self-hosted gitlab integration
2023-09-22 15:47:05 +01:00
32bec03adf Patch self-hosted gitlab integration 2023-09-22 15:19:26 +01:00
5b6c2e05f2 Merge pull request #1016 from Infisical/fix-teamcity
Fix TeamCity integration blank screen issue
2023-09-22 12:11:59 +01:00
c623f572b7 Fix TeamCity integration 2023-09-22 12:06:42 +01:00
48f7bd146f added bun docs 2023-09-20 23:53:31 -07:00
da6fa6d8ce added bun docs 2023-09-20 23:44:15 -07:00
cf8e597c7d Update bun.mdx 2023-09-20 23:34:12 -07:00
43c31332e4 added bun docs 2023-09-20 19:15:58 -07:00
88fbf6f88e added bun docs 2023-09-20 19:06:45 -07:00
119730ac1a Update build-docker-image-to-prod.yml 2023-09-20 17:06:12 -04:00
1d66dbbce3 Merge pull request #1010 from akhilmhdh:code-editor-fix
Stable multiline input
2023-09-20 16:26:05 -04:00
b0991c33b0 Merge pull request #1013 from Infisical:fix-github-sso-email
Update method to obtain email for GitHub SSO
2023-09-20 16:09:36 -04:00
d863dece79 Merge pull request #1012 from akhilmhdh/feat/accordion-component
fix: resolved broken style of accordion component and added storybook
2023-09-20 17:46:20 +01:00
96fbc6c5a0 Update method to obtain email for GitHub SSO 2023-09-20 17:38:00 +01:00
a93631d41c fix: resolved broken style of accordion component and added storybook 2023-09-20 20:59:34 +05:30
2c7aac37a2 feat: resolved trailing whitespace not showing up 2023-09-20 17:05:57 +05:30
6b8d4c2fea fix: padding 2023-09-20 17:05:57 +05:30
f84235eea3 fix: scroll 2023-09-20 17:05:57 +05:30
63e8ecce5b fix: break 2023-09-20 17:05:57 +05:30
ef7bf09398 fix: trimming 2023-09-20 17:05:57 +05:30
3be3867579 fix: font size 2023-09-20 17:05:57 +05:30
7f753b23f8 fix: secret input 2023-09-20 17:05:57 +05:30
81827e2deb chore: remove content editable 2023-09-20 17:05:57 +05:30
f02ea8d9b8 Merge pull request #1006 from vwbusguy/bugfix/helm-chart-frontend-resources
Update cpu default frontend value.  Fixes #1005.
2023-09-19 12:46:13 -04:00
1609bd4652 update chart version and frontend cpu 2023-09-19 12:45:07 -04:00
a620f1c924 add workspace index for SecretBlindIndexData 2023-09-19 11:46:37 -04:00
0a3e7731d9 Merge pull request #1009 from Infisical/google-github-sso-docs
Add Google and GitHub SSO configuration docs
2023-09-19 16:00:52 +01:00
0ca8425965 Add enterprise notice to SAML SSO docs 2023-09-19 15:54:56 +01:00
14a260b785 Finish adding docs for Google SSO and GitHub SSO configuration 2023-09-19 15:53:19 +01:00
b6219e14f0 Finish optimizing qovery integration, add docs for it 2023-09-19 14:43:20 +01:00
663c4869b9 Merge pull request #1008 from Infisical/fix-vercel-integration
Patch integrations involving teamId
2023-09-19 14:09:14 +01:00
3103075c3f Bring back missing teamId when fetching integrationAuth 2023-09-19 14:06:06 +01:00
e3ef826f52 Update qovery integration 2023-09-19 13:50:01 +01:00
215ef0bb29 Merge pull request #975 from JanetEne/update-use-secret-path-across-app
update and use secret path examples where applicable across app
2023-09-18 21:36:46 -07:00
9cc220e51f Merge branch 'main' into update-use-secret-path-across-app 2023-09-18 21:31:34 -07:00
8fa90d94ac Deleted console.log 2023-09-18 21:29:49 -07:00
609204f7f6 Merge pull request #987 from Infisical/permissioning-style-improvements
change certain permissioning text
2023-09-18 21:17:35 -07:00
d501130e64 Update secret-reference.mdx 2023-09-18 19:30:33 -07:00
45734d78c0 Update cpu default frontend value. Fixed #1005.
Signed-off-by: Scott Williams <scottwilliams@ucsb.edu>
2023-09-18 19:14:14 -07:00
dd9a2dd345 Update secret-reference.mdx 2023-09-18 18:44:08 -07:00
80bec24219 Fix no Auto-Capitalization for secrets get/set. Fixes https://github.com/Infisical/infisical/issues/1003
Signed-off-by: Scott Williams <scottwilliams@ucsb.edu>
2023-09-18 15:07:58 -07:00
4765dd0696 remove backfillPermission (no longer needed) 2023-09-18 12:50:40 -04:00
0d4cacdc3e remove old ValidateEnvironmentName check 2023-09-18 12:26:09 -04:00
0b59a92dfb Added Qovery integration 2023-09-17 18:15:47 -07:00
64d5a82e1b Merge pull request #988 from Infisical/integrations-uiux-updates
improved ui/ux for checkly, gcp, and vault integrations
2023-09-16 18:40:00 +01:00
a89ed40dcd Update Fly.io integration authorization page to use react-form 2023-09-16 18:13:55 +01:00
1634f9ec49 Merge pull request #996 from akhilmhdh/fix/integration-revoke
fix: integration bot deactive resolved and redis type error
2023-09-16 14:26:43 +01:00
dd1bb84361 fix: patched auto bot removal for integration 2023-09-16 18:03:13 +05:30
0f003e8ab6 Add type assertion for GitLab selfhosted URL 2023-09-16 12:42:23 +01:00
14d253d01a Update GCP SM and GitLab integrations to have overwrite popup warning 2023-09-16 12:36:29 +01:00
750c1b46da fix: integration bot deactive resolved and redis type error 2023-09-16 16:47:41 +05:30
2e07512bae Merge remote-tracking branch 'origin' into integrations-uiux-updates 2023-09-16 09:27:49 +01:00
20a6497218 Fixed UI for TeamCity, Fly.io, Render integrations 2023-09-15 22:18:25 -07:00
72839719fd Fixed UI for the AWS integrations 2023-09-15 21:43:46 -07:00
d8d480f2bc edge case: read write for non existing env in deniedPermissions 2023-09-15 22:32:42 -04:00
58c3a4ebc1 Merge branch 'main' into integrations-uiux-updates 2023-09-15 18:22:29 -07:00
0d83954c39 fixed merged conflicts for gitlab and added animation for tabs 2023-09-15 18:18:40 -07:00
80cee40b39 Updated UI for CircleCI integration 2023-09-15 17:53:58 -07:00
6059070d29 Updated UI for Vercel, GCP, GitLab, Checkly, Vault integrations 2023-09-15 17:24:30 -07:00
f16944024b Update build-staging-img.yml 2023-09-15 20:18:05 -04:00
29da8843a3 add prefix for role name 2023-09-15 20:11:06 -04:00
8cd6a1f564 no release lock after backfill complete 2023-09-15 18:56:41 -04:00
e8fd3c8045 update lock time for permission backfill 2023-09-15 18:14:31 -04:00
59cd8580d5 bring back tests for CI 2023-09-15 17:56:12 -04:00
859cec49d1 make redis client conditional 2023-09-15 17:55:43 -04:00
fccbf9810f fixed gcp integration UIUX 2023-09-15 14:30:37 -07:00
5494bc6c3c Update build-staging-img.yml 2023-09-15 17:22:54 -04:00
95385b1f45 Merge pull request #991 from akhilmhdh/feat/rbac-migratio
feat(rbac): migration script for permission from old permission to new
2023-09-15 17:09:51 -04:00
b88a319582 add lock mechanism for backfillPermission script 2023-09-15 16:39:59 -04:00
db5883ae56 fix merge conflicts 2023-09-15 13:28:11 -07:00
26229b07bc Merge pull request #994 from Infisical/integration-options
Finish integration options/react form refactor for GitLab and GCP SM …
2023-09-15 21:00:55 +01:00
3ab5db9b2a Finish integration options/react form refactor for GitLab and GCP SM integrations, add docs for it 2023-09-15 20:53:31 +01:00
717b831e94 Merge pull request #992 from serin0837/parse-env-file
fix hyphen env variable import bug
2023-09-15 11:45:36 -07:00
336b5897f0 update role description 2023-09-15 12:40:40 -04:00
0ce5aaf61c add role deduplication logic 2023-09-15 11:44:48 -04:00
adfa90340d remove unsetting deniedPermissions 2023-09-15 09:53:17 -04:00
444aca0070 fix hyphen env variable import bug 2023-09-15 14:52:30 +01:00
029766c534 feat(rbac): migration script for permission from old permission to new 2023-09-15 16:20:21 +05:30
bde788c4f6 improved ui/ux for checkly, gcp, and vault integrations 2023-09-14 22:21:59 -07:00
9b14b64ec2 Merge pull request #983 from ragnarbull/main
Docs: Update FAQ for Alpine CDN error
2023-09-15 00:31:14 -04:00
0a72dccdcf add back defaultOpen="true" 2023-09-15 00:30:15 -04:00
7fe94d66cd Create new FAQ page under developer setup docs 2023-09-15 11:59:15 +10:00
f503f8c76d Merge pull request #985 from xphyr/main
changing CMD for Dockerfile  to address issue #984
2023-09-14 19:32:09 -04:00
7982b1d668 replace owner role for local dev user 2023-09-14 19:27:23 -04:00
7a78209613 Merge pull request #977 from akhilmhdh:feat/permission-patch-2
feat(rbac): removed owner role and changed member permissions
2023-09-14 18:57:02 -04:00
019024e4ae remove the use of owner everywhere else 2023-09-14 18:23:59 -04:00
4d6895a793 Merge pull request #933 from MohamadTahir:add_resource_probs_to_deployments
Add resource specification to frontend and backend deployment containers
2023-09-14 16:36:51 -04:00
36b5ba2855 remove change log bc will get replaced by auto generated one soon 2023-09-14 16:35:13 -04:00
44d2a6c553 clearing npm cache to save space 2023-09-14 13:57:58 -04:00
a073a746f2 changing CMD for Dockerfile to use node instead of npm to address issue #984 2023-09-14 13:33:15 -04:00
edb3e66267 fix(integrations): resolved integration bot deactive revoke bug 2023-09-14 21:56:57 +05:30
75be302166 Merge pull request #969 from quinton11:feat/secrets-get-cmd-path
fix: `path` option for `secret get` sub cmd
2023-09-14 12:22:16 -04:00
b459d2d5f5 Merge pull request #954 from jessebot/patch-1
Update values.yaml - add `mongodb.auth.existingSecret` parameter to helm chart
2023-09-14 12:08:03 -04:00
942e1a82c2 feat(rbac): removed audit log option for time being, v3 secret patch and reload permission flash screen fix 2023-09-14 21:34:13 +05:30
9d2bc25cb4 Merge pull request #978 from hcourdent/patch-1
Backlink to Windmill.dev in windmill.mdx
2023-09-14 11:35:52 -04:00
13083d7676 remove link from description - anti pattern in docs 2023-09-14 11:35:33 -04:00
18d843f3e6 feat(rbac): fixed ip allow list api 2023-09-14 20:40:57 +05:30
ee96325034 Update FAQ for Alpine CDN error 2023-09-14 23:24:32 +10:00
954f15e4df Merge pull request #980 from Infisical/fix-gitlab-integration-self-hosted
Update all GitLab integration URLs to point to self-hosted instances if applicable (else GitLab Cloud)
2023-09-14 11:42:00 +01:00
88842951cb Change +metadata to metadata 2023-09-14 11:16:59 +01:00
8e88a3a25f Point getTeamsGitLab method to self-hosted GitLab instance if applicable 2023-09-14 10:59:09 +01:00
f1e1ca07df Backlink to Windmill.dev in windmill.mdx 2023-09-14 11:20:44 +02:00
5bf2c2f52b Point getAppsGitLab method to self-hosted GitLab instance if applicable, fix integration revocation issue related to missing metadata param partly 2023-09-14 10:14:10 +01:00
3d2a2651b8 feat(rbac): removed owner role and changed member permissions 2023-09-14 13:20:42 +05:30
0f02ef701e change some permissioning text 2023-09-13 19:02:24 -07:00
1c5e80e68a update file naming 2023-09-14 02:33:08 +01:00
c30381edbc uchange component name to globpatternexample 2023-09-14 02:17:33 +01:00
2554ad2b3c update and use secret path examples where applicable across app 2023-09-14 01:51:05 +01:00
c3696bdbbc Merge pull request #972 from akhilmhdh/feat/permission-patch
feat(rbac): grouped folder and imports permission into secret permission
2023-09-13 13:33:26 -04:00
1be924d210 Update kubernetes.mdx 2023-09-13 10:10:45 -07:00
2333675262 feat(rbac): grouped folder and imports permission into secret permission 2023-09-13 22:30:56 +05:30
c3c16f4e42 Merge pull request #966 from serin0837/gcp-secret-manager-add-labels
add label managed by infisical
2023-09-13 10:12:16 +01:00
99a2203b38 Update GCP SM docs to include note on Cloud Resource Manager API enabled requirement and label 2023-09-13 10:10:50 +01:00
92b64d3553 Merge remote-tracking branch 'origin' into gcp-secret-manager-add-labels 2023-09-13 09:50:26 +01:00
533e628183 Merge pull request #973 from Infisical/expose-folders-api
Implement workaround for swagger-autogen edgecase and expose folders,…
2023-09-13 09:44:35 +01:00
3b7096710c Fix merge conflicts 2023-09-13 09:42:36 +01:00
97b7a5ebdf Fix merge conflicts 2023-09-13 09:35:53 +01:00
4f69257595 Implement workaround for swagger-autogen edgecase and expose folders, secret imports API 2023-09-13 09:28:29 +01:00
8f93141d54 Merge pull request #962 from G3root/fix-login
fix: overflow issue login page
2023-09-12 16:57:07 -07:00
2bbba8a43a Merge pull request #970 from zenorocha/resend
Add Resend instructions
2023-09-12 16:34:45 -07:00
4da251bdfc Add Resend instructions 2023-09-12 16:21:04 -07:00
8520ae8d43 Update .infisicalignore 2023-09-12 19:05:29 -04:00
e38abb128a Update .infisicalignore 2023-09-12 19:05:03 -04:00
d46a6f7270 fix: path option for secret get sub cmd 2023-09-12 22:11:15 +00:00
33adbc0f24 add label managed by infisical 2023-09-12 09:57:03 +01:00
fc3b0e1de9 Merge pull request #963 from akhilmhdh/feat/move-roles-ee
refactor: moved role folder to ee
2023-09-11 21:38:33 -04:00
ba225dd504 Added card tiles to security page 2023-09-11 16:37:17 -07:00
d221bf8ae9 Merge pull request #964 from Infisical/update-security-docs
Add/update docs to include internals
2023-09-12 00:04:49 +01:00
d7354e1aca Finish docs for internals 2023-09-12 00:02:43 +01:00
86b2b95d11 update readme file and values.yaml documentations 2023-09-11 22:10:47 +03:00
e7c5e6a789 refactor: moved role folder to ee 2023-09-11 23:53:05 +05:30
1ca106279e fix: overflow login page 2023-09-11 22:42:02 +05:30
49d6f85f42 Merge pull request #961 from akhilmhdh/fix/folder-bug
fix: resovled batch function not working with folders
2023-09-11 15:25:34 +01:00
c894952e84 fix: resovled batch function not working with folders 2023-09-11 13:01:49 +05:30
4ce0eccfa1 Merge pull request #959 from JanetEne/helper-text-for-secret-path
Add helper text and tooltip for secret path
2023-09-09 18:17:24 -07:00
ad710f4860 fixed the style of the webhook tooltip 2023-09-09 18:10:02 -07:00
45117ba1f4 show tooltip on hover of icon and not onclick 2023-09-09 22:35:55 +01:00
1b2b1ca30b Update README.md 2023-09-09 13:24:16 -07:00
b89a90066a Merge pull request #958 from Infisical/check-gcp-sm-integration
Patch broken integrations due to permission update
2023-09-09 17:59:31 +01:00
83d2a39fb1 Patch broken integratins due to permission update 2023-09-09 17:29:19 +01:00
412b1123af Begin security docs revamp 2023-09-09 10:00:09 +01:00
dc91615b43 add helper text and tooltip for secret path 2023-09-09 04:06:16 +01:00
110153385b permissioning style update 2023-09-08 20:05:02 -07:00
e37810f302 Update Chart.yaml - bump helm chart version after adding mongodb.auth.existingSecret parameter to values.yaml 2023-09-08 18:52:16 +02:00
f02e39e7e3 Merge branch 'Infisical:main' into patch-1 2023-09-08 18:50:24 +02:00
305ddd3813 Merge pull request #860 from akhilmhdh/feat/new-permission-system
New Permission System
2023-09-08 11:59:49 -04:00
b023bb1df2 feat(rbac): removed unused variables 2023-09-08 21:26:10 +05:30
63ff669612 feat(rbac): applied subscription banner in custom roles 2023-09-08 21:22:36 +05:30
8003273b2c feat(rbac): changed withpermission hoc text and removed loading on overview pageg 2023-09-08 21:22:36 +05:30
2f9b35b2f9 feat(rbac): resolved submit button getting disabled 2023-09-08 21:22:36 +05:30
91c1aca588 add role index 2023-09-08 21:22:36 +05:30
e8f7b0c181 feat(rbac): resolved batch bug in permission check 2023-09-08 21:22:36 +05:30
43735b8183 feat(rbac): updated role controller to check permissions and batch v2 selectively permission check 2023-09-08 21:22:36 +05:30
988bb4ffb6 remove created & justify delete buttons to right 2023-09-08 21:22:36 +05:30
0f46f53a7d update denied text 2023-09-08 21:22:36 +05:30
9c9d46824c only backfill roles for non empty permissions array 2023-09-08 21:22:36 +05:30
43b97b411b feat(rbac): updated helper text on glob pattern in multi env permission field 2023-09-08 21:22:36 +05:30
38c044f9a7 feat(rbac): put back condition matcher for workspace permission check and added migration function 2023-09-08 21:22:36 +05:30
c91a93ef2a feat(rbac): resolved secret import not listing 2023-09-08 21:22:36 +05:30
a4ef829046 feat(rbac): added glob support in permission and revealed settings 2023-09-08 21:22:36 +05:30
2ed079830a make org permissions more readable 2023-09-08 21:22:36 +05:30
98893a40f1 feat(rbac): made changes from testing with maidul 2023-09-08 21:22:36 +05:30
252042fb20 feat(rbac): resolved viewer permission in list 2023-09-08 21:22:36 +05:30
4ca95f4d79 feat(rbac): resolved zod validator issues and trim in frontend 2023-09-08 21:22:36 +05:30
26028e7312 feat(rbac): fixed broken view when clicking editing predefined role 2023-09-08 21:22:36 +05:30
6bbdc4a405 feat(rbac): fixed broken invite and role missing in dropdown 2023-09-08 21:22:36 +05:30
cb9ee00ed3 feat(rbac): made org role section permission into one component and fixed some small issues 2023-09-08 21:22:36 +05:30
f1a291a52a feat(rbac): fixed merge conflicts and resolved some more issues with permission checks 2023-09-08 21:22:36 +05:30
bcfe1bda84 feat(rbac): made new permission check for v3 secrets and v2 batch 2023-09-08 21:22:36 +05:30
82d4c8f000 Split requireBlindIndicesEnabled, E2EEOff, requireIPAllowlistCheck away from requireWorkspaceAuth 2023-09-08 21:22:36 +05:30
7c698e755a feat(rbac): added frontend permission validation for missing pages and merged new features 2023-09-08 21:22:36 +05:30
4b0bc238fc feat(rbac): changed the action names for org 2023-09-08 21:22:36 +05:30
ea9e638d03 feat(rbac): resolved merge conflict 2023-09-08 21:22:36 +05:30
6671699867 feat(rbac): added new permission check for workspace in frontend 2023-09-08 21:22:36 +05:30
549121f44e feat(rbac): added new validation to all routes and permission check to most 2023-09-08 21:22:36 +05:30
520a553ea1 feat(rbac): implemented project based permission loading and role management 2023-09-08 21:22:36 +05:30
aac3168c80 feat(rbac): implemented granular blocking of actions based on permissions on org level ui 2023-09-08 21:21:43 +05:30
34fb7be1c4 feat(rbac): added org permission context for pulling permission from api 2023-09-08 21:21:43 +05:30
e342e88499 feat(rbac): added new zod validation and permission check for all org level api 2023-09-08 21:20:29 +05:30
96437fd1b7 feat(rbac): added rest of permissions in ui and backend api for org level 2023-09-08 21:20:29 +05:30
c5f76b1e6f feat(rbac): base ui for org rbac management 2023-09-08 21:20:29 +05:30
1167b1bc60 feat(rbac): ui components and hooks for rbac 2023-09-08 21:20:29 +05:30
1bf9041ac9 feat(rbac): implemented backend base apis for rbac permission system 2023-09-08 21:20:29 +05:30
6bca7dcc58 update k8 self host image versions examples 2023-09-07 22:33:57 -04:00
a9f062b469 Merge branch 'main' of https://github.com/Infisical/infisical 2023-09-07 17:35:38 -07:00
43caccad9f removed intercom 2023-09-07 17:35:26 -07:00
9cef1d3b10 Merge pull request #943 from RezaRahemtola/main
docs: Fixing multiple typos
2023-09-07 14:18:05 -04:00
6c126606da Update README.md for helm chart parameters to include mongodb.auth.existingSecret 2023-09-07 20:02:15 +02:00
ae37d80891 Update values.yaml - add mongodb.auth.existingSecret parameter 2023-09-07 19:55:21 +02:00
8e6b0ca0a9 Merge pull request #953 from serin0837/patch-1
Update gcp-secret-manager.mdx
2023-09-07 13:58:55 +01:00
a288a0bf2a Update gcp-secret-manager.mdx
Just found out that there is a typo for the redirect URL.
2023-09-07 11:03:57 +01:00
9dd77ae36e Merge pull request #951 from Infisical/gitlab-integration-selfhosted
Extend GitLab integration to support syncing to self-hosted instances of GitLab
2023-09-06 11:15:39 +01:00
1e20715511 Add self-hosted gitlab url to sync function 2023-09-06 11:11:11 +01:00
d07b2dafc3 Finish adding support for self-hosted GitLab integration 2023-09-06 10:57:27 +01:00
04548313ab Merge remote-tracking branch 'origin' into gitlab-integration-selfhosted 2023-09-06 09:55:19 +01:00
86bf2ddd89 Merge pull request #949 from Infisical/aws-ps-integration
Fix null appId argument for creating PAT integration
2023-09-05 20:15:26 +01:00
2ad663c021 Update integration app and appId values 2023-09-05 20:10:50 +01:00
56317a3f53 Fix null appId argument for creating PAT integration 2023-09-05 19:55:54 +01:00
ad0bc4efdc Continue progress on self-hosted gitlab integration 2023-09-05 19:46:59 +01:00
bf74b75c4a Merge pull request #932 from daninge98/secret-scan-whole-repo
Secret scanning: scan for leaked secrets in historical commits
2023-09-04 15:49:21 -04:00
7f543b635c Merge pull request #944 from Infisical/gcp-sm-integration-auth
Add support for service account JSON authentication method for GCP Secret Manager integration
2023-09-04 14:20:15 +01:00
353dfeb2a9 Fix lint issues 2023-09-04 14:15:46 +01:00
16196e6343 Fix lint issues 2023-09-04 14:13:16 +01:00
3f2b74a28a Merge 2023-09-04 12:49:11 +01:00
4a603da425 Finished adding support for service account JSON auth method for GCP secret manager integration 2023-09-04 12:48:15 +01:00
0d9ce70000 docs: Fixing multiple typos 2023-09-03 23:28:19 +02:00
9fa1e415c8 Merge pull request #942 from omahs/patch-1
Fix typos
2023-09-03 14:24:37 +01:00
09b22f36c0 fix typo 2023-09-03 15:12:13 +02:00
43c8c42249 fix typo 2023-09-03 15:11:26 +02:00
d4a7ad713c fix typos 2023-09-03 15:10:19 +02:00
9afad7df32 fix typos 2023-09-03 15:09:18 +02:00
3f61a24ef1 fix typos 2023-09-03 15:06:53 +02:00
f342c345b7 Merge pull request #934 from Infisical/aws-ps-integration-fix
Add support for syncing secrets containing / to AWS parameter store integration
2023-09-02 11:17:06 +01:00
6dd46885f8 Add support for syncing secrets containing / to AWS parameter store integration 2023-09-02 11:11:27 +01:00
5704dfb35f add resource specification to frontend and backend deployment containers 2023-09-02 09:29:23 +03:00
41774fa97c fixed the bug with reading secrets from json 2023-09-01 19:34:38 -07:00
32b26c331c Fix gitignore 2023-09-01 23:10:12 +01:00
011507b8e0 Small fixes 2023-09-01 23:08:38 +01:00
4adb2a623e Merge remote-tracking branch 'origin/main' into secret-scan-whole-repo 2023-09-01 22:43:20 +01:00
1d410c8420 Fix type 2023-09-01 22:40:19 +01:00
35f3d6c776 Initial implementation 2023-09-01 22:36:52 +01:00
5e24b517cc Merge pull request #928 from davidhonig/fix-webhook-status-date-formatting
Fix date-fn format string
2023-09-01 14:05:50 -04:00
bf95415a0d Update k8 operator Chart version 2023-09-01 13:33:51 -04:00
4025063732 Merge pull request #923 from xphyr/main
Fix for issue#922 - helm chart for secrets-operator
2023-09-01 13:33:13 -04:00
e3ecfbaaa5 Fix date-fn format string 2023-09-01 12:16:44 +02:00
a7b3d12844 only capture non sign up secret events 2023-08-31 20:58:07 -04:00
f5145c6c39 fixing deployment.yaml file in secrets-operator helm chart. removed extra LF that was breaking the template 2023-08-31 17:43:15 -04:00
d9abe671af Start GCP SM integration update 2023-08-31 15:22:26 +01:00
37ae05fa2a Update changelog 2023-08-31 14:46:22 +01:00
086ce0d2a6 Merge pull request #918 from Infisical/revert-917-snyk-fix-29828c58f69ea88c3d50dad65d7767d2
Revert "[Snyk] Fix for 1 vulnerabilities"
2023-08-30 16:36:22 -04:00
06dec29773 Revert "[Snyk] Fix for 1 vulnerabilities" 2023-08-30 16:35:44 -04:00
ed8e942a5d Update low entropy password error message 2023-08-30 21:28:35 +01:00
e770bdde24 Update low entropy password error message 2023-08-30 21:27:31 +01:00
a84dab1219 Merge pull request #917 from Infisical/snyk-fix-29828c58f69ea88c3d50dad65d7767d2
[Snyk] Fix for 1 vulnerabilities
2023-08-30 16:26:20 -04:00
02d9d7b6a4 fix: backend/package.json & backend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-MONGODB-5871303
2023-08-30 20:05:26 +00:00
f21eb3b7c8 Patch GCP secret manager integration edge-case 2023-08-30 21:04:39 +01:00
219e3884e7 Merge pull request #912 from Infisical/integration-suffixes
Added suffixes to the Checkly integration
2023-08-30 10:29:08 +01:00
41cd8b7408 Move secretSuffix to separate metadata field 2023-08-30 10:04:44 +01:00
f6be86a26b Added suffixes to integrations 2023-08-29 22:17:48 -07:00
85e5822ece Merge pull request #908 from akhilmhdh/fix/sec-override-fail
fix: resolved personal override not showing up
2023-08-29 14:07:09 -04:00
5c9e89a8e2 Merge pull request #904 from Infisical/dashboard-get-secrets
Rewire dashboard to pull from v3/secrets with folderId support
2023-08-29 13:54:37 -04:00
46a77d5e58 Merge pull request #909 from Infisical/team-city-branch-config
Add support for build-configuration environment variable sync for TeamCity integration
2023-08-29 14:43:17 +01:00
a6e9643464 Finish adding support for build-configuration level syncs for TeamCity integration 2023-08-29 14:37:58 +01:00
affa2ee695 fix: resolved personal override not showing up 2023-08-29 12:23:12 +05:30
dc0d577cbb Patch TeamCity integration 2023-08-29 07:46:11 +01:00
9e8ddd2956 Merge pull request #907 from ragnarbull/patch-1
Update overview.mdx
2023-08-28 17:41:26 -07:00
b40b876fb2 Update overview.mdx
New password criteria + keep formatting consistent
2023-08-29 10:20:15 +10:00
2ba6a65da4 Change order of password check 2023-08-28 11:43:40 +01:00
76cf79d201 Merge pull request #885 from ragnarbull/ragnarbull-auth-pwd-fixes
Password fixes - enforce max length, add checks (pwd breach, PII, low entropy), improved UX, deprecate common-passwords api
2023-08-28 11:33:57 +01:00
a79c6227b1 Fix frontend lint issues 2023-08-28 11:25:50 +01:00
f1f64e6ff5 Fix flaky regex g flag causing unexpected validation password validation issue 2023-08-28 11:08:00 +01:00
d72ddfe315 Rewire dashboard to pull from v3/secrets with folderId support 2023-08-28 09:12:04 +01:00
f924d0c02c Update kubernetes-helm.mdx 2023-08-27 22:39:19 -07:00
ef1b75d890 remove the use of aggregation for documentDB compatibility 2023-08-27 14:41:35 -04:00
d8094b2ab1 Merge pull request #903 from Infisical/integration-setup-docs
Add self-hosted setup/configuration docs for OAuth2 integrations
2023-08-27 12:16:26 +01:00
ad61fa845c Add self-hosted configuration docs for GitHub, GitLab, GCP SM, Vercel, Heroku, Netlify, Azure KV 2023-08-27 12:14:17 +01:00
6bb5e7078f Merge pull request #902 from Infisical/gcp-integration
GCP Secret Manager Integration
2023-08-26 17:42:59 +01:00
a07ddb806d Finish GCP secret manager integration 2023-08-26 17:36:20 +01:00
6e7d3d6912 Merge pull request #901 from Infisical/environment-api
Expose CRUD Environment Operations to Public REST API
2023-08-26 08:49:35 +01:00
84a866eb88 Add API Key auth method to environment endpoints, add endpoints to public REST API docs 2023-08-26 08:47:36 +01:00
9416fca832 update to doc5.0 engine 2023-08-25 17:23:31 -04:00
2ea518b107 add redis to cloud formation 2023-08-25 15:35:11 -04:00
62399dd293 Merge pull request #897 from akhilmhdh/fix/sec-v3-fail
fix: moved backend get sec to v2 for dashboard
2023-08-25 12:09:04 -04:00
16f1360550 fix: moved backend get sec to v2 for dashboard 2023-08-25 21:37:05 +05:30
a99751eb72 Moved pwd checks into a subfolder 2023-08-25 12:36:53 +10:00
9ea414fb25 Merge pull request #894 from akhilmhdh/fix/multi-line-html-encode
fix(multi-line): resolved breaking ui when secret value contains < or >
2023-08-24 22:12:42 -04:00
a9fa3ebab2 update post hog event name 2023-08-24 19:01:59 -04:00
293a62b632 update secrets posthog event logic 2023-08-24 18:48:46 -04:00
a1f08b064e add tags support in secret imports 2023-08-24 17:21:14 -04:00
50977cf788 reduce k8 events 2023-08-24 15:41:29 -04:00
fccec083a9 fix(multi-line): resolved breaking ui when secret value contains < or > 2023-08-24 23:07:58 +05:30
63af7d4a15 Merge remote-tracking branch 'origin' into gcp-integration 2023-08-25 00:35:11 +07:00
ab3533ce1c Checkpoint GCP secret manager integration 2023-08-25 00:34:46 +07:00
4d6a8f0476 Fixed form (error messages too long). Consolidated tests & errors. Moved regexes to another file. Added regex to check for PII & reject pwd if true. Confirmed hashing & encryption/decryption works with top 50 languages, emojis etc (screen videos & unit tests to come). 2023-08-25 01:44:02 +10:00
688cf91eb7 Removed unnecessary validator library & @types/validator in favor of yup 2023-08-24 14:08:11 +10:00
8ee6710e9b Merge pull request #889 from EBEN4REAL/custom-tag-colors
Custom tag colors
2023-08-23 21:03:46 -07:00
14fc78eaaf Switched to crypto.subtle, cleaned up code, added types & properly cleared sensitive data from memory (even if error) 2023-08-24 14:01:26 +10:00
9fa28f5b5e Fix: added empty string as default for tag color and added regex to resolve issue with multiple spacing in tag names. 2023-08-24 03:59:49 +01:00
368855a44e >>> yup for email & url validation, fixed minor err in error msgs 2023-08-24 12:59:24 +10:00
ae375916e8 Fix: added nullable check for adding tag color in project settings 2023-08-24 03:39:46 +01:00
21f1648998 Merge pull request #887 from Infisical/signup-secret-tagging
Update signup secret distinction/tagging for better telemetry
2023-08-23 19:23:44 -07:00
88695a2f8c Merge pull request #884 from monto7926/sortable-secrets-overview
feat: make secrets overview sortable
2023-08-23 17:47:34 -07:00
77114e02cf fixed the import linting issues 2023-08-23 17:42:29 -07:00
3ac1795a5b Update kubernetes-helm.mdx 2023-08-23 17:42:07 -04:00
8d6f59b253 up infisical chart version 2023-08-23 17:15:30 -04:00
7fd77b14ff print default connection string in helm 2023-08-23 17:14:09 -04:00
8d3d7d98e3 chore: updated style for tag color label 2023-08-23 18:50:24 +01:00
6cac879ed0 chore: removed console log 2023-08-23 16:46:06 +01:00
ac66834daa chore: fixed error with typings 2023-08-23 16:36:48 +01:00
0616f24923 Merge pull request #866 from Killian-Smith/email-case-sensitive
fix: normalize email when inviting memebers and logging in.
2023-08-23 18:08:28 +07:00
4e1abc6eba Add login email lowercasing to backend 2023-08-23 18:02:18 +07:00
8f57377130 Merge remote-tracking branch 'origin' into email-case-sensitive 2023-08-23 17:50:46 +07:00
2d7c7f075e Remove metadata from SecretVersion schema 2023-08-23 17:47:25 +07:00
c342b22d49 Fix telemetry issue for signup secrets 2023-08-23 17:37:01 +07:00
b8120f7512 Merge pull request #886 from Infisical/audit-log-paywall
Add paywall to Audit Logs V2
2023-08-23 17:00:27 +07:00
ca18883bd3 Add paywall for audit logs v2 2023-08-23 16:55:07 +07:00
8b381b2b80 Checkpoint add metadata to secret and secret version data structure 2023-08-23 16:30:42 +07:00
6bcf5cb54c override secrets before expand 2023-08-22 23:37:32 -04:00
51b425dceb swap out v2 login 2023-08-22 23:37:32 -04:00
7ec00475c6 +maxRetryAttempts, padding & safer error handling. Improved readability & comments. 2023-08-23 12:59:00 +10:00
84840bddb5 Merge branch 'main' of https://github.com/Infisical/infisical 2023-08-22 15:10:30 -07:00
93640c9d69 added tooltips to the sercret overview 2023-08-22 15:10:18 -07:00
ec856f0bcc remove return from integration loop 2023-08-22 21:18:18 +00:00
3e46bec6f7 add simple api to trigger integration sync 2023-08-22 14:55:08 -04:00
25fc508d5e Fixed spelling 2023-08-23 02:56:03 +10:00
ea262da505 Added check that password is not an email address 2023-08-23 02:14:22 +10:00
954806d950 chore: code cleanup 2023-08-22 17:59:11 +02:00
2960f86647 Fix comments explaining "international" password requirements 2023-08-23 01:41:37 +10:00
b2888272f2 Added password criterion support for multiple languages and emojis 2023-08-23 01:27:30 +10:00
d6d3302659 feat: make secrets overview sortable 2023-08-22 17:21:21 +02:00
e5c87442e5 Changed to use ES2018 rather than load scripts 2023-08-23 01:04:52 +10:00
be08417c8b internationalize password requirements 2023-08-23 00:48:45 +10:00
61e44e152c optimised import 2023-08-22 23:47:33 +10:00
52c4f64655 Removed log and fixed comments 2023-08-22 23:36:24 +10:00
81743d55ab fix infisical radar app name 2023-08-22 09:35:31 -04:00
3e36adcf5c Removed all references to commonPasswords & the data file. This api route can be deprecated in favor of the client-side secure call to the haveIBeenPwnd password API. Further the datafile contains no passwords that meet the minimum password criteria. 2023-08-22 23:30:24 +10:00
1f60a3d73e fixed more error handling for password checks & translations 2023-08-22 22:42:02 +10:00
00089a6bba Added breached pwd error translations 2023-08-22 20:57:12 +10:00
026ea29847 further fixes to password check logic 2023-08-22 20:42:07 +10:00
1242d88acb Fixed breached pwd error messages 2023-08-22 20:20:54 +10:00
f47a119474 fixed breached pwd error messages 2023-08-22 20:20:13 +10:00
0b359cd797 Made breached pwd API comments clearer 2023-08-22 19:45:35 +10:00
c5ae402787 Added comments to explain breach passwords API 2023-08-22 18:14:03 +10:00
e288402ec4 Properly added pwndpasswords API to CSP 2023-08-22 17:58:10 +10:00
196beb8355 removed logs & added pwndpasswords.com api to CSP 2023-08-22 17:50:43 +10:00
d6222d5cee attempt to fix crypto.subtle issue 2023-08-22 17:33:35 +10:00
e855d4a0ba added types for crypto 2023-08-22 17:26:00 +10:00
20f34b4764 removed async in crypto.subtle 2023-08-22 17:14:18 +10:00
0eb21919fb Password breach check 2023-08-22 16:49:17 +10:00
fbeb210965 add to pwd length issue 2023-08-22 15:34:45 +10:00
0d1aa713ea added translations for error messges (used Google translate) 2023-08-22 14:57:02 +10:00
9a1b453c86 Feat: added tag color widgt and changed tag popover design 2023-08-22 05:12:23 +01:00
534d96ffb6 Set max password length (100 chars) to help prevent DDOS attack 2023-08-22 14:05:00 +10:00
5b342409e3 Merge pull request #815 from Infisical/snyk-fix-477e109149f5e5a943a435c5bf8814b7
[Snyk] Security upgrade winston-loki from 6.0.6 to 6.0.7
2023-08-21 16:02:02 -04:00
a9f54009b8 Merge pull request #848 from narindraditantyo/fix/rootless-frontend-image
fix: frontend image displaying some errors due to sed write permission
2023-08-21 15:54:29 -04:00
82947e183c Merge pull request #851 from sreehari2003/main
fix: form not submitting on keyboard enter
2023-08-21 15:53:15 -04:00
eb7ef2196a Merge pull request #872 from iamunnip/blogs
added blog link for setting up infisical in developement cluster
2023-08-21 14:09:18 -04:00
ad3801ce36 Merge pull request #882 from akhilmhdh/feat/integration-var-not-found
fix(integration): instead of throwing error console and return empty string on interpolation
2023-08-21 13:51:16 -04:00
b7aac1a465 fix(integration): instead of throwing error console and return empty string on interpolation 2023-08-21 20:06:24 +05:30
e28ced8eed Provide default path for logging dashboard secrets event 2023-08-21 18:27:18 +07:00
4a95f936ea Correct enable blind-indexing web ui rendering condition 2023-08-21 17:27:32 +07:00
85a39c60bb Fix query condition on delete secret v3 2023-08-21 16:51:31 +07:00
66ea3ba172 feat: added custom design for tags 2023-08-20 10:02:40 +01:00
01d91c0dc7 update helm version 2023-08-19 17:19:42 -04:00
dedd27a781 remove unsed redis template 2023-08-19 17:19:07 -04:00
57a6d1fff6 fix syntax error in helm chart 2023-08-19 14:47:46 -04:00
554f0c79a4 update redis doc 2023-08-19 14:31:28 -04:00
2af88d4c99 Merge pull request #843 from Infisical/add-bull-queue
add bull queue
2023-08-19 14:13:34 -04:00
fc8b567352 fix syntax error in /api/status 2023-08-19 14:03:02 -04:00
ec234e198a Merge branch 'main' into add-bull-queue 2023-08-19 13:46:26 -04:00
6e1cc12e3a update redis banner text 2023-08-19 13:43:01 -04:00
1b4b7a967b fix docs typos 2023-08-19 13:42:33 -04:00
e47d6b7f2f added blog link for setting up infisical in developement cluster 2023-08-19 08:59:58 +05:30
45a13d06b5 add redis why docs & update redis notice 2023-08-18 21:20:20 -04:00
4a48c088df Merge pull request #868 from daninge98/custom-environment-sorting
Adds user customizable environment ordering
2023-08-18 17:05:37 -07:00
2b65f65063 Rename things and fix bug in error checking 2023-08-18 17:33:59 -04:00
065e150847 update status api 2023-08-18 09:42:33 -04:00
ab72eb1178 added scrollbar to modal 2023-08-17 14:03:56 -07:00
816099a8b4 Merge pull request #869 from Infisical/bring-back-file-vault
Bring back file vault
2023-08-17 15:28:37 -04:00
b5f672cc61 update vault docs 2023-08-17 15:20:17 -04:00
ddc7be18eb link to forked keyring and bring back vault command 2023-08-17 15:07:12 -04:00
c0ce92cf3d Formattting fix 2023-08-16 17:42:39 -04:00
0073fe459e Fix typo 2023-08-16 17:37:41 -04:00
a7f52a9298 Small formatting fixes 2023-08-16 17:36:07 -04:00
29c0d8ab57 Enable users to change the ordering of environments 2023-08-16 17:30:50 -04:00
d7b26cbf04 Fix Select placeholder in audit logs v2 2023-08-17 01:51:44 +07:00
767abe51ef Fix lint errors 2023-08-17 01:34:24 +07:00
5ac1816392 Correct SSO linking case and uncomment audit logs v2 2023-08-17 01:24:41 +07:00
c5b1e7298e Merge pull request #867 from daninge98/improve-tag-colors-and-sorting
Improve tag colors and sorting on dashboard page
2023-08-15 22:01:43 -07:00
3436e6be0e Small formatting changes 2023-08-15 18:46:17 -04:00
b000a78f74 Change tag color assignments and sorting 2023-08-15 18:39:15 -04:00
cb42db3de4 Normalize email when inviting memebers and logging in. 2023-08-15 15:57:27 +01:00
11bb0d648f fixed capitalization 2023-08-14 18:36:44 -07:00
90517258a2 added redis note 2023-08-14 18:30:40 -07:00
d78b37c632 add redis docs 2023-08-14 16:25:16 -04:00
4a6fc9e84f remove console.log and add redis to /status api 2023-08-14 16:24:43 -04:00
8030104c02 update helm read me with redis config details 2023-08-14 15:02:22 -04:00
3825269cbb Merge pull request #857 from Infisical/signup-secrets-fix
added a check for signup events
2023-08-14 11:22:16 +07:00
baa907dbb6 Update source to metadata.source 2023-08-14 11:09:22 +07:00
83465dff2d added a check for signup events 2023-08-13 18:26:41 -07:00
67a8211cb0 Merge pull request #853 from Infisical/linking-sso
Add linking for existing users without SSO enabled logging in via SSO
2023-08-13 22:57:13 +07:00
bc108a82b6 Add SSO linking feature for existing users 2023-08-13 22:47:29 +07:00
05be5910d0 Update changelog 2023-08-13 17:18:18 +07:00
2341ec0e11 Merge pull request #829 from sheensantoscapadngan/feature/enable-users-to-select-multi-auth-methods
Feature: enable users to select multi auth methods (backward compatible)
2023-08-13 17:15:39 +07:00
9652d534b6 fix: moved handler to form submission 2023-08-13 14:00:30 +05:30
dd8f55804c finalized sso controller 2023-08-13 16:18:11 +08:00
95d25b114e Fix incorrect field in validateProviderAuthToken 2023-08-13 14:08:26 +07:00
c0f3aecad3 Fix lint issues 2023-08-13 11:12:07 +07:00
f650cd3925 fix: form not submitting on keyboard enter 2023-08-13 00:54:22 +05:30
8a514e329f fix: frontend image displaying some errors due to sed write permission 2023-08-12 21:53:12 +07:00
dbd55441f2 Update login with multiple auth methods to toggle button and logic 2023-08-12 13:53:55 +07:00
01e613301a console.log queue errors 2023-08-11 19:47:15 -04:00
de7bd27b4b Update README.md 2023-08-11 14:54:34 -07:00
a4cdd14014 Merge pull request #846 from narindraditantyo/fix/rootless-backend-image
fix: backend image failed to start due to npm cache permission
2023-08-11 16:31:11 -04:00
632c78f401 fix: backend image failed to start due to npm cache permission 2023-08-11 23:34:41 +07:00
051f4501e8 Merge pull request #837 from akhilmhdh/fix/supbase-del-res-secret
fix: supabase failed integration due to res secret deletion
2023-08-11 10:38:15 -04:00
69605a1a54 Fast forward 2023-08-11 12:46:30 +07:00
e47912edd7 Merge pull request #838 from Infisical/deprecation
Cleaning & deprecating parts of code
2023-08-11 12:10:19 +07:00
a4edf6bd0c Remove remaining SecurityClient auth calls in favor of hooks, keep RouteGuard 2023-08-11 11:27:33 +07:00
b11cd29943 close all queues 2023-08-10 19:13:09 -04:00
395b51c265 Merge pull request #844 from hahnbeelee/main
cursor-pointer for Explore button
2023-08-10 17:54:17 -04:00
27f56be466 cursor-pointer fir Explore button 2023-08-10 14:33:37 -07:00
dfe95ac773 add bull queue 2023-08-10 17:22:20 -04:00
2dba7847b6 Convert all SecurityClient API calls to hooks except auth 2023-08-10 17:19:23 +07:00
78802409bd Move all integration queries/mutations to hooks 2023-08-10 14:15:24 +07:00
9963724a6a Continue removing unused frontend components/logic, improve querying in select pages 2023-08-10 12:18:17 +07:00
b49ef9efc9 minor frontend UX fixes 2023-08-09 12:03:00 -07:00
a31ffe9617 Update README.md 2023-08-09 10:12:10 -07:00
18beed7540 Merge pull request #839 from Infisical/permissions-audit-log
Add audit logs for workspace user role and read/write permission changes
2023-08-09 22:02:48 +07:00
0a538ac1a7 Add GitHub SSO to changelog 2023-08-09 22:02:19 +07:00
a75ad5ef26 Add logs for workspace user role and read/write permission changes 2023-08-09 21:54:56 +07:00
b47f61f1ad Delete more deprecated frontend calls 2023-08-09 17:55:57 +07:00
2a1665a2c3 Begin marking endpoints for deprecation, clean unused frontend code 2023-08-09 16:45:52 +07:00
e993bd048e fix: supabase failed integration due to res secret deletion 2023-08-09 14:41:42 +05:30
11833ccf0f Note endpoints to deprecate 2023-08-09 11:32:57 +07:00
37d52432d0 Revert run as node backend due to slow build times 2023-08-08 14:11:06 -04:00
04b7e04d98 run full backend docker image as node user 2023-08-08 14:02:39 -04:00
57a3384f32 Merge pull request #834 from akhilmhdh/fix/copy-sec-not-populating
fix: secret path / with a new env causes secret list to be empty
2023-08-08 13:18:41 -04:00
c813c91aec Merge pull request #825 from akhilmhdh/feat/service-token-folder-api
feat: implemented service token support for folder and secret import api
2023-08-08 13:17:54 -04:00
91947df5f6 feat: refactored getPathfromId to folder service 2023-08-08 22:33:59 +05:30
8330890087 feat: implemented service token support for folder and secret import api 2023-08-08 22:20:36 +05:30
221e601173 Correct user agent function name in test 2023-08-08 22:57:38 +07:00
28c24fc8c1 Merge pull request #828 from Infisical/audit-log-revamp
Audit Log V2
2023-08-08 22:45:54 +07:00
a9389643b8 Log GET secrets v2 2023-08-08 22:42:13 +07:00
58854e6b81 Fix merge conflicts 2023-08-08 22:07:22 +07:00
7ae859e9ae Update secret imports audit log v2 2023-08-08 22:04:23 +07:00
ff6e07bdcf minor style changes 2023-08-08 11:03:28 -04:00
fc9393b77f run backend docker commands as node instead of root 2023-08-08 09:48:27 -04:00
0cad823267 Add dashboard-specific secret and secret import audit logs, re-touch audit logs v2 UI 2023-08-08 19:03:24 +07:00
97a0728f02 fix: secret path / with a new env causes secret list to be empty 2023-08-08 12:18:21 +05:30
6cb8cf53f8 Add date filter and pagination component to audit log v2 2023-08-08 12:52:34 +07:00
1ac607b42e Merge remote-tracking branch 'origin' into audit-log-revamp 2023-08-08 10:00:47 +07:00
ec21e35f8c Merge pull request #831 from akhilmhdh/feat/pagination-datepicker
Feat/pagination datepicker
2023-08-08 09:57:41 +07:00
2591161272 Add more audit log events 2023-08-08 09:50:49 +07:00
be86e4176c feat(ui): added datepicker component 2023-08-07 16:22:15 +05:30
2067c021ed feat(ui): added pagination component 2023-08-07 16:21:51 +05:30
648968c453 Run linter 2023-08-07 11:25:06 +07:00
dc3f2c78c1 resolved lint issue 2023-08-06 22:38:24 +08:00
b4dbdbabac used const 2023-08-06 22:33:53 +08:00
681255187f modified initialize org to check for auth providers 2023-08-06 22:30:02 +08:00
bde30049bc ensured backwards compatibility 2023-08-06 22:29:31 +08:00
0a140f5333 updated implementation of user update after sso change 2023-08-06 22:04:39 +08:00
3a9bf5409b finalization of create token logic 2023-08-06 21:46:20 +08:00
04fdccc45d modified backend controllers to support new auth providers 2023-08-06 19:45:27 +08:00
5604232aea added user controller and modified auth method page 2023-08-06 19:09:38 +08:00
373dfff8e0 Remove print statement 2023-08-05 17:06:49 +07:00
b9ce448bed Fix merge conflicts 2023-08-05 16:57:21 +07:00
142fcf0a01 Finish preliminary v2 audit logs 2023-08-05 16:55:06 +07:00
49bcd8839f move github scanning service to ee 2023-08-04 18:15:07 -04:00
d5f6e20c78 Merge pull request #822
fix: added dirty flag to set fn in dropzone paste secret
2023-08-04 17:49:19 -04:00
00030f2231 move secret scanning to ee 2023-08-04 17:42:29 -04:00
24d23e89d0 add exit code to run command 2023-08-04 12:10:46 -04:00
3fe592686a add clarity to CLI docs 2023-08-04 12:01:42 -04:00
9cba0970be Merge pull request #827 from Infisical/maidul98-patch-2
Update usage.mdx
2023-08-04 11:11:48 -04:00
8b50150ec8 Update usage.mdx 2023-08-04 11:11:17 -04:00
5af1eb508c disable trust IP 2023-08-04 10:49:05 -04:00
9d57b1db87 fix: added dirty flag to set fn in dropzone paste secret 2023-08-04 11:46:23 +05:30
9a5329300c Merge pull request #817 from akhilmhdh/feat/import-sec-dashboard
feat: added copy secret feature in dashboard
2023-08-03 18:09:56 -04:00
b03c346985 nit: text update 2023-08-03 18:03:35 -04:00
84efc3de46 pushed out soem changes 2023-08-03 13:30:22 -04:00
2ff3818ecb feat: made changes as discussed with team on dropzone 2023-08-03 22:23:14 +05:30
6fbcbc4807 Add signup with GitHub option 2023-08-03 16:05:14 +07:00
9048988e2f Merge pull request #818 from JunedKhan101/feature-github-signin
initial-setup for github signin
2023-08-03 15:44:15 +07:00
98cfd72928 Merge remote-tracking branch 'origin' into feature-github-signin 2023-08-03 15:39:45 +07:00
2293abfc80 Revise and finish login with GitHub 2023-08-03 15:34:02 +07:00
817a783ec2 feat: updated text and added select all in copy secrets for dashboard 2023-08-03 13:15:28 +05:30
9006212ab5 Merge pull request #819 from Infisical/view-licenses
Add tab to view enterprise license keys in usage and billing section
2023-08-03 11:59:44 +07:00
1627674c2a Merge remote-tracking branch 'origin' into view-licenses 2023-08-03 11:51:12 +07:00
bc65bf1238 Add section for users to view purchased enterprise license keys in organization usage and billing section 2023-08-03 11:48:10 +07:00
3990b6dc49 fixed the autocapitalization ability 2023-08-02 20:01:06 -07:00
a3b8de2e84 Update mint.json 2023-08-02 18:37:59 -07:00
b5bffdbcac Merge pull request #813 from akhilmhdh/feat/sec-exp-ingtegration
Secret expansion and import in integrations
2023-08-02 19:16:03 -04:00
23e40e523a highlight infisical version in k8 docs 2023-08-02 17:41:45 -04:00
d1749deff0 enable checkIPAllowlist 2023-08-02 12:41:04 -04:00
960aceed29 initial-setup for github signin 2023-08-02 21:28:35 +05:30
bd8397bda7 add status code and url to CallGetAccessibleEnvironments 2023-08-02 11:23:22 -04:00
9dac06744b delay cli update notification 2023-08-02 10:57:04 -04:00
bd80c2ccc3 swap out keyring package 2023-08-02 10:11:23 -04:00
466dadc611 feat: added pull secret feature in dashboard with env json parsing and multiline parsing 2023-08-02 16:15:04 +05:30
cc5ca30057 feat: updated multi line format on integrations sync secrets 2023-08-02 16:12:58 +05:30
62fa59619b Add community call link in README 2023-08-02 12:42:32 +07:00
7accaeffcf Revert "keyring swap, better error messages/warnings, delay upgrade notif"
This reverts commit 7f69a3b23f873cd094f40a2a58f6c3fd7ca18832.
2023-08-01 22:55:19 -04:00
7f69a3b23f keyring swap, better error messages/warnings, delay upgrade notif 2023-08-01 21:12:13 -04:00
285a6d633a return instance of ServiceTokenData instead of object 2023-08-01 14:40:08 -04:00
12b71bcf67 Merge pull request #816 from jamesoyanna/fix-blog-404-issue
fix: Blog Section in Doc API Reference Redirects to 404
2023-08-01 13:33:33 -04:00
6c0be52ffa upgrade major version of mongoose from v6 to v7 2023-08-01 13:27:01 -04:00
9df51424a2 upgrade major version of mongoose from v6 to v7 2023-08-01 13:24:38 -04:00
bb466dbe1c fix: backend/package.json & backend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-PROTOBUFJS-5756498
2023-08-01 15:50:51 +00:00
531938a3f1 fix: Blog Section in Doc API Reference Redirects to 404 2023-08-01 16:28:32 +01:00
941a8699b5 Merge pull request #806 from atimapreandrew/teamcity-integration
Teamcity integration
2023-08-01 19:04:20 +07:00
6e42da9063 Optimize TeamCity integration 2023-08-01 18:59:27 +07:00
b1981df8f0 chore: resolved merge conflict 2023-08-01 15:29:34 +05:30
086652a89f fix: resolved infinite recursion cases 2023-08-01 15:24:18 +05:30
6574b6489f fix: added support for secret import and expansion in integrations 2023-08-01 15:24:18 +05:30
69903c0d5c Merge pull request #799 from afrieirham/fix/digital-ocean-sync
fix: digital ocean reset settings on sync
2023-08-01 15:24:57 +07:00
8ff33a4e63 Fix sentence in security mechanics 2023-08-01 14:46:50 +07:00
1d71864092 hide sign up with inviteOnlySignup=true 2023-07-31 23:54:35 -04:00
4b1a27b301 Update changelog 2023-08-01 10:11:22 +07:00
b78150e78d Updated membership logic for SAML auth 2023-07-31 17:36:42 +07:00
a0f08c73af Added images for TeamCity docs 2023-07-30 13:13:52 +01:00
59ebe0c22e Merge pull request #805 from Infisical/jumpcloud-saml
Add JumpCloud SAML Support
2023-07-30 14:36:10 +07:00
6729caeb75 Add JumpCloud SAML 2023-07-30 14:29:47 +07:00
3543a15c09 TeamCity integration 2023-07-30 01:57:39 +01:00
33e0f13eea Added TeamCity integration docs 2023-07-30 00:26:06 +01:00
e9cff4fe69 TeamCity integration 2023-07-30 00:00:24 +01:00
26867f7328 Update overview.mdx 2023-07-29 15:16:11 -07:00
233459d063 Merge pull request #804 from Infisical/jumpcloud-saml
Optimize SAML SSO configuration flow and add documentation for Azure AD SAML
2023-07-29 15:01:41 +07:00
ba6355e4d2 Fix lint errors 2023-07-29 14:58:13 +07:00
e961a30937 Optimize SAML SSO configuration flow, add docs for Azure AD SAML 2023-07-29 14:39:06 +07:00
53ff420304 Merge pull request #802 from akhilmhdh/feat/org-overview-loading
feat: added loading state for org overview page
2023-07-28 15:13:51 -04:00
196a613f16 Merge pull request #790 from subh-cs/subh-cs/better-logs-k8-operator
Better log for k8-operator
2023-07-28 15:11:40 -04:00
cc4b749ce8 Revise SAML flow, update Okta SAML docs 2023-07-29 01:57:07 +07:00
8cc5f2ef43 typo in cli usage.mdx 2023-07-28 13:26:35 -04:00
06bc02c392 feat: added loading state and show empty state only when loading for org overview page 2023-07-28 21:33:31 +05:30
3682c4d044 Merge pull request #800 from akhilmhdh/fix/style-overview-fixes
feat: fixed padding, added progress bar for routing, added sticky hea…
2023-07-28 09:51:33 -04:00
52892c26e5 feat: fixed padding, added progress bar for routing, added sticky header for overview 2023-07-28 16:55:22 +05:30
5ce67bf750 fix: send current app settings with env sync update 2023-07-28 18:00:46 +08:00
ed2cf68935 Merge remote-tracking branch 'origin' into jumpcloud-saml 2023-07-28 14:52:28 +07:00
386bc09d49 Update Okta SSO image convention 2023-07-28 14:52:07 +07:00
353c6e9166 Merge pull request #798 from Infisical/windmill-docs
Add docs for Windmill integration
2023-07-28 13:26:20 +07:00
1f69467207 Add docs for Windmill integration 2023-07-28 13:24:33 +07:00
5ab218f1f8 fixed parsing .env with : 2023-07-27 19:40:39 -07:00
e1b25aaa54 fixed the padding issue for the secret raw 2023-07-27 19:03:02 -07:00
9193e7ef58 fix styling issues with secret rows 2023-07-27 15:50:42 -07:00
3f998296fe ip table fix 2023-07-27 13:33:31 -07:00
6f7601f2c4 Merge pull request #793 from akhilmhdh/feat/new-overview-page
Feat/new overview page
2023-07-27 15:42:34 -04:00
b7c7544baf minor style changes 2023-07-27 12:37:18 -07:00
4b7ae2477a Merge pull request #765 from sunilk4u/feat/windmill-integration
Feature: Windmill.dev cloud Integeration
2023-07-28 02:11:27 +07:00
e548883bba Fix lint errors, merge conflicts 2023-07-28 02:02:26 +07:00
a7ece1830e Revise Windmill integration 2023-07-28 01:30:28 +07:00
6502d232c9 Start Azure AD SAML docs 2023-07-27 23:48:53 +07:00
f31e8ddfe9 feat: added width for expandable table and secret missing count ui fix 2023-07-27 20:57:38 +05:30
7bbbdcc58b feat: implemented new overview page with improvement in dashboard 2023-07-27 16:36:34 +05:30
bca14dd5c4 feat: added new secret input component and updated toolbar key special prop to innerKey 2023-07-27 16:36:34 +05:30
b6b3c8a736 fix: resolved v2 secret update bug and object returning in import secret empty 2023-07-27 16:29:43 +05:30
d458bd7948 Merge branch 'feat/northflank-integration' 2023-07-27 15:18:28 +07:00
239989ceab Update contributors README 2023-07-27 15:17:15 +07:00
7ff13242c0 Add docs for Northflank 2023-07-27 15:16:11 +07:00
7db8555b65 Merge pull request #788 from ChukwunonsoFrank/feat/northflank-integration
Feature: Northflank integration
2023-07-27 15:15:36 +07:00
980a578bd5 Revise Northflank integration 2023-07-27 14:52:52 +07:00
adb27bb729 fix: allow apps which have write access 2023-07-27 13:11:48 +05:30
d89d360880 Merge pull request #792 from Infisical/fix-ip-whitelisting
Update IP allowlist implementation
2023-07-27 11:47:56 +07:00
8ed5dbb26a Add default IPV6 CIDR for creating workspace 2023-07-27 11:23:57 +07:00
221a43e8a4 Update IP allowlist implementation 2023-07-27 11:18:36 +07:00
e8a2575f7e logging workspaceId, tokenName from k8-operator 2023-07-27 09:10:08 +05:30
41c1828324 roll forward: disable IP white listing 2023-07-26 20:50:53 -04:00
c2c8cf90b7 Merge branch 'main' of https://github.com/Infisical/infisical 2023-07-26 14:03:47 -07:00
00b4d6bd45 changed the icon 2023-07-26 14:03:37 -07:00
f5a6270d2a add workspace auth for multi env/glob request 2023-07-26 16:50:35 -04:00
bc9d6253be change isDisabled criteria for Create Integration button 2023-07-26 21:19:02 +01:00
a5b37c80ad chore: resolve merge conflicts 2023-07-26 20:39:51 +01:00
7b1a4fa8e4 change regexp to accept deeper level paths 2023-07-27 00:48:17 +05:30
7457f573e9 add dash and underscores for secret pattern test 2023-07-26 23:43:44 +05:30
d67e96507a fix:unauthorized response for app name 2023-07-26 23:14:42 +05:30
46545c1462 add secretGroup to integrationController.ts 2023-07-26 18:19:54 +01:00
8331cd4de8 Merge pull request #761 from atimapreandrew/terraform-cloud-integration
Terraform cloud integration
2023-07-26 23:16:51 +07:00
3447074eb5 Fix merge conflicts 2023-07-26 23:13:33 +07:00
5a708ee931 Optimize Terraform Cloud sync function 2023-07-26 23:10:38 +07:00
9913b2fb6c Initialize TrustedIP upon creating a new workspace 2023-07-26 22:20:51 +07:00
2c021f852f Update filter for trusted IPs backfill 2023-07-26 21:15:07 +07:00
8dbc894ce9 Replace insertMany operation with upsert for backfilling trusted ips 2023-07-26 20:45:58 +07:00
511904605f Merge pull request #786 from Infisical/debug-integrations
Fix PATCH IP whitelist behavior and breaking integrations due to incorrect project id in local storage
2023-07-26 17:55:24 +07:00
7ae6d1610f Fix IP whitelist PATCH endpoint, update localStorage project id to reflect navigated to project 2023-07-26 17:46:33 +07:00
7da6d72f13 Remove save call from backfilling trusted ips 2023-07-26 16:18:13 +07:00
ad33356994 Remove required comment for trusted IP schema 2023-07-26 15:29:43 +07:00
cfa2461479 Merge pull request #785 from Infisical/network-access
Add support for IP allowlisting / trusted IPs
2023-07-26 15:11:04 +07:00
bf08bfacb5 Fix lint errors 2023-07-26 15:06:18 +07:00
cf77820059 Merge remote-tracking branch 'origin' into network-access 2023-07-26 14:55:12 +07:00
1ca90f56b8 Add docs for IP allowlisting 2023-07-26 14:51:25 +07:00
5899d7aee9 Complete trusted IPs feature 2023-07-26 13:34:56 +07:00
b565194c43 create versions for brew releases 2023-07-25 15:39:29 -04:00
86e04577c9 print exec error messages as is 2023-07-25 14:17:31 -04:00
f4b3cafc5b Added Terraform Cloud integration docs 2023-07-25 16:51:53 +01:00
18aad7d520 Terraform Cloud integration 2023-07-25 15:25:11 +01:00
54c79012db fix the org-members link 2023-07-25 07:01:27 -07:00
4b720bf940 Update kubernetes.mdx 2023-07-24 18:13:35 -04:00
993866bb8b Update secret-reference.mdx 2023-07-24 17:18:07 -04:00
8c39fa2438 add conditional imports to raw api 2023-07-24 15:01:31 -04:00
7bccfaefac Merge pull request #784 from akhilmhdh/fix/import-delete
fix: resolved secret import delete and include_import response control
2023-07-24 11:47:02 -04:00
e2b666345b fix: resolved secret import delete and include_import response control 2023-07-24 20:53:59 +05:30
90910819a3 Merge pull request #778 from afrieirham/docs/running-docs-locally
docs: add running infisical docs locally guide
2023-07-24 09:00:46 -04:00
8b070484dd docs: add running infisical docs locally 2023-07-24 20:36:42 +08:00
a764087c83 Merge pull request #782 from Infisical/improve-security-docs
Add section on service token best practices
2023-07-24 18:14:26 +07:00
27d5fa5aa0 Add section on service token best practices 2023-07-24 18:10:37 +07:00
2e7705999c Updated changelog and contributors in README 2023-07-24 12:56:02 +07:00
428bf8e252 Merge branch 'main' of https://github.com/Infisical/infisical 2023-07-23 13:59:42 -07:00
264740d84d style updates 2023-07-23 13:59:25 -07:00
723bcd4d83 Update react.mdx 2023-07-23 15:44:54 -04:00
9ed516ccb6 Uncomment Google SSO for signup 2023-07-24 02:35:51 +07:00
067ade94c8 Merge branch 'main' of https://github.com/Infisical/infisical 2023-07-24 01:54:09 +07:00
446edb6ed9 Add CLI support for SAML SSO 2023-07-24 01:53:56 +07:00
896529b7c6 auto scope raw secrets GET with service token 2023-07-23 12:31:03 -04:00
5c836d1c10 Merge pull request #779 from afrieirham/integration/digital-ocean-app-platform
Digital Ocean App Platform Integration
2023-07-23 23:01:35 +07:00
409d46aa10 Fix merge conflicts 2023-07-23 22:55:18 +07:00
682c63bc2a Fix DigitalOcean getApps case where there are no apps 2023-07-23 22:42:41 +07:00
1419371588 Merge pull request #776 from afrieirham/integration/cloud66
Cloud 66 integration
2023-07-23 22:19:23 +07:00
77fdb6307c Optimize Cloud66 integration sync function 2023-07-23 22:16:27 +07:00
c61bba2b6b docs: add digital ocean app platform integration guide 2023-07-23 22:39:08 +08:00
2dc0563042 view: fix integration name only show 3 words 2023-07-23 22:02:59 +08:00
b5fb2ef354 feat: DO app platform integration 2023-07-23 22:01:48 +08:00
dc01758946 Update SAML Okta docs screenshots 2023-07-23 16:59:08 +07:00
1f8683f59e Merge pull request #777 from Infisical/saml-docs
Add docs for Okta SAML 2.0 SSO
2023-07-23 16:49:47 +07:00
a5273cb86f Add docs for Okta SAML 2.0 SSO 2023-07-23 16:47:43 +07:00
d48b5157d4 docs: add cloud 66 integration guide 2023-07-23 17:39:29 +08:00
94a23bfa23 feat: add cloud 66 integration 2023-07-23 16:36:26 +08:00
fcdfa424bc Restrict changing user auth methods if SAML SSO is enforced 2023-07-23 15:19:17 +07:00
3fba1b3ff7 Merge pull request #774 from Infisical/saml-sso-edge-cases
Block inviting members to organization if SAML SSO is configured
2023-07-23 13:27:30 +07:00
953eed70b2 Add back attribution source for non-SAML SSO case 2023-07-23 13:24:12 +07:00
39ba795604 Block inviting members to organization if SAML SSO is configured 2023-07-23 13:05:37 +07:00
5b36227321 Merge pull request #773 from Infisical/debug-google-sso
Initialize organization bot upon creating organization
2023-07-23 12:07:45 +07:00
70d04be978 Initialize organization bot upon creating organization 2023-07-23 12:03:39 +07:00
c2be6674b1 chore: resolve merge conflicts 2023-07-22 11:29:40 +01:00
565f234921 Merge pull request #772 from Infisical/switch-to-google-sso
Add user support for changing authentication methods
2023-07-22 12:38:22 +07:00
ab43e32982 Add user support for changing auth methods 2023-07-22 12:33:57 +07:00
be677fd6c2 disable token error 2023-07-21 18:41:32 -04:00
3d93c6a995 add sentry error to integ 2023-07-21 17:45:27 -04:00
edb201e11f comment out unused import 2023-07-21 17:33:46 -04:00
1807b3e029 add logs for integration and comment out google sso 2023-07-21 17:29:56 -04:00
c02c8e67d3 Merge branch 'main' of https://github.com/Infisical/infisical 2023-07-21 23:54:03 +07:00
d4c5be5f48 Update file casing 2023-07-21 23:53:50 +07:00
5f33c9a389 Update file casing 2023-07-21 23:53:16 +07:00
c9acb22261 Merge pull request #770 from Infisical/docs
Add/revise docs for Codefresh and Bitbucket integrations
2023-07-21 23:44:57 +07:00
33f0510995 Add docs for Codefresh integration, revise docs for Bitbucket integration 2023-07-21 23:41:04 +07:00
25b239a18b Merge pull request #755 from zwkee/integration/bitbucket
BitBucket Integration
2023-07-21 20:55:42 +07:00
504e0f6dc3 Fix lint issues backend 2023-07-21 20:52:35 +07:00
f450be3a00 Fix merge conflicts 2023-07-21 20:49:41 +07:00
d9f6c27e4d Update Bitbucket sync function 2023-07-21 20:16:39 +07:00
9cef35e9e6 Merge pull request #769 from Infisical/saml
Add Google SSO and SAML SSO (Okta)
2023-07-21 18:02:10 +07:00
2621ccdcf1 Add descriptions for SSO endpoints 2023-07-21 17:59:15 +07:00
75e90201c0 Lint and move redirectSSO into controller 2023-07-21 17:54:09 +07:00
fd3cf70e13 Add Google SSO 2023-07-21 17:48:36 +07:00
44108621b4 Run linter, fix import error 2023-07-21 15:01:28 +07:00
5ee65359bf Fix merge conflicts 2023-07-21 14:37:13 +07:00
241dceb845 Remove bodyparser and audit fix deps 2023-07-21 13:39:07 +07:00
af650ef4c7 patch env delete bug 2023-07-20 20:03:01 -04:00
817ddd228c Update overview.mdx 2023-07-20 19:15:58 -04:00
15d81233b4 update docs overvew 2023-07-20 18:25:54 -04:00
705b1833d0 update CLI usage and docs for pinning docker 2023-07-20 18:18:43 -04:00
beb8d2634a add docs to pin cli 2023-07-20 17:59:59 -04:00
fb3ceb4581 Revamp docker docs 2023-07-20 17:28:33 -04:00
2df33dc84f Merge pull request #764 from akhilmhdh/fix/include-optional
made include_imports optional in raw secrets fetch
2023-07-20 09:54:47 -04:00
c62504d658 correct codefresh image file name 2023-07-20 19:21:04 +05:30
ce08512ab5 Merge remote-tracking branch 'upstream/main' into feat/windmill-integration 2023-07-20 19:20:38 +05:30
8abe7c7f99 add secretGroup attribute to model definition 2023-07-20 12:58:07 +01:00
043133444d fix: made include_imports optional in raw secrets fetch 2023-07-20 14:18:35 +05:30
b3baaac5c8 map secret comments to windmill api description 2023-07-20 12:57:16 +05:30
aa019e1501 add pattern match for windmill stored secrets 2023-07-20 02:12:36 +05:30
0f8b505c78 change label for windmill workspace form 2023-07-20 01:45:16 +05:30
5b7e23cdc5 add authorization of user for each app 2023-07-20 01:44:21 +05:30
df25657715 Merge pull request #760 from chisom5/feature-codefresh-integration
Codefresh integration
2023-07-20 00:14:27 +07:00
79c2baba1a Merge branch 'Infisical:main' into feature-codefresh-integration 2023-07-19 17:39:45 +01:00
52a2a782f1 Merge pull request #762 from akhilmhdh/fix/sec-import-fail
fix: resolved empty secrets on fresh env and added empty states
2023-07-19 12:38:01 -04:00
eda095b55f Fix merge conflicts 2023-07-19 23:29:01 +07:00
93761f6487 fix: resolved empty secrets on fresh env and added empty states 2023-07-19 21:58:03 +05:30
c5438fbe6d Fix merge conflicts 2023-07-19 23:25:52 +07:00
e8fdaf571c Make sync function for Codefresh 2023-07-19 23:17:59 +07:00
846e2e037f Update 2023-07-19 22:23:48 +07:00
a0a7ff8715 Codefresh integration
Worked on codefresh integration syncing secrets to infiscial
2023-07-19 16:22:25 +01:00
ec1e842202 change windmill workspace label 2023-07-19 19:04:59 +05:30
83d5291998 add interface for windmill request body 2023-07-19 15:00:42 +05:30
638e011cc0 add windmill logo to integration variable 2023-07-19 14:47:37 +05:30
d2d23a7aba add windmill logo 2023-07-19 14:47:15 +05:30
a52c2f03bf add integration slug name mapping for windmill 2023-07-19 14:12:05 +05:30
51c12e0202 Merge branch 'Infisical:main' into feat/windmill-integration 2023-07-19 13:15:21 +05:30
4db7b0c05e add function for windmill secret sync 2023-07-19 13:13:14 +05:30
284608762b update secret import docs 2023-07-19 00:57:35 -04:00
8960773150 Update overview.mdx 2023-07-18 21:51:18 -07:00
4684c9f8b1 Update secret-reference.mdx 2023-07-19 00:40:32 -04:00
abbf3e80f9 Update secret-reference.mdx 2023-07-19 00:31:45 -04:00
d272f580cf update k8 helm for import feature 2023-07-19 00:25:22 -04:00
da9cb70184 only send risk notif when risks are found 2023-07-19 00:05:19 -04:00
1f3f0375b9 add secret import to k8 operator 2023-07-18 23:59:03 -04:00
8ad851d4b0 added the ability to change user name 2023-07-18 18:36:34 -07:00
edef22d28e Terraform Cloud integration 2023-07-18 23:14:41 +01:00
3b5bc151ba Merge pull request #758 from akhilmhdh/feat/secret-import
Implemented secret link/import feature
2023-07-18 16:58:46 -04:00
678cdd3308 Merge branch 'main' into feat/secret-import 2023-07-18 16:52:25 -04:00
76f43ab6b4 Terraform Cloud integration 2023-07-18 21:08:30 +01:00
33554f4057 patch bug when imports don't show with no secrets 2023-07-18 15:32:14 -04:00
c539d4d243 remove print 2023-07-18 15:31:31 -04:00
124e6dd998 feat(secret-import): added workspace validation for get imports and imported secret api 2023-07-18 19:40:35 +05:30
cef29f5dd7 minor style update 2023-07-17 21:39:05 -07:00
95c914631a patch notify user on risk found 2023-07-17 21:52:24 -04:00
49ae61da08 remove border from risk selection 2023-07-17 21:49:58 -04:00
993abd0921 add secret scanning status to api 2023-07-17 21:28:47 -04:00
f37b497e48 Update overview.mdx 2023-07-17 21:11:27 -04:00
0d2e55a06f add telemetry for cloud secret scanning 2023-07-17 20:29:20 -04:00
040243d4f7 add telemetry for cloud secret scanning 2023-07-17 20:29:07 -04:00
c450b01763 update email for secret leak 2023-07-17 20:20:11 -04:00
4cd203c194 add ss-webhook to values file k8-infisical 2023-07-17 19:56:07 -04:00
178d444deb add web hook under api temporarily 2023-07-17 18:58:39 -04:00
139ca9022e Update build-staging-img.yml 2023-07-17 17:36:57 -04:00
34d3e80d17 Merge pull request #743 from Infisical/git-scanning-app
bring back secret engine for dev
2023-07-17 17:21:34 -04:00
deac5fe101 Merge branch 'main' into git-scanning-app 2023-07-17 17:20:04 -04:00
216f3a0d1b reload page after org link 2023-07-17 17:18:55 -04:00
6ee7081640 add secret groups field functionality 2023-07-17 22:00:48 +01:00
43f4110c94 update risk status names 2023-07-17 16:46:15 -04:00
56d430afd6 update risk status and update email notifications 2023-07-17 16:41:33 -04:00
f681f0a98d fix(secret-import): resolved build failure in frontend 2023-07-18 00:29:42 +05:30
23cd6fd861 doc(secret-imports): updated docs for secret import 2023-07-17 23:10:42 +05:30
cf45c3dc8b feat(secret-import): updated cli to support secret import 2023-07-17 23:10:14 +05:30
45584e0c1a feat(secret-import): implemented ui for secret import 2023-07-17 23:08:57 +05:30
202900a7a3 feat(secret-import): implemented api for secret import 2023-07-17 23:08:42 +05:30
38b6a48bee Merge pull request #754 from JunedKhan101/docs-typo-fix
fixed typo
2023-07-17 10:49:46 -04:00
04611d980b create windmill get all workspaces list function 2023-07-17 16:50:27 +05:30
6125246794 add integration authorize redirect url 2023-07-17 16:35:11 +05:30
52e26fc6fa create integration pages for windmill 2023-07-17 16:34:39 +05:30
06bd98bf56 add windmill variables to model schema 2023-07-17 15:12:12 +05:30
7c24e0181a add windmill variables to integration 2023-07-17 15:09:15 +05:30
53abce5780 remove secret engine folder 2023-07-16 16:51:01 -04:00
8c844fb188 move secret scanning to main container 2023-07-16 16:48:36 -04:00
ceeebc24fa Terraform Cloud integration 2023-07-16 21:12:35 +01:00
df7ad9e645 feat(integration): add integration with BitBucket 2023-07-16 22:04:51 +08:00
a9135cdbcd fixed typo 2023-07-16 14:47:35 +05:30
9b96daa185 Merge pull request #752 from afrieirham/feat/sort-integrations-alphabetically
feat: sort cloud and framework integrations alphabetically
2023-07-16 14:34:26 +05:30
9919d3ee6a feat: sort cloud and framework integrations alphabetically 2023-07-16 11:05:37 +08:00
dfcd6b1efd changed docs structure 2023-07-14 19:14:36 -07:00
07bc4c4a3a change docs structure 2023-07-14 19:11:39 -07:00
d69465517f Added styling 2023-07-14 16:26:18 -07:00
6d807c0c74 Merge pull request #749 from RezaRahemtola/fix/cli-vault-cmd-last-line-break
fix(cli): Missing trailing linebreak in vault commands
2023-07-14 18:38:23 -04:00
868cc80210 fix(cli): Missing trailing linebreak in vault commands 2023-07-14 23:09:25 +02:00
3d4a616147 remove secret scanning from prod docker compose 2023-07-14 15:21:04 -04:00
bd3f9130e4 Merge pull request #747 from unkletayo/adetayoreadme-youtubelink-fix
docs(readme):update broken YouTube  page link
2023-07-14 09:19:51 -07:00
f607841acf Update README.md with the correct youtube link 2023-07-14 17:15:09 +01:00
55d813043d Update README.md
This PR fixes broken link to the YouTube page in the Readme file
2023-07-14 08:15:51 +01:00
b2a3a3a0e6 added click-to-copy and changed the slack link 2023-07-13 19:09:00 -07:00
67d5f52aca extract correct params after git app install 2023-07-13 19:56:49 -04:00
a34047521c styled cli redirect 2023-07-13 16:37:05 -07:00
7ff806e8a6 fixed the signup orgId issue 2023-07-13 16:16:00 -07:00
9763353d59 Fixed routing issues 2023-07-13 16:09:33 -07:00
4382935cb5 Merge pull request #733 from akhilmhdh/feat/webhooks
Feat/webhooks
2023-07-13 18:47:47 -04:00
7e3646ddcd add docs on how to pin k8 operator to avoid breaking changes 2023-07-13 17:53:59 -04:00
f7766fc182 fix: resolved just space in a secret value and not changing save state 2023-07-13 23:53:24 +05:30
3176370ef6 feat(webhook): removed console.log 2023-07-13 23:22:20 +05:30
9bed1682fc feat(webhooks): updated docs 2023-07-13 23:22:20 +05:30
daf2e2036e feat(webhook): implemented ui for webhooks 2023-07-13 23:22:20 +05:30
0f81c78639 feat(webhook): implemented api for webhooks 2023-07-13 23:21:18 +05:30
8a19cfe0c6 removed secret scanning from the menu 2023-07-13 10:31:54 -07:00
a00fec9bca trigger standalone docker img too 2023-07-13 11:23:41 -04:00
209f224517 Merge pull request #745 from Infisical/docs-sdk
Remove individual SDK pages from docs
2023-07-13 17:10:26 +07:00
0b7f2b7d4b Remove individual SDK pages from docs in favor of each SDKs README on GitHub 2023-07-13 17:08:32 +07:00
eff15fc3d0 Merge pull request #744 from Infisical/usage-billing
Fix subscription context get organization from useOrganization
2023-07-13 17:07:42 +07:00
2614459772 Fix subscription context get organization from useOrganization 2023-07-13 17:01:53 +07:00
4e926746cf fixing the pro trial bug 2023-07-12 15:46:42 -07:00
f022f6d3ee update secret engine port 2023-07-12 16:39:45 -04:00
1133ae4ae9 bring back secret engine for dev 2023-07-12 16:10:09 -04:00
edd5afa13b remove secret engine from main 2023-07-12 15:50:36 -04:00
442f572acc Merge branch 'infisical-radar-app' into main 2023-07-12 12:12:24 -07:00
be58f3c429 removed the learning item from sidebar 2023-07-12 11:50:36 -07:00
3eea5d9322 Merge pull request #735 from Infisical/new-sidebars
fixing the bugs with sidebars
2023-07-12 11:23:26 -07:00
e4e87163e8 removed org member section 2023-07-12 11:19:56 -07:00
d3aeb729e0 fixing ui/ux bugs 2023-07-12 11:18:42 -07:00
112d4ec9c0 refactor: modify Northflank integration sync logic 2023-07-12 12:25:44 +01:00
2e7c7cf1da fix typo in folder docs 2023-07-12 01:41:14 -04:00
5d39416532 replace cli quick start 2023-07-12 01:38:59 -04:00
af95adb589 Update usage.mdx 2023-07-12 01:31:09 -04:00
0fc4f96773 Merge pull request #736 from Infisical/revamp-docs
Revamp core docs
2023-07-12 01:29:10 -04:00
0a9adf33c8 revamp core docs 2023-07-12 01:23:28 -04:00
f9110cedfa fixing the bug with switching orgs 2023-07-11 22:13:54 -07:00
88ec55fc49 Merge pull request #700 from Infisical/new-sidebars
new sidebars
2023-07-11 17:29:48 -07:00
98b2a2a5c1 adding trial to the sidebar 2023-07-11 17:26:36 -07:00
27eeafbf36 Merge pull request #730 from Infisical/main
Catching up the branch
2023-07-11 16:19:39 -07:00
0cf63028df fixing style and solving merge conflicts 2023-07-11 16:19:07 -07:00
0b52b3cf58 Update mint.json 2023-07-11 14:14:23 -07:00
e1764880a2 Update overview.mdx 2023-07-11 14:09:57 -07:00
d3a47ffcdd Update mint.json 2023-07-11 13:56:24 -07:00
9c1f88bb9c Update mint.json 2023-07-11 13:49:55 -07:00
ae2f3184e2 Merge pull request #711 from afrieirham/form-ux-enhancement
fix: enable users to press `Enter` in forms
2023-07-11 16:34:21 -04:00
3f1db47c30 Merge pull request #731 from Infisical/office-365-smtp
Add support for Office365 SMTP
2023-07-11 15:04:26 +07:00
3e3bbe298d Add support for Office365 SMTP 2023-07-11 14:50:41 +07:00
46dc357651 final changes to sidebars 2023-07-11 00:04:14 -07:00
07d25cb673 extract version from tag 2023-07-10 23:26:14 -04:00
264f75ce8e correct gha for k8 operator 2023-07-10 23:20:45 -04:00
9713a19405 add semvar to k8 images 2023-07-10 23:14:10 -04:00
a3836b970a Terraform Cloud integration 2023-07-10 23:44:55 +01:00
ccfb8771f1 Merge pull request #728 from JunedKhan101/feature-723-remove-trailing-slash
Implemented feature to remove the trailing slash from the domain url
2023-07-10 10:26:53 -04:00
5e2b31cb6c add window redirect for the Northflank integration 2023-07-10 12:57:16 +01:00
b36801652f Merge pull request #729 from Infisical/trial-revamp
Infisical Cloud Pro Free Trial Update
2023-07-10 15:13:28 +07:00
9e5b9cbdb5 Fix lint errors 2023-07-10 15:06:00 +07:00
bdf4ebd1bc second iteration of the new sidebar 2023-07-09 23:58:27 -07:00
e91e7f96c2 Update free plan logic 2023-07-10 13:48:46 +07:00
34fef4aaad Implemented feature to remove the trailing slash from the domain url 2023-07-10 12:16:51 +05:30
09330458e5 Merge pull request #721 from agoodman1999/main
add --path flag to docs for infisical secrets set
2023-07-10 00:09:09 -04:00
ed95b99ed1 Merge branch 'main' into main 2023-07-10 00:08:25 -04:00
dc1e1e8dcb Merge pull request #726 from RezaRahemtola/fix/docs
fix(docs): Wrong integration name and missing link
2023-07-10 00:05:47 -04:00
13a81c9222 add 401 error message for get secrets in cli 2023-07-09 23:25:35 -04:00
6354464859 update terraform docs with path and env 2023-07-09 22:40:00 -04:00
ec26404b94 Merge pull request #727 from Infisical/main
Catching up with main
2023-07-09 11:13:40 -07:00
3c45941474 chore: resolve merge conflicts 2023-07-09 17:38:45 +01:00
91e172fd79 add Northflank specific create.tsx file 2023-07-09 16:18:58 +01:00
5ef2508736 docs: Add missing pull request contribution link 2023-07-09 15:44:25 +02:00
93264fd2d0 docs: Fix wrong integration name 2023-07-09 15:40:59 +02:00
7020c7aeab fix: completing allow user to press Enter in forgot password flow 2023-07-09 15:08:25 +08:00
25b1673321 improve k8 operator docs 2023-07-08 21:48:06 -04:00
628bc711c2 update k8 docks for quick start 2023-07-08 21:12:05 -04:00
a3b4228685 add path to export command 2023-07-08 16:15:45 -04:00
374c8e4a1a Update ingress class values.yaml 2023-07-08 13:47:13 -04:00
5afcf2798f Update build-staging-img.yml 2023-07-08 13:32:35 -04:00
1657cf0a7e Update values.yaml 2023-07-08 13:16:10 -04:00
c9820d0071 Update values.yaml 2023-07-08 12:55:49 -04:00
3e975dc4f0 Terraform Cloud integration 2023-07-08 00:07:38 +01:00
b53c046eef Merge pull request #713 from akhilmhdh/feat/secret-reference
secret reference
2023-07-07 19:02:57 -04:00
fd10d7ed34 add docs for k8 secret refs 2023-07-07 18:59:23 -04:00
c5aae44249 add docs for k8 secret refs 2023-07-07 18:56:38 -04:00
83aa6127ec update k8 chart version 2023-07-07 15:56:47 -04:00
5a2299f758 update k8 operator crd for secret refs 2023-07-07 15:55:45 -04:00
57cdab0727 update k8 operator crd for secret refs 2023-07-07 15:55:22 -04:00
f82fa1b3b3 add secret reference support 2023-07-07 15:49:21 -04:00
e95eef2071 Merge branch 'main' of https://github.com/Infisical/infisical 2023-07-07 13:01:51 +07:00
53efdac0f0 Bring back catch TokenExpiredError in backend error-handling middleware 2023-07-07 13:01:38 +07:00
f5eafc39c5 Merge pull request #717 from atimapreandrew/add-laravel-forge-docs
Added docs for Laravel Forge Integration
2023-07-07 12:14:47 +07:00
0f72ccf82e Remove Laravel Forge from self-hosting docs, update image name 2023-07-07 12:13:47 +07:00
c191eb74fd Update README.md 2023-07-06 21:39:05 -07:00
f9fca42c5b fix incorrect leading slash in example 2023-07-06 13:36:15 -04:00
11a19eef07 add --path flag to docs for infisical secrets set 2023-07-06 13:20:48 -04:00
8a237af4ac feat(secret-ref): updated reference corner cases of trailing slashes 2023-07-06 22:15:10 +05:30
24413e1edd Added docs for Laravel Forge Integration 2023-07-06 15:43:43 +01:00
5aba0c60b8 feat(secret-ref): removed migration field unset op, refactored service token scope check to a utility fn 2023-07-06 20:01:46 +05:30
5599132efe fix(secret-ref): resolved service token unable to fetch secrets in cli 2023-07-06 18:58:48 +05:30
7f9e27e3d3 Update README.md 2023-07-05 15:41:38 -07:00
7d36360111 Updated AWS deploy image 2023-07-05 15:34:22 -07:00
d350297ce1 Deploy to AWS button updated 2023-07-05 15:28:17 -07:00
18d4e42d1f Update README.md 2023-07-05 15:19:54 -07:00
9faf5a3d5c add secret scanning to gamma values 2023-07-05 18:17:09 -04:00
da113612eb diable secret scan by default 2023-07-05 18:09:46 -04:00
e9e2eade89 update helm chart version 2023-07-05 17:56:30 -04:00
3cbc9c1b5c update helm chart to include git app 2023-07-05 17:54:29 -04:00
0772510e47 update gha for git app gamma deploy 2023-07-05 15:52:43 -04:00
f389aa07eb update docker file for prod build 2023-07-05 15:39:44 -04:00
27a110a93a build secret scanning 2023-07-05 15:22:29 -04:00
13eaa4e9a1 feat(secret-ref): updated doc 2023-07-05 23:00:17 +05:30
7ec7d05fb0 feat(secret-ref): implemented cli changes for secret reference 2023-07-05 23:00:17 +05:30
7fe4089bb0 feat(secret-ref): implemented ui for service token changes 2023-07-05 23:00:17 +05:30
0cee453202 feat(secret-ref): implemented backend changes for multi env and folder in service token 2023-07-05 23:00:17 +05:30
088d8097a9 Merge pull request #712 from atimapreandrew/laravel-forge-integration
Laravel forge integration
2023-07-05 23:43:43 +07:00
4e6fae03ff Patch sync Laravel Forge integration 2023-07-05 23:40:43 +07:00
732d0dfdca Added docs for Laravel Forge Integration 2023-07-05 13:45:10 +01:00
93e0232c21 fix: allow user to press Enter in forgot password page 2023-07-05 19:02:48 +08:00
37707c422a fix: allow user to press Enter in login page 2023-07-05 18:40:48 +08:00
2f1bd9ca61 fix: enable user to press Enter in signup flow 2023-07-05 18:32:03 +08:00
3d9ddbf9bc Merge branch 'main' of https://github.com/Infisical/infisical 2023-07-05 13:52:06 +07:00
7c9140dcec Update trial message 2023-07-05 13:51:50 +07:00
a63d179a0d add email notifications for risks 2023-07-04 22:06:29 -04:00
95dd8718bd Merge pull request #709 from raykeating/add-path-flag-to-infisical-run-docs
add --path flag to docs
2023-07-04 20:25:56 -04:00
ff2c9e98c0 add --path flag to docs 2023-07-04 19:48:36 -04:00
d9ab38c590 chore: resolve merge conflicts 2023-07-04 22:52:23 +01:00
23f4a350e7 Added docs for Laravel Forge Integration 2023-07-04 21:08:15 +01:00
696225d8d2 laravel forge integration 2023-07-04 20:01:49 +01:00
6c1ccc17b3 laravel forge integration 2023-07-04 19:28:42 +01:00
aa60f3a664 Merge branch 'main' of github.com:atimapreandrew/infisical 2023-07-04 17:49:08 +01:00
f01fb2830a patch Eslint GetToken issue 2023-07-04 11:11:05 -04:00
9f6aa6b13e add v1 secret scanning 2023-07-04 10:54:44 -04:00
b2ee15a4ff Merge pull request #708 from Infisical/free-trial
Initialize users on Infisical Cloud to Pro (Trial) Tier
2023-07-04 16:26:05 +07:00
42de0fbe73 Fix lint errors 2023-07-04 16:22:06 +07:00
553c986aa8 Update free trial indicator in usage and billing page 2023-07-04 16:01:20 +07:00
9a1e2260a0 Merge pull request #701 from Infisical/main
Update branch
2023-06-30 16:54:26 -07:00
98f7ce2585 Merge branch 'main' of github.com:atimapreandrew/infisical 2023-06-30 17:55:22 +01:00
c30ec8cb5f Merge pull request #697 from Infisical/revamp-project-settings
Standardize styling of Project Settings Page
2023-06-30 16:44:02 +07:00
104c752f9a Finish preliminary standardization of project settings page 2023-06-30 16:38:54 +07:00
b66bea5671 Merge pull request #692 from akhilmhdh/feat/multi-line-secrets
multi line support for secrets
2023-06-29 17:35:25 -04:00
f9313204a7 add docs for k8 re sync interval 2023-06-29 16:08:43 -04:00
cb5c371a4f add re-sync interval 2023-06-29 15:02:53 -04:00
a32df58f46 Merge pull request #695 from Infisical/check-rbac
Rewire RBAC paywall to new mechanism
2023-06-29 18:53:07 +07:00
e2658cc8dd Rewire RBAC paywall to new mechanism 2023-06-29 18:47:35 +07:00
1fbec20c6f Merge pull request #694 from Infisical/clean-org-settings
Clean Personal Settings and Organization Settings Pages
2023-06-29 18:19:24 +07:00
ddff8be53c Fix build error 2023-06-29 18:15:59 +07:00
114d488345 Fix merge conflicts 2023-06-29 17:53:33 +07:00
c4da5a6ead Fix merge conflicts 2023-06-29 17:49:01 +07:00
056f5a4555 Finish preliminary making user settings, org settings styling similar to usage and billing page 2023-06-29 17:47:23 +07:00
dfc88d99f6 first draft new sidebar 2023-06-28 14:28:52 -07:00
033f41a7d5 Merge branch 'main' of github.com:atimapreandrew/infisical 2023-06-28 19:15:08 +01:00
5612a01039 fix(multi-line): resolved linting issues 2023-06-28 20:50:02 +05:30
f1d609cf40 fix: resolved secret version empty 2023-06-28 20:32:12 +05:30
0e9c71ae9f feat(multi-line): added support for multi-line in ui 2023-06-28 20:32:12 +05:30
d1af399489 Merge pull request #684 from akhilmhdh/feat/integrations-page-revamp
integrations page revamp
2023-06-27 17:50:49 -04:00
f445bac42f swap out for v3 secrets 2023-06-27 17:20:30 -04:00
798f091ff2 fix fetching secrets via service token 2023-06-27 15:00:03 -04:00
8381944bb2 feat(integrations-page): fixed id in delete modal 2023-06-27 23:56:43 +05:30
f9d0e0d971 Replace - with Unlimited in compare plans table 2023-06-27 22:00:13 +07:00
29d50f850b Correct current plan text in usage and billing 2023-06-27 19:01:31 +07:00
81c69d92b3 Restyle org name change section 2023-06-27 18:48:26 +07:00
5cd9f37fdf Merge pull request #687 from Infisical/paywalls
Add paywall for PIT and redirect paywall to contact sales in self-hosted
2023-06-27 17:49:42 +07:00
1cf65aca1b Remove print statement 2023-06-27 17:46:36 +07:00
470c429bd9 Merge remote-tracking branch 'origin' into paywalls 2023-06-27 17:46:18 +07:00
c8d081e818 Remove print statement 2023-06-27 17:45:20 +07:00
492c6a6f97 Fix lint errors 2023-06-27 17:30:37 +07:00
1dfd18e779 Add paywall for PIT and redirect paywall to contact sales in self-hosted 2023-06-27 17:19:33 +07:00
caed17152d Merge pull request #686 from Infisical/org-settings
Revamped organization usage and billing page for Infisical Cloud
2023-06-27 16:16:02 +07:00
825143f17c Adjust breadcrumb spacing 2023-06-27 16:12:18 +07:00
da144b4d02 Hide usage and billing from Navbar in self-hosted 2023-06-27 15:56:48 +07:00
f4c4545099 Merge remote-tracking branch 'origin' into org-settings 2023-06-27 15:39:51 +07:00
924a969307 Fix lint errors for revamped billing and usage page 2023-06-27 15:39:36 +07:00
072f6c737c UI update to inetgrations 2023-06-26 18:08:00 -07:00
5f683dd389 feat(integrations-page): updated current integrations width and fixed id in delete modal 2023-06-26 14:31:13 +05:30
2526cbe6ca Add padding Checkly integration page 2023-06-26 12:39:29 +07:00
6959fc52ac minor style updates 2023-06-25 21:49:28 -07:00
81bd684305 removed unnecessary variable declarations 2023-06-25 17:17:29 +01:00
68c8dad829 Merge pull request #682 from atimapreandrew/remove-unnecessary-backend-dependencies
removed await-to-js and builder-pattern dependencies from backend
2023-06-25 18:41:56 +07:00
ca3f7bac6c Remove catch error-handling in favor of error-handling middleware 2023-06-25 17:31:19 +07:00
a127d452bd Continue to make progress on usage and billing page revamp 2023-06-25 17:03:41 +07:00
7c77cc4ea4 fix(integrations-page): eslint fixes to the new upstream changes made 2023-06-24 23:44:52 +05:30
9c0e32a790 fix(integrations-page): added back cloudflare changes in main integrations page 2023-06-24 23:35:55 +05:30
611fae785a chore: updated to latested storybook v7 stable version 2023-06-24 23:31:37 +05:30
0ef4ac1cdc feat(integration-page): implemented new optimized integrations page 2023-06-24 23:31:37 +05:30
c04ea7e731 feat(integration-page): updated components and api hooks 2023-06-24 23:30:27 +05:30
9bdecaf02f removed await-to-js and builder-pattern dependencies from backend 2023-06-24 00:29:31 +01:00
6b222bad01 youtube link change 2023-06-22 19:49:21 -07:00
079d68c042 remove dummy file content 2023-06-22 22:28:39 -04:00
4b800202fb git app with probot 2023-06-22 22:26:23 -04:00
12d0916625 casting to date 2023-06-22 16:25:21 -07:00
e0976d6bd6 added ? to getTime 2023-06-22 16:16:46 -07:00
a31f364361 converted date to unix 2023-06-22 16:10:54 -07:00
8efa17928c intercom date fix 2023-06-22 15:57:20 -07:00
48bfdd500d date format intercom 2023-06-22 15:30:21 -07:00
4621122cfb added created timestamp to intercom 2023-06-22 15:17:11 -07:00
62fb048cce intercom debugging 2023-06-22 15:09:02 -07:00
d4d0fe60b3 Merge branch 'main' of https://github.com/Infisical/infisical 2023-06-22 15:00:16 -07:00
0a6e8e009b intercom update 2023-06-22 14:59:55 -07:00
9f319d7ce3 add dummy value for intercom 2023-06-22 17:17:38 -04:00
7b3bd54386 intercom check 2023-06-22 13:29:26 -07:00
8d82e2d0fc Replace generic error with BadRequestError for missing refresh token in exchange 2023-06-22 18:08:23 +07:00
ffd4655e2f Add API Key auth mode support for v1/workspace 2023-06-22 17:53:09 +07:00
8f119fbdd3 Merge pull request #677 from Infisical/stripe-error
Remove all Stripe logic from codebase + any related issues
2023-06-22 17:24:11 +07:00
b22a179a17 Fix lint issues 2023-06-22 17:03:28 +07:00
1cbab58d29 Merge remote-tracking branch 'origin' into stripe-error 2023-06-22 16:38:46 +07:00
28943f3b6f Finish removing Stripe from codebase 2023-06-22 16:38:02 +07:00
b1f4e17aaf increase limit 2023-06-22 00:40:45 -04:00
afd0c6de08 remove unused import 2023-06-21 15:08:47 -04:00
cf114b0d3c Merge pull request #648 from quinton11/feat/cli-login-redirect
feat: cli login via browser
2023-06-21 14:47:47 -04:00
f785d62315 remove img from login by cli 2023-06-21 14:46:57 -04:00
7aeda9e245 remove service accounts from k8 docs 2023-06-21 12:45:56 -04:00
8a5e655122 fix: frontend lint errors 2023-06-21 07:29:05 +00:00
9b447a4ab0 Merge branch 'main' into feat/cli-login-redirect 2023-06-21 06:47:57 +00:00
f3e84dc6eb Merge pull request #667 from Stijn-Kuijper/cloudflare-pages-integration
Cloudflare Pages integration
2023-06-21 13:09:38 +07:00
a18a86770e Add docs for Cloudflare Pages integration 2023-06-21 13:05:35 +07:00
6300f86cc4 Optimize and patch minor issues for Cloudflare Pages integration 2023-06-21 12:11:53 +07:00
df662b1058 Resolve merge conflicts 2023-06-21 11:44:07 +07:00
db019178b7 Merge pull request #661 from akhilmhdh/feat/folder-doc
doc(folders): updated docs about folders
2023-06-20 13:27:28 -04:00
dcec2dfcb0 Merge pull request #664 from khoa165/add-eslint
Add eslint rule and fix as many issues Add eslint rule and fix as many issues as possibleas possible
2023-06-21 00:03:26 +07:00
e6ad153e83 feat: option to choose target environment 2023-06-20 13:43:22 +02:00
9d33e4756b Add eslint rule and fix as many issues as possible 2023-06-19 23:42:04 -04:00
c267aee20f feat: interactive login 2023-06-19 22:25:21 +00:00
381e40f9a3 doc(folders): updated docs about folders 2023-06-19 22:38:38 +05:30
1760b319d3 cleanup 2023-06-19 16:00:53 +02:00
59737f89c1 fix: cloudlfare pages sync request fix 2023-06-19 15:44:41 +02:00
17097965d9 feat: cloudflare pages integration sync 2023-06-19 15:14:57 +02:00
1a54bf34ef feat: fix getApps and create for cloudflare pages integration 2023-06-19 13:58:38 +02:00
7e8ba077ae fix: terminal text alignment 2023-06-19 09:32:55 +00:00
6ca010e2ba Merge branch 'Infisical:main' into cloudflare-pages-integration 2023-06-18 18:26:55 +02:00
e9eacc445d Merge pull request #650 from akhilmhdh/feat/integrations-page
Feat/integrations page
2023-06-17 10:06:30 +02:00
db12dafad2 Merge branch 'main' of https://github.com/Infisical/infisical 2023-06-17 10:35:49 +07:00
75acda0d7d Add option to attach accessId onto integration auth middleware 2023-06-17 10:35:42 +07:00
b98e276767 Merge pull request #658 from Infisical/cli-switch-v2-to-v3-secrets
cli: switch from v2 secrets to v3
2023-06-16 18:23:23 -04:00
149c58fa3e cli: switch from v2 secrets to v3 2023-06-16 17:49:25 -04:00
62d79b82f8 Merge pull request #642 from akhilmhdh/feat/folder-env-overview
Folder support in secret overview page
2023-06-16 13:19:16 -04:00
7f7e63236b fix: resolved dashboardpage latestKey undefined error 2023-06-16 20:45:31 +05:30
965a5cc113 update rate limits 2023-06-16 10:03:12 -04:00
5a4a36a06a fix: minor change 2023-06-16 13:20:17 +00:00
dd0fdea19f fix: included mfa login flow 2023-06-16 12:58:00 +00:00
af31549309 Update pairing-session link 2023-06-16 01:15:24 +01:00
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
43f2cf8dc3 bugfix(docs): remove duplicate api key header from API reference documentation 2023-06-15 16:49:50 -07:00
0aca308bbd Update README.md 2023-06-15 15:01:52 -07:00
ff567892f9 added empty state for integrations and terraform 2023-06-15 14:58:40 -07:00
15fc12627a minor style updates 2023-06-15 13:51:28 -07:00
a743c12c1b feat(folder-scoped-integrations): implemented ui for folders in integration page 2023-06-15 22:46:40 +05:30
2471418591 feat(folder-scoped-integration): implemented api changes for integrations to support folders 2023-06-15 22:46:39 +05:30
c77ebd4d0e Merge pull request #649 from Infisical/environment-paywall
Update implementation for environment limit paywall
2023-06-15 15:56:32 +01:00
ccaf9a9ffc Update implementation for environment limit paywall 2023-06-15 15:48:19 +01:00
381806d84b feat: initial getApps for Cloudflare Pages 2023-06-15 09:46:09 +02:00
391e37d49e fixed bugs with env and password reset 2023-06-14 21:27:37 -07:00
7088b3c9d8 patch refresh token cli 2023-06-14 17:32:01 -04:00
ccf0877b81 Revert "Revert "add refresh token to cli""
This reverts commit 6b0e0f70d299ed8bf4fa23e4d70f8426e0a40a5f.
2023-06-14 17:32:01 -04:00
9e9129dd02 feat: cli login via browser 2023-06-14 19:12:56 +00:00
0aa9390ece Merge pull request #647 from Budhathoki356/fix/typo
fix: minor typos in code
2023-06-14 14:51:44 -04:00
e47934a08a Merge branch 'main' into fix/typo 2023-06-14 14:47:22 -04:00
04b7383bbe fix: minor typos in code 2023-06-15 00:17:00 +05:45
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
82a026a426 Update refreshPlan to consider workspace 2023-06-14 12:28:01 +01:00
92647341a9 Update getPlan with workspace-specific consideration and add environmentLimit to returned plan 2023-06-14 11:52:48 +01:00
776cecc3ef create prod release action 2023-06-13 22:16:26 -04:00
a4fb2378bb wait for helm upgrade before mark complete 2023-06-13 22:06:53 -04:00
9742fdc770 rename docker image 2023-06-13 22:00:51 -04:00
786778fef6 isolate gamma environment 2023-06-13 21:56:15 -04:00
3f946180dd add terraform docs 2023-06-13 18:28:41 -04:00
b1b32a34c9 feat(folder-sec-overview): made folder cell fully select 2023-06-13 20:16:14 +05:30
3d70333f9c Update password-reset email response 2023-06-13 15:31:55 +01:00
a6cf7107b9 feat(folder-sec-overview): implemented folder based ui for sec overview 2023-06-13 19:26:33 +05:30
d590dd5db8 feat(folder-sec-overview): added folder path support in get secrets and get folders 2023-06-13 19:26:33 +05:30
c64cf39b69 feat: cloudflare pages integration create page 2023-06-13 12:37:07 +02:00
f4404f66b8 Correct link to E2EE API usage example 2023-06-13 11:30:47 +01:00
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
e24c1f38e0 Add REST API integration option in docs introduction 2023-06-13 11:23:13 +01:00
dffcee52d7 feat: cloudflare integration auth page 2023-06-13 11:52:40 +02:00
db28536ea8 feat: add clouflare pages button to integrations page 2023-06-13 11:12:12 +02:00
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
37d2d580f4 Improve API docs for non-E2EE 2023-06-13 10:02:10 +01:00
41dd2fda8a Changed the intercom to aprovider model 2023-06-12 21:42:29 -07:00
22ca4f2e92 Fixed the typeerror issue 2023-06-12 20:56:19 -07:00
5882eb6f8a Merge pull request #639 from Infisical/intercom-tour
Switched intercom to AppLayout
2023-06-12 20:20:06 -07:00
c13d5e29f4 add intercom env replace during start up 2023-06-12 16:19:27 -07:00
d99c54ca50 Switched intercom to layout 2023-06-12 15:38:12 -07:00
9dd0dac2f9 Patch frontend lint error 2023-06-12 18:07:15 +01:00
98efffafaa Patch subscription plan frontend validation 2023-06-12 17:47:32 +01:00
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
553cf11ad2 Fix lint issue 2023-06-12 12:16:23 +01:00
4616cffecd Add support for read/write non-e2ee secrets 2023-06-12 12:04:28 +01:00
39feb9a6ae Merge branch 'main' of https://github.com/Infisical/infisical 2023-06-11 19:24:38 -07:00
82c1f8607d Added intercom 2023-06-11 19:23:30 -07:00
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
1dea6749ba Allow custom environments in self-hosted instances 2023-06-11 18:19:01 -05:00
631eac803e Finish preliminary v3/secrets/raw endpoints 2023-06-11 12:11:25 +01:00
facabc683b Fix merge conflicts 2023-06-10 11:07:31 +01:00
4b99a9ea93 Merge pull request #633 from akhilmhdh/feat/folders-service-token
Folder scoped service token
2023-06-10 11:02:16 +01:00
445afb397c feat(folder-scoped-st): added batch,create secrets v2 secretpath support and service token 2023-06-10 12:10:43 +05:30
7d554f46d5 feat(folder-scoped-st): changed text css transformation in folders 2023-06-10 12:09:43 +05:30
bbef7d415c remove old commit 2023-06-09 18:41:10 -07:00
bb7b398fa7 throw unauthorized error instead of 500 for permission denied 2023-06-09 18:40:41 -07:00
570457c7c9 check path before service token create 2023-06-09 18:38:39 -07:00
1b77b1d70b fixed the etxt issue 2023-06-09 17:02:41 -07:00
0f697a91ab updated the workspace limit 2023-06-09 16:14:35 -07:00
df6d23d1d3 fixed the ts error 2023-06-09 15:31:38 -07:00
0187d3012b Add non-e2ee option for getSecret, getSecrets, start createSecret 2023-06-09 21:20:12 +01:00
4299a76fcd changed the default envs 2023-06-09 12:52:44 -07:00
2bae6cf084 lots of frontend improvements 2023-06-09 12:50:17 -07:00
22beebc5d0 feat(folder-scoped-st): implemented frontend ui for folder scoped service token 2023-06-09 23:44:33 +05:30
6cb0a20675 feat(folder-scoped-st): implemented backend api for folder scoped service tokens 2023-06-09 23:44:33 +05:30
00fae0023a Add cluster URL image to docs for Vault integration 2023-06-09 15:57:47 +01:00
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
00dfcfcf4e Finish preliminary Vault integration, made docs for Vault and Checkly 2023-06-09 15:36:37 +01:00
f5441e9996 Merge branch 'main' of https://github.com/Infisical/infisical 2023-06-08 11:08:48 -07:00
ee2fb33b50 changed the docs order 2023-06-08 11:08:27 -07:00
c51b194ba6 Merge pull request #629 from Infisical/optimize-checkly
Optimize Checkly integration
2023-06-08 11:21:28 +01:00
2920ba5195 Update Checkly envars only if changed 2023-06-08 11:18:23 +01:00
cd837b07aa Remove Sentry, part-try-catch from sync Checkly 2023-06-08 11:04:34 +01:00
a8e71e8170 Merge pull request #627 from Infisical/checkly-integration
Checkly integration
2023-06-08 10:56:19 +01:00
5fa96411d6 Merge branch 'main' into checkly-integration 2023-06-08 10:53:10 +01:00
329ab8ae61 Add +devices for verifyMfaToken user 2023-06-08 00:58:23 +01:00
3242d9b44e Fix change password button active state on no errors 2023-06-08 00:28:51 +01:00
8ce48fea43 Fix change password button active state on no errors 2023-06-08 00:27:59 +01:00
b011144258 reduce password forgot limit 2023-06-07 16:27:16 -07:00
674828e8e4 Copy data folder into backend build folder 2023-06-07 23:57:37 +01:00
c0563aff77 Bring back try-catch for initGlobalFeatureSet 2023-06-07 23:13:25 +01:00
7cec42a7fb Merge pull request #628 from Infisical/pentest-remediation
Fix issues/bugs
2023-06-07 22:52:08 +01:00
78493d9521 Fix lint errors 2023-06-07 22:47:47 +01:00
49b3e8b538 comment fixes 2023-06-07 13:12:58 -07:00
a3fca200fc comment fixes 2023-06-07 13:12:21 -07:00
158eb584d2 integration with checkly done 2023-06-07 13:11:39 -07:00
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
604810ebd2 fix(ui): resolved reloading when form submission 2023-06-07 22:45:50 +05:30
d4108d1fab update email docs for self hosting 2023-06-07 10:13:43 -07:00
4d6ae0eef8 Merge remote-tracking branch 'origin' into pentest-remediation 2023-06-07 16:30:13 +01:00
8193490d7f Merge pull request #624 from Infisical/stabilize-server-try-catch
Bring back express-async-errors
2023-06-07 16:27:16 +01:00
0deba5e345 Bring back express-async-errors 2023-06-07 16:25:13 +01:00
a2055194c5 Fix merge conflicts 2023-06-07 13:12:54 +01:00
8c0d643a37 Fix merge conflicts 2023-06-07 12:58:24 +01:00
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
04765ffb94 update email setup docs 2023-06-06 23:27:15 -07:00
6b9aa200b5 login/signup styling fixes 2023-06-06 19:41:02 -07:00
5667e47b31 Add default rely on Cloudflare for IP addresses 2023-06-07 00:50:25 +01:00
a8ed187443 Add check for most common passwords 2023-06-07 00:06:35 +01:00
c5be497052 Strengthen password requirement 2023-06-06 23:06:44 +01:00
77d47e071b add folder id to versions in batch update 2023-06-06 13:31:08 -07:00
4bf2407d13 remove encryptionKey validation check 2023-06-06 09:43:11 -07:00
846f5c6680 Upgraded JWT invalidation/session logic to separate TokenVersion model. 2023-06-06 16:36:52 +01:00
6f1f07c9a5 Merge branch 'main' into removing-sentry-logs 2023-06-06 15:17:59 +01:00
aaca66e5a4 Patch support for ENCRYPTION_KEY and ROOT_ENCRYPTION_KEY in generateSecretBlindIndexHelper 2023-06-06 14:24:06 +01:00
b9dad5c3f0 Begin preliminary tokenVersion impl 2023-06-06 11:25:08 +01:00
3a79a855cb Merge pull request #622 from Infisical/folder-patch-v2
Patch backfill data
2023-06-05 23:14:14 -07:00
e28d0cbace bring back tags to secret version 2023-06-05 23:12:48 -07:00
c0fbe82ecb update populate number 2023-06-05 20:21:09 -07:00
b0e7304bff Patch backfill data 2023-06-05 20:19:15 -07:00
5a1b6acc93 Fix merge conflicts with auth changes 2023-06-05 21:27:01 +01:00
5f5ed5d0a9 Change export convention for helper functions 2023-06-05 21:00:23 +01:00
bfee0a6d30 feat: remove try-catch blocks for handling errors in middleware 2023-06-05 21:15:35 +03:00
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
6dee858154 feat(folders): resolved auth issues and added the env dropdown change inside folders 2023-06-05 20:47:58 +05:30
0c18bd71c4 Implement preliminary pentest remediations 2023-06-05 00:44:10 +01:00
b9dfff1cd8 add migration complete logs 2023-06-04 16:37:57 -07:00
44b9533636 Merge pull request #609 from akhilmhdh/feat/folders
Feat/folders
2023-06-04 16:24:43 -07:00
599c8d94c9 Merge pull request #620 from Infisical/single-rate-limit-store
use mongo rate limit store
2023-06-04 13:45:56 -07:00
77788e1524 use mongo rate limit store 2023-06-04 13:43:57 -07:00
3df62a6e0a patch dup email bug for login 2023-06-04 11:07:52 -07:00
e74cc471db fix(folders): changed to secret path in controllers for get by path op 2023-06-04 13:26:20 +05:30
58d3f3945a feat(folders): removed old comments 2023-06-04 13:22:29 +05:30
29fa618bff feat(folders): changed / to root in breadcrumbs for folders 2023-06-04 13:18:05 +05:30
668b5a9cfd feat(folders): adopted new strategy for rollback on folders 2023-06-04 13:18:05 +05:30
6ce0f48b2c fix(folders): fixed algorithm missing in rollback versions and resolved env change reset folderid 2023-06-04 13:18:05 +05:30
467e85b717 Minor style changes 2023-06-04 13:18:05 +05:30
579516bd38 feat(folders): implemented ui for folders in dashboard 2023-06-04 13:18:05 +05:30
deaa85cbe7 feat(folders): added support for snapshot by env and folder 2023-06-04 13:18:05 +05:30
08a4404fed fixed the email issue 2023-06-03 13:58:53 -07:00
73aa01c568 prevent passport init when envs are undefined 2023-06-03 12:20:04 -07:00
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
f619ee7297 revise self hosting order 2023-06-02 14:10:40 -07:00
bb825c3d68 add DO docs link 2023-06-02 14:06:04 -07:00
6bbd7f05a2 add digital ocean docs 2023-06-02 14:05:00 -07:00
4865b69e6d updated the slack link 2023-06-01 13:54:02 -07:00
6f3c7c0fbf fix ts issues 2023-05-31 11:29:08 -07:00
be80b5124f Final style changes to login/signup 2023-05-31 11:25:37 -07:00
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
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
94141fedd6 Update contributing docs, add pull requests section 2023-05-31 12:25:08 +03:00
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
1a1693dbbf Add encryption key validation to validation script 2023-05-30 20:46:20 +03:00
9440afa386 Remove re-encryption from Infisical, move to migration script 2023-05-30 20:30:58 +03:00
8b1ec1424d patch quote type in docs 2023-05-30 11:19:15 -04:00
bd56f3b64c update docker run command for self host 2023-05-30 11:14:22 -04:00
f20ea723b7 Merge branch 'main' into feature/google-signin-signup-integration 2023-05-30 20:46:20 +08:00
86f76ebe70 no default user for selfhosting docs 2023-05-30 08:39:43 -04:00
821385c2f3 Revert "add prod img publish ste p"
This reverts commit f7dbd41431b4f3459161c16667951e6b2005daa4.
2023-05-30 07:29:45 -04:00
03c65c8635 Revert "add dummy step"
This reverts commit 893c4777fea475e905b97ef8bcfb2feb21a37154.
2023-05-30 07:29:31 -04:00
893c4777fe add dummy step 2023-05-29 18:49:14 -04:00
f7dbd41431 add prod img publish ste p 2023-05-29 18:46:04 -04:00
8d1f3e930a revert keychain name 2023-05-29 18:08:49 -04:00
f25715b3c4 update keychain name 2023-05-29 17:36:07 -04:00
37251ed607 Begin migration script for re-encryption 2023-05-29 23:46:16 +03:00
c078fb8bc1 allow user to create new keychain 2023-05-29 15:56:48 -04:00
2ae3c48b88 Merge branch 'main' of https://github.com/Infisical/infisical 2023-05-29 14:21:27 +03:00
ce28151952 Update posthog-js version 2023-05-29 14:21:16 +03:00
e6027b3c72 removed try catch from requireAuth middleware 2023-05-29 18:14:16 +08:00
7f4db518cc Merge branch 'main' into feature/google-signin-signup-integration 2023-05-29 18:00:43 +08:00
7562e7d667 Merge pull request #607 from Infisical/changelog
Add preliminary changelog to docs
2023-05-29 12:18:27 +03:00
5c8f33a2d8 Add preliminary changelog to docs 2023-05-29 12:15:47 +03:00
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
50609d06f5 Final style changes to signup 2023-05-28 14:22:37 -07:00
3a5ad93450 Final style changes to signup 2023-05-28 14:22:17 -07:00
8493d51f5c Merge branch 'main' of https://github.com/Infisical/infisical 2023-05-28 22:23:51 +03:00
e90f63b375 Install and require express-async-errors earlier 2023-05-28 22:23:26 +03:00
af9ffdc51f delete pre commit (pre-commit.com) 2023-05-28 14:36:07 -04:00
3a76a82438 add dummy ENCRYPTION_KEY for testing backend docker img 2023-05-28 14:09:32 -04:00
8e972c704a resolved error handling issue with requireAuth middleware 2023-05-28 23:06:20 +08:00
b975115443 Merge branch 'main' into feature/google-signin-signup-integration 2023-05-28 22:12:02 +08:00
4a1821d537 Merge pull request #606 from Infisical/gitlab-integration
Add pagination to retrieve envars for GitLab integration
2023-05-28 16:51:29 +03:00
01b87aeebf Add pagination to retrieve envars for GitLab integration 2023-05-28 16:46:05 +03:00
cea3b59053 Merge branch 'main' of https://github.com/Infisical/infisical 2023-05-27 19:12:47 -07:00
a6f6711c9a posthog attribution adjustment 2023-05-27 19:12:32 -07:00
3d3b416da2 Merge pull request #602 from piyushchhabra/fix/project-list-scroll
fix(ui): fixed scroll on project list selection
2023-05-26 23:13:07 -07:00
bfbe2f2dcf brought the button back down and removed side bar for other browsers 2023-05-26 23:08:57 -07:00
8e5db3ee2f Merge pull request #605 from Infisical/revert-601-add-refresh-token-cli
Revert "add refresh token to cli"
2023-05-26 16:56:13 -04:00
6b0e0f70d2 Revert "add refresh token to cli" 2023-05-26 16:56:02 -04:00
1fb9aad08a Revert "only re-store user creds when token expire"
This reverts commit df9efa65e7cc523723cd19902f4d183a464022bb.
2023-05-26 16:55:29 -04:00
61a09d817b Merge pull request #604 from Infisical/revised-encryption-key
Update dummy variables in test
2023-05-26 17:31:59 +03:00
57b8ed4eef Merge remote-tracking branch 'origin' into revised-encryption-key 2023-05-26 17:29:54 +03:00
c3a1d03a9b Update test dummy variables 2023-05-26 17:29:23 +03:00
11afb6db51 Merge pull request #603 from Infisical/revised-encryption-key
Add encryption metadata and upgrade ENCRYPTION_KEY to ROOT_ENCRYPTION_KEY
2023-05-26 17:01:00 +03:00
200d9de740 Fix merge conflicts 2023-05-26 16:41:17 +03:00
17060b22d7 Update README.md 2023-05-25 21:24:07 -07:00
c730280eff Update FeatureSet interface to include used counts 2023-05-26 00:26:16 +03:00
c45120e6e9 add shorter env name for file vault 2023-05-25 13:27:20 -04:00
c96fbd3724 fix(ui): fixing scroll on project list selection 2023-05-25 19:44:06 +05:30
e1e2eb7c3b Add SecretBlindIndexData for development user initialization 2023-05-25 16:07:08 +03:00
7812061e66 Update isPaid telemetry accounting to be tier-based instead of via slug 2023-05-25 12:59:18 +03:00
ca41c65fe0 small helm doc changes 2023-05-24 23:46:34 -04:00
d8c15a366d Merge pull request #600 from piyushchhabra/fix/gui-tags-overflow
fix(ui): fixed tags overflow in delete card
2023-05-24 20:19:53 -07:00
df9efa65e7 only re-store user creds when token expire 2023-05-24 19:46:02 -04:00
1c5616e3b6 revise pre commit doc 2023-05-24 19:11:33 -04:00
27030138ec Merge pull request #601 from Infisical/add-refresh-token-cli
add refresh token to cli
2023-05-24 18:53:52 -04:00
c37ce4eaea add refresh token to cli 2023-05-24 18:51:42 -04:00
5aa367fe54 fix(ui): fixed tags overflow in card + port correction in README 2023-05-24 23:03:12 +05:30
fac4968193 moved oauth controller endpoints to auth 2023-05-24 23:44:30 +08:00
17647587f9 remove tests for time being 2023-05-24 10:48:11 -04:00
f3dc7fcf7b add timout to pull requests 2023-05-24 10:48:11 -04:00
93cf7cde2d fixed login issue after mfa 2023-05-24 21:33:24 +08:00
422d04d7d7 migrated to standard request 2023-05-24 18:50:59 +08:00
4c41d279e9 Merge branch 'main' into feature/google-signin-signup-integration 2023-05-24 18:39:18 +08:00
e65c6568e1 Modify convention for PostHog isPaid attr to be tier-based instead of slug 2023-05-24 10:26:06 +03:00
9d40a96633 Update README.md 2023-05-23 20:22:01 -04:00
859fe09ac6 Merge pull request #598 from Infisical/maidul98-patch-1
add pre commit install command to README.md
2023-05-23 20:20:57 -04:00
d0d6419d4d add pre commit install command to README.md 2023-05-23 20:20:10 -04:00
8b05ce11f7 add pre commit to husky 2023-05-23 20:15:39 -04:00
a7fb0786f9 improve pre commit docs 2023-05-23 19:45:10 -04:00
f2de1778cb catch case when hook path is default 2023-05-23 19:34:31 -04:00
952cf47b9a Merge branch 'main' of https://github.com/Infisical/infisical 2023-05-23 15:41:43 -07:00
1d17596af1 added boolean flag for the plan in posthog logging 2023-05-23 15:41:34 -07:00
01385687e0 make posthog failed calls level=debug 2023-05-23 18:16:13 -04:00
d2e3aa15b0 patch standalone docker image 2023-05-23 17:16:32 -04:00
96607153dc Modularize getOrganizationPlan function 2023-05-23 23:54:54 +03:00
a8502377c7 Add endpoint for updating organization plan 2023-05-23 23:14:20 +03:00
5aa99001cc Merge pull request #597 from Infisical/connect-to-license-server
Added add/remove/get organization payment methods and get cloud plans…
2023-05-23 22:39:35 +03:00
83dd35299c Added add/remove/get organization payment methods and get cloud plans from license server 2023-05-23 22:28:41 +03:00
b5b2f402ad add missing required envrs 2023-05-23 14:09:45 -04:00
ec34572087 patch invite only 2023-05-23 13:18:21 -04:00
7f7d120c2f Merge pull request #595 from Infisical/connect-to-license-server
Add support for fetching plan details from license server
2023-05-23 17:02:20 +03:00
899d46514c Add forwarding usedSeats and subscription quantity to license server on org member add/delete 2023-05-23 16:59:13 +03:00
658df21189 Add auto install pre commit 2023-05-23 00:09:00 -04:00
51914c6a2e resolved package-lock conflicts 2023-05-22 22:07:30 +08:00
ad37a14f2e Merge branch 'main' into feature/google-signin-signup-integration 2023-05-22 21:54:51 +08:00
8341faddc5 Add support for pulling plan details from license server with LICENSE_KEY, LICENSE_SERVER_KEY 2023-05-22 15:43:33 +03:00
8e3a23e6d8 fix prod node img for standalone 2023-05-22 08:18:50 -04:00
bc61de4a80 add provider auth secret to kubernetes and docker yaml 2023-05-20 23:15:36 +08:00
1c89474159 hello 2023-05-19 17:23:15 -04:00
2f765600b1 add pre-commit hook 2023-05-19 17:20:27 -04:00
d9057216b5 remove keyring access during telemetry 2023-05-19 16:10:59 -04:00
6aab90590f add version to cli run telemtry 2023-05-19 12:24:49 -04:00
f7466d4855 update cli telemetry 2023-05-19 12:20:37 -04:00
ea2565ed35 Merge pull request #591 from Infisical/cli-telemetry
Cli telemetry
2023-05-19 10:55:27 -04:00
7722231656 Merge pull request #590 from Infisical/infisical-scan-docs
Infisical scan docs
2023-05-18 15:59:51 -04:00
845a476974 add secret scanning to README.md 2023-05-18 15:57:48 -04:00
fc19a17f4b update readme with scaning feature 2023-05-18 15:42:25 -04:00
0890b1912f Merge pull request #589 from Infisical/infisical-scan-docs
add docs for infisical scan
2023-05-18 15:20:26 -04:00
82ecc2d7dc add secret scanning to resources 2023-05-18 15:18:29 -04:00
446a63a917 add docs for infisical scan 2023-05-18 14:55:39 -04:00
4367822777 re-added token caching and redirection 2023-05-18 23:04:55 +08:00
ca4a9b9937 resolved MFA not appearing 2023-05-18 02:07:07 +08:00
ec8d62d106 show toast when oauth login error 2023-05-18 01:58:35 +08:00
6ca3b8ba61 handled error cases for external auth login 2023-05-18 00:39:20 +08:00
4b2e91da74 added proper error handling for user creation 2023-05-18 00:12:28 +08:00
fac8affe78 added missing envs for documentation 2023-05-17 22:24:04 +08:00
1ccec486cc removed caching of providerAuthToken 2023-05-17 21:43:18 +08:00
6746f04f33 added self-hosting documentation for google 2023-05-15 23:39:19 +08:00
dba19b4a1d Merge branch 'main' into feature/google-signin-signup-integration 2023-05-15 20:41:08 +08:00
884aed74a5 made last name optional 2023-05-15 20:39:45 +08:00
9dc7cc58a7 uncommented code 2023-05-15 00:41:27 +08:00
6f66b56e7c updated package-lock 2023-05-12 22:24:38 +08:00
be2bac41bb Merge branch 'main' into feature/google-signin-signup-integration 2023-05-12 22:22:51 +08:00
0afa44a9f0 removed express-session types 2023-05-11 15:39:04 +08:00
5a99878d15 Final style edits to the login and signup flows 2023-05-10 16:12:57 -07:00
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
967e520173 More designed changes to login flow 2023-05-10 12:55:08 -07:00
ccfe0b1eb9 reverted changes made to nginx config 2023-05-11 00:52:49 +08:00
0ef5779776 add providerAuthToken for MFA login 2023-05-11 00:47:16 +08:00
a194e90644 removed session references 2023-05-11 00:41:38 +08:00
addc849fa6 changed google-auth strategy and removed session use 2023-05-11 00:37:02 +08:00
074c0bdd77 utilized mongodb as persistent store for sessions 2023-05-10 23:01:44 +08:00
7ee33e9393 resolved merge conflict issues and updated use of translations 2023-05-10 21:39:11 +08:00
32cef27e8e Merge branch 'main' into feature/google-sign 2023-05-10 21:11:42 +08:00
1fce8cc769 More style changes to login 2023-05-09 23:24:37 -07:00
4e7145dfe5 Style changes to login 2023-05-09 20:45:59 -07:00
4c434555a4 finalized signup/signin ux regarding redirects 2023-05-07 20:54:30 +08:00
f011d61167 Merge remote-tracking branch 'origin' into revised-encryption-key 2023-05-06 22:22:03 +03:00
87e047a152 Checkpoint finish preliminary support for ROOT_ENCRYPTION_KEY 2023-05-06 22:07:59 +03:00
ea86e59d4f resolved component alignment of signup 2023-05-06 19:26:42 +08:00
3e19e6fd99 finalized login and signup ui 2023-05-06 18:52:01 +08:00
3d3d7c9821 Merge remote-tracking branch 'origin' into revised-encryption-key 2023-05-05 10:27:44 +03:00
5eeda6272c Checkpoint adding crypto metadata 2023-05-04 20:35:06 +03:00
b734b51954 developed new ui for new login and signup page 2023-05-05 00:39:28 +08:00
1172726e74 added signup v3 endpoints and developed initial new signup flow 2023-05-04 01:32:40 +08:00
c766686670 Fix merge conflicts for variable imports 2023-05-03 19:30:30 +03:00
099cee7f39 Begin refactoring backfilling and preparation operations into setup and start adding encryption metadata to models 2023-05-03 14:21:42 +03:00
f703ee29e5 implemented comments 2023-05-03 18:58:32 +08:00
dfb84e9932 developed initial version of new login page 2023-04-27 23:10:27 +08:00
2dd1570200 updated use of environment variables to utilize await 2023-04-27 02:00:16 +08:00
69472514af Merge branch 'main' into feature/google-signin-signup-integration 2023-04-27 01:46:26 +08:00
f956170820 added auth v3 endpoints for login1 and login2 2023-04-27 01:38:06 +08:00
007e8c4442 initial setup for google signin signup integration 2023-04-25 23:47:46 +08:00
1519 changed files with 102850 additions and 52456 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
backend/node_modules
frontend/node_modules

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
@ -23,6 +25,9 @@ JWT_SIGNUP_LIFETIME=
# Required
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
# Redis
REDIS_URL=redis://redis:6379
# Optional credentials for MongoDB container instance and Mongo-Express
MONGO_USERNAME=root
MONGO_PASSWORD=example
@ -45,11 +50,13 @@ CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB=
CLIENT_ID_GITLAB=
CLIENT_ID_BITBUCKET=
CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB=
CLIENT_SECRET_GITLAB=
CLIENT_SECRET_BITBUCKET=
CLIENT_SLUG_VERCEL=
# Sentry (optional) for monitoring errors
@ -59,10 +66,6 @@ SENTRY_DSN=
# Ignore - Not applicable for self-hosted version
POSTHOG_HOST=
POSTHOG_PROJECT_API_KEY=
STRIPE_SECRET_KEY=
STRIPE_PUBLISHABLE_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_PRODUCT_STARTER=
STRIPE_PRODUCT_TEAM=
STRIPE_PRODUCT_PRO=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
CLIENT_ID_GOOGLE=
CLIENT_SECRET_GOOGLE=

View File

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

BIN
.github/images/Deploy to AWS.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -13,6 +13,7 @@ services:
- MONGO_URL=mongodb://test:example@mongo:27017/?authSource=admin
- MONGO_USERNAME=test
- MONGO_PASSWORD=example
- ENCRYPTION_KEY=a984ecdf82ec779e55dbcc21303a900f
networks:
- infisical-test

15
.github/values.yaml vendored
View File

@ -1,3 +1,10 @@
# secretScanningGitApp:
# enabled: false
# deploymentAnnotations:
# secrets.infisical.com/auto-reload: "true"
# image:
# repository: infisical/staging_deployment_secret-scanning-git-app
frontend:
enabled: true
name: frontend
@ -6,7 +13,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 +32,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
@ -51,8 +58,8 @@ mongodbConnection:
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: "nginx"
# annotations:
# kubernetes.io/ingress.class: "nginx"
# cert-manager.io/issuer: letsencrypt-nginx
hostName: gamma.infisical.com ## <- Replace with your own domain
frontend:

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,25 +1,19 @@
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
run: npm ci --only-production
working-directory: backend
- name: 🧪 Run tests
run: npm run test:ci
working-directory: backend
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
@ -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 --install
if [[ $(helm status infisical) == *"FAILED"* ]]; then
echo "Helm upgrade failed"
exit 1

View File

@ -13,6 +13,7 @@ jobs:
check-be-pr:
name: Check
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: ☁️ Checkout source
@ -26,17 +27,17 @@ jobs:
- name: 📦 Install dependencies
run: npm ci --only-production
working-directory: backend
- name: 🧪 Run tests
run: npm run test:ci
working-directory: backend
- name: 📁 Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: be-test-results
path: |
./backend/reports
./backend/coverage
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
# - name: 📁 Upload test results
# uses: actions/upload-artifact@v3
# if: always()
# with:
# name: be-test-results
# path: |
# ./backend/reports
# ./backend/coverage
- name: 🏗️ Run build
run: npm run build
working-directory: backend

View File

@ -2,40 +2,35 @@ name: Check Frontend Pull Request
on:
pull_request:
types: [ opened, synchronize ]
types: [opened, synchronize]
paths:
- 'frontend/**'
- '!frontend/README.md'
- '!frontend/.*'
- 'frontend/.eslintrc.js'
- "frontend/**"
- "!frontend/README.md"
- "!frontend/.*"
- "frontend/.eslintrc.js"
jobs:
check-fe-pr:
name: Check
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
-
name: ☁️ Checkout source
- name: ☁️ Checkout source
uses: actions/checkout@v3
-
name: 🔧 Setup Node 16
- name: 🔧 Setup Node 16
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
node-version: "16"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
-
name: 📦 Install dependencies
- name: 📦 Install dependencies
run: npm ci --only-production --ignore-scripts
working-directory: frontend
# -
# name: 🧪 Run tests
# run: npm run test:ci
# working-directory: frontend
-
name: 🏗️ Run build
- name: 🏗️ Run build
run: npm run build
working-directory: frontend

View File

@ -1,11 +1,17 @@
name: Release standalone docker image
on: [workflow_dispatch]
on:
push:
tags:
- "infisical/v*.*.*"
jobs:
infisical-standalone:
name: Build infisical standalone 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
with:
@ -64,5 +70,6 @@ jobs:
tags: |
infisical/infisical:latest
infisical/infisical:${{ steps.commit.outputs.short }}
infisical/infisical:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
file: Dockerfile.standalone-infisical

View File

@ -1,10 +1,16 @@
name: Release Docker image for K8 operator
on: [workflow_dispatch]
name: Release Docker image for K8 operator
on:
push:
tags:
- "infisical-k8-operator/v*.*.*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical-k8-operator/}"
- uses: actions/checkout@v2
- name: 🔧 Set up QEMU
@ -26,4 +32,6 @@ jobs:
context: k8-operator
push: true
platforms: linux/amd64,linux/arm64
tags: infisical/kubernetes-operator:latest
tags: |
infisical/kubernetes-operator:latest
infisical/kubernetes-operator:${{ steps.extract_version.outputs.version }}

4
.gitignore vendored
View File

@ -2,6 +2,7 @@
node_modules
.env
.env.dev
.env.gamma
.env.prod
.env.infisical
@ -56,3 +57,6 @@ yarn-error.log*
# Infisical init
.infisical.json
# Editor specific
.vscode/*

View File

@ -15,13 +15,12 @@ monorepo:
tag_prefix: infisical-cli/
dir: cli
env:
- POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
builds:
- id: darwin-build
binary: infisical
ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
ldflags:
- -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
- -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
flags:
- -trimpath
env:
@ -39,7 +38,9 @@ builds:
env:
- CGO_ENABLED=0
binary: infisical
ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
ldflags:
- -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
- -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
flags:
- -trimpath
goos:
@ -107,6 +108,22 @@ brews:
zsh_completion.install "completions/infisical.zsh" => "_infisical"
fish_completion.install "completions/infisical.fish"
man1.install "manpages/infisical.1.gz"
- name: 'infisical@{{.Version}}'
tap:
owner: Infisical
name: homebrew-get-cli
commit_author:
name: "Infisical"
email: ai@infisical.com
folder: Formula
homepage: "https://infisical.com"
description: "The official Infisical CLI"
install: |-
bin.install "infisical"
bash_completion.install "completions/infisical.bash" => "infisical"
zsh_completion.install "completions/infisical.zsh" => "_infisical"
fish_completion.install "completions/infisical.fish"
man1.install "manpages/infisical.1.gz"
nfpms:
- id: infisical

View File

@ -1,5 +1,6 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
infisical scan git-changes --staged -v

1
.infisicalignore Normal file
View File

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

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
@ -77,7 +82,7 @@ RUN npm ci --only-production
COPY --from=backend-build /app .
# Production stage
FROM node:14-alpine AS production
FROM node:16-alpine AS production
WORKDIR /

106
README.md

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,40 @@
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"plugins": [
"@typescript-eslint",
"unused-imports"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-console": 2
"no-empty-function": "off",
"@typescript-eslint/no-empty-function": "off",
"no-console": 2,
"quotes": [
"error",
"double",
{
"avoidEscape": true
}
],
"comma-dangle": [
"error",
"only-multiline"
],
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"sort-imports": 1
}
}
}

7
backend/.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"singleQuote": false,
"printWidth": 100,
"trailingComma": "none",
"tabWidth": 2,
"semi": true
}

View File

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

View File

@ -14,7 +14,7 @@ declare global {
JWT_SIGNUP_LIFETIME: string;
JWT_SIGNUP_SECRET: string;
MONGO_URL: string;
NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
NODE_ENV: "development" | "staging" | "testing" | "production";
VERBOSE_ERROR_OUTPUT: string;
LOKI_HOST: string;
CLIENT_ID_HEROKU: string;
@ -39,12 +39,6 @@ declare global {
SMTP_PASSWORD: string;
SMTP_FROM_ADDRESS: string;
SMTP_FROM_NAME: string;
STRIPE_PRODUCT_STARTER: string;
STRIPE_PRODUCT_TEAM: string;
STRIPE_PRODUCT_PRO: string;
STRIPE_PUBLISHABLE_KEY: string;
STRIPE_SECRET_KEY: string;
STRIPE_WEBHOOK_SECRET: string;
TELEMETRY_ENABLED: string;
LICENSE_KEY: string;
}

View File

@ -1,9 +1,9 @@
export default {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverageFrom: ['src/*.{js,ts}', '!**/node_modules/**'],
modulePaths: ['<rootDir>/src'],
testMatch: ['<rootDir>/tests/**/*.test.ts'],
setupFiles: ['<rootDir>/test-resources/env-vars.js'],
setupFilesAfterEnv: ['<rootDir>/tests/setupTests.ts']
preset: "ts-jest",
testEnvironment: "node",
collectCoverageFrom: ["src/*.{js,ts}", "!**/node_modules/**"],
modulePaths: ["<rootDir>/src"],
testMatch: ["<rootDir>/tests/**/*.test.ts"],
setupFiles: ["<rootDir>/test-resources/env-vars.js"],
setupFilesAfterEnv: ["<rootDir>/tests/setupTests.ts"],
};

17232
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +1,60 @@
{
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.319.0",
"@casl/ability": "^6.5.0",
"@casl/mongoose": "^7.2.1",
"@godaddy/terminus": "^4.12.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.49.0",
"@sentry/tracing": "^7.48.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"@ucast/mongo2js": "^1.3.4",
"argon2": "^0.30.3",
"await-to-js": "^3.0.0",
"aws-sdk": "^2.1364.0",
"axios": "^1.3.5",
"axios-retry": "^3.4.0",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.4.0",
"builder-pattern": "^2.2.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"express-async-errors": "^3.1.1",
"express-rate-limit": "^6.7.0",
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"infisical-node": "^1.1.3",
"infisical-node": "^1.2.1",
"ioredis": "^5.3.2",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.10.5",
"mongodb": "^5.7.0",
"mongoose": "^7.4.1",
"nanoid": "^3.3.6",
"node-cache": "^5.1.2",
"nodemailer": "^6.8.0",
"passport": "^0.6.0",
"passport-github": "^1.1.0",
"passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0",
"probot": "^12.3.1",
"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",
"swagger-ui-express": "^4.6.2",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
"winston-loki": "^6.0.6",
"zod": "^3.21.4"
},
"name": "infisical-api",
"version": "1.0.0",
@ -53,7 +63,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",
@ -78,6 +88,7 @@
"@posthog/plugin-scaffold": "^1.3.4",
"@types/bcrypt": "^5.0.0",
"@types/bcryptjs": "^2.4.2",
"@types/bull": "^4.10.0",
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
@ -86,6 +97,8 @@
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6",
"@types/passport": "^1.0.12",
"@types/picomatch": "^2.3.0",
"@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",
@ -93,12 +106,15 @@
"@typescript-eslint/parser": "^5.40.1",
"cross-env": "^7.0.3",
"eslint": "^8.26.0",
"eslint-plugin-unused-imports": "^2.0.0",
"install": "^0.13.0",
"jest": "^29.3.1",
"jest-junit": "^15.0.0",
"nodemon": "^2.0.19",
"npm": "^8.19.3",
"smee-client": "^1.2.3",
"supertest": "^6.3.3",
"swagger-autogen": "^2.23.5",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1"
},

File diff suppressed because it is too large Load Diff

View File

@ -1,69 +1,100 @@
import InfisicalClient from 'infisical-node';
import InfisicalClient from "infisical-node";
const client = new InfisicalClient({
token: process.env.INFISICAL_TOKEN!
export const client = new InfisicalClient({
token: process.env.INFISICAL_TOKEN!,
});
export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000;
export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue == undefined ? false : (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue;
export const getEncryptionKey = async () => (await client.getSecret('ENCRYPTION_KEY')).secretValue;
export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10;
export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d';
export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue;
export const getJwtMfaLifetime = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m';
export const getJwtMfaSecret = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m';
export const getJwtRefreshLifetime = async () => (await client.getSecret('JWT_REFRESH_LIFETIME')).secretValue || '90d';
export const getJwtRefreshSecret = async () => (await client.getSecret('JWT_REFRESH_SECRET')).secretValue;
export const getJwtServiceSecret = async () => (await client.getSecret('JWT_SERVICE_SECRET')).secretValue;
export const getJwtSignupLifetime = async () => (await client.getSecret('JWT_SIGNUP_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';
export const getVerboseErrorOutput = async () => (await client.getSecret('VERBOSE_ERROR_OUTPUT')).secretValue === 'true' && true;
export const getLokiHost = async () => (await client.getSecret('LOKI_HOST')).secretValue;
export const getClientIdAzure = async () => (await client.getSecret('CLIENT_ID_AZURE')).secretValue;
export const getClientIdHeroku = async () => (await client.getSecret('CLIENT_ID_HEROKU')).secretValue;
export const getClientIdVercel = async () => (await client.getSecret('CLIENT_ID_VERCEL')).secretValue;
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 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 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';
export const getSentryDSN = async () => (await client.getSecret('SENTRY_DSN')).secretValue;
export const getSiteURL = async () => (await client.getSecret('SITE_URL')).secretValue;
export const getSmtpHost = async () => (await client.getSecret('SMTP_HOST')).secretValue;
export const getSmtpSecure = async () => (await client.getSecret('SMTP_SECURE')).secretValue === 'true' || false;
export const getSmtpPort = async () => parseInt((await client.getSecret('SMTP_PORT')).secretValue) || 587;
export const getSmtpUsername = async () => (await client.getSecret('SMTP_USERNAME')).secretValue;
export const getSmtpPassword = async () => (await client.getSecret('SMTP_PASSWORD')).secretValue;
export const getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue;
export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical';
export const getStripeProductStarter = async () => (await client.getSecret('STRIPE_PRODUCT_STARTER')).secretValue;
export const getStripeProductPro = async () => (await client.getSecret('STRIPE_PRODUCT_PRO')).secretValue;
export const getStripeProductTeam = async () => (await client.getSecret('STRIPE_PRODUCT_TEAM')).secretValue;
export const getStripePublishableKey = async () => (await client.getSecret('STRIPE_PUBLISHABLE_KEY')).secretValue;
export const getStripeSecretKey = async () => (await client.getSecret('STRIPE_SECRET_KEY')).secretValue;
export const getStripeWebhookSecret = async () => (await client.getSecret('STRIPE_WEBHOOK_SECRET')).secretValue;
export const getTelemetryEnabled = async () => (await client.getSecret('TELEMETRY_ENABLED')).secretValue !== 'false' && true;
export const getLoopsApiKey = async () => (await client.getSecret('LOOPS_API_KEY')).secretValue;
export const getSmtpConfigured = async () => (await client.getSecret('SMTP_HOST')).secretValue == '' || (await client.getSecret('SMTP_HOST')).secretValue == undefined ? false : true
export const getPort = async () => (await client.getSecret("PORT")).secretValue || 4000;
export const getEncryptionKey = async () => {
const secretValue = (await client.getSecret("ENCRYPTION_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
}
export const getRootEncryptionKey = async () => {
const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
}
export const getInviteOnlySignup = async () => (await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true"
export const getSaltRounds = async () => parseInt((await client.getSecret("SALT_ROUNDS")).secretValue) || 10;
export const getJwtAuthLifetime = async () => (await client.getSecret("JWT_AUTH_LIFETIME")).secretValue || "10d";
export const getJwtAuthSecret = async () => (await client.getSecret("JWT_AUTH_SECRET")).secretValue;
export const getJwtMfaLifetime = async () => (await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m";
export const getJwtMfaSecret = async () => (await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m";
export const getJwtRefreshLifetime = async () => (await client.getSecret("JWT_REFRESH_LIFETIME")).secretValue || "90d";
export const getJwtRefreshSecret = async () => (await client.getSecret("JWT_REFRESH_SECRET")).secretValue;
export const getJwtServiceSecret = async () => (await client.getSecret("JWT_SERVICE_SECRET")).secretValue;
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";
export const getVerboseErrorOutput = async () => (await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true;
export const getLokiHost = async () => (await client.getSecret("LOKI_HOST")).secretValue;
export const getClientIdAzure = async () => (await client.getSecret("CLIENT_ID_AZURE")).secretValue;
export const getClientIdHeroku = async () => (await client.getSecret("CLIENT_ID_HEROKU")).secretValue;
export const getClientIdVercel = async () => (await client.getSecret("CLIENT_ID_VERCEL")).secretValue;
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 getClientIdBitBucket = async () => (await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
export const getClientIdGCPSecretManager = async () => (await client.getSecret("CLIENT_ID_GCP_SECRET_MANAGER")).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 getClientSecretBitBucket = async () => (await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
export const getClientSecretGCPSecretManager = async () => (await client.getSecret("CLIENT_SECRET_GCP_SECRET_MANAGER")).secretValue;
export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIENT_ID_GOOGLE_LOGIN")).secretValue;
export const getClientSecretGoogleLogin = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
export const getClientIdGitHubLogin = async () => (await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
export const getClientSecretGitHubLogin = async () => (await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).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";
export const getSentryDSN = async () => (await client.getSecret("SENTRY_DSN")).secretValue;
export const getSiteURL = async () => (await client.getSecret("SITE_URL")).secretValue;
export const getSmtpHost = async () => (await client.getSecret("SMTP_HOST")).secretValue;
export const getSmtpSecure = async () => (await client.getSecret("SMTP_SECURE")).secretValue === "true" || false;
export const getSmtpPort = async () => parseInt((await client.getSecret("SMTP_PORT")).secretValue) || 587;
export const getSmtpUsername = async () => (await client.getSecret("SMTP_USERNAME")).secretValue;
export const getSmtpPassword = async () => (await client.getSecret("SMTP_PASSWORD")).secretValue;
export const getSmtpFromAddress = async () => (await client.getSecret("SMTP_FROM_ADDRESS")).secretValue;
export const getSmtpFromName = async () => (await client.getSecret("SMTP_FROM_NAME")).secretValue || "Infisical";
export const getSecretScanningWebhookProxy = async () => (await client.getSecret("SECRET_SCANNING_WEBHOOK_PROXY")).secretValue;
export const getSecretScanningWebhookSecret = async () => (await client.getSecret("SECRET_SCANNING_WEBHOOK_SECRET")).secretValue;
export const getSecretScanningGitAppId = async () => (await client.getSecret("SECRET_SCANNING_GIT_APP_ID")).secretValue;
export const getSecretScanningPrivateKey = async () => (await client.getSecret("SECRET_SCANNING_PRIVATE_KEY")).secretValue;
export const getRedisUrl = async () => (await client.getSecret("REDIS_URL")).secretValue;
export const getLicenseKey = async () => {
const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
}
export const getLicenseServerKey = async () => {
const secretValue = (await client.getSecret("LICENSE_SERVER_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
}
export const getLicenseServerUrl = async () => (await client.getSecret("LICENSE_SERVER_URL")).secretValue || "https://portal.infisical.com";
export const getTelemetryEnabled = async () => (await client.getSecret("TELEMETRY_ENABLED")).secretValue !== "false" && true;
export const getLoopsApiKey = async () => (await client.getSecret("LOOPS_API_KEY")).secretValue;
export const getSmtpConfigured = async () => (await client.getSecret("SMTP_HOST")).secretValue == "" || (await client.getSecret("SMTP_HOST")).secretValue == undefined ? false : true
export const getHttpsEnabled = async () => {
if ((await getNodeEnv()) != "production") {
// no https for anything other than prod
return false
}
if ((await client.getSecret('HTTPS_ENABLED')).secretValue == undefined || (await client.getSecret('HTTPS_ENABLED')).secretValue == "") {
if ((await client.getSecret("HTTPS_ENABLED")).secretValue == undefined || (await client.getSecret("HTTPS_ENABLED")).secretValue == "") {
// default when no value present
return true
}
return (await client.getSecret('HTTPS_ENABLED')).secretValue === 'true' && true
}
return (await client.getSecret("HTTPS_ENABLED")).secretValue === "true" && true
}

View File

@ -1,10 +1,24 @@
import axios from 'axios';
import axiosRetry from 'axios-retry';
import axios from "axios";
import axiosRetry from "axios-retry";
import {
getLicenseKeyAuthToken,
getLicenseServerKeyAuthToken,
setLicenseKeyAuthToken,
setLicenseServerKeyAuthToken,
} from "./storage";
import {
getLicenseKey,
getLicenseServerKey,
getLicenseServerUrl,
} from "./index";
const axiosInstance = axios.create();
// should have JWT to interact with the license server
export const licenseServerKeyRequest = axios.create();
export const licenseKeyRequest = axios.create();
export const standardRequest = axios.create();
// add retry functionality to the axios instance
axiosRetry(axiosInstance, {
axiosRetry(standardRequest, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries
retryCondition: (error) => {
@ -13,4 +27,98 @@ axiosRetry(axiosInstance, {
},
});
export default axiosInstance;
export const refreshLicenseServerKeyToken = async () => {
const licenseServerKey = await getLicenseServerKey();
const licenseServerUrl = await getLicenseServerUrl();
const { data: { token } } = await standardRequest.post(
`${licenseServerUrl}/api/auth/v1/license-server-login`, {},
{
headers: {
"X-API-KEY": licenseServerKey,
},
}
);
setLicenseServerKeyAuthToken(token);
return token;
}
export const refreshLicenseKeyToken = async () => {
const licenseKey = await getLicenseKey();
const licenseServerUrl = await getLicenseServerUrl();
const { data: { token } } = await standardRequest.post(
`${licenseServerUrl}/api/auth/v1/license-login`, {},
{
headers: {
"X-API-KEY": licenseKey,
},
}
);
setLicenseKeyAuthToken(token);
return token;
}
licenseServerKeyRequest.interceptors.request.use((config) => {
const token = getLicenseServerKeyAuthToken();
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, (err) => {
return Promise.reject(err);
});
licenseServerKeyRequest.interceptors.response.use((response) => {
return response
}, async function (err) {
const originalRequest = err.config;
if (err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// refresh
const token = await refreshLicenseServerKeyToken();
axios.defaults.headers.common["Authorization"] = "Bearer " + token;
return licenseServerKeyRequest(originalRequest);
}
return Promise.reject(err);
});
licenseKeyRequest.interceptors.request.use((config) => {
const token = getLicenseKeyAuthToken();
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, (err) => {
return Promise.reject(err);
});
licenseKeyRequest.interceptors.response.use((response) => {
return response
}, async function (err) {
const originalRequest = err.config;
if (err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// refresh
const token = await refreshLicenseKeyToken();
axios.defaults.headers.common["Authorization"] = "Bearer " + token;
return licenseKeyRequest(originalRequest);
}
return Promise.reject(err);
});

View File

@ -0,0 +1,30 @@
const MemoryLicenseServerKeyTokenStorage = () => {
let authToken: string;
return {
setToken: (token: string) => {
authToken = token;
},
getToken: () => authToken,
};
};
const MemoryLicenseKeyTokenStorage = () => {
let authToken: string;
return {
setToken: (token: string) => {
authToken = token;
},
getToken: () => authToken,
};
};
const licenseServerTokenStorage = MemoryLicenseServerKeyTokenStorage();
const licenseTokenStorage = MemoryLicenseKeyTokenStorage();
export const getLicenseServerKeyAuthToken = licenseServerTokenStorage.getToken;
export const setLicenseServerKeyAuthToken = licenseServerTokenStorage.setToken;
export const getLicenseKeyAuthToken = licenseTokenStorage.getToken;
export const setLicenseKeyAuthToken = licenseTokenStorage.setToken;

View File

@ -1,29 +1,29 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import * as bigintConversion from 'bigint-conversion';
import { Request, Response } from "express";
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 { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
import { checkUserDevice } from '../../helpers/user';
const jsrp = require("jsrp");
import { LoginSRPDetail, TokenVersion, User } from "../../models";
import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth";
import { checkUserDevice } from "../../helpers/user";
import { ACTION_LOGIN, ACTION_LOGOUT } from "../../variables";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { EELogService } from "../../ee/services";
import { getUserAgentType } from "../../utils/posthog";
import {
ACTION_LOGIN,
ACTION_LOGOUT
} from '../../variables';
import { BadRequestError } from '../../utils/errors';
import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getJwtRefreshSecret,
getHttpsEnabled,
getJwtAuthLifetime,
getJwtAuthSecret,
getHttpsEnabled
} from '../../config';
getJwtRefreshSecret
} from "../../config";
import { ActorType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module 'jsonwebtoken' {
declare module "jsonwebtoken" {
export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string;
refreshVersion?: number;
}
}
@ -34,47 +34,42 @@ declare module 'jsonwebtoken' {
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
try {
const {
email,
clientPublicKey
}: { email: string; clientPublicKey: string } = req.body;
const {
body: { email, clientPublicKey }
} = await validateRequest(reqValidator.Login1V1, req);
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 }, {
await LoginSRPDetail.findOneAndReplace(
{ email: email },
{
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false })
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 +80,88 @@ 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 {
body: { email, clientProof }
} = await validateRequest(reqValidator.Login2V1, req);
if (!user) throw new Error('Failed to find user');
const user = await User.findOne({
email
}).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag");
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email })
if (!user) throw new Error("Failed to find user");
if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
}
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email });
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
if (!loginSRPDetailFromDB) {
return BadRequestError(
Error(
"It looks like some details from the first login are not found. Please try login one again"
)
);
}
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// issue tokens
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
await checkUserDevice({
user,
ip: req.ip,
userAgent: req.headers['user-agent'] ?? ''
});
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() });
await checkUserDevice({
user,
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()
});
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
loginAction && await EELogService.createLog({
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
});
channel: getUserAgentType(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,41 +171,51 @@ 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.actor.type === ActorType.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: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP
}));
return res.status(200).send({
message: 'Successfully logged out.'
message: "Successfully logged out."
});
};
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."
});
};
@ -218,52 +227,60 @@ export const logout = async (req: Request, res: Response) => {
*/
export const checkAuth = async (req: Request, res: Response) => {
return res.status(200).send({
message: 'Authenticated'
message: "Authenticated"
});
}
};
/**
* 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()
if (!refreshToken)
throw BadRequestError({
message: "Failed to find refresh token in request cookies"
});
return res.status(200).send({
token
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"
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Invalid request'
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,12 +1,20 @@
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';
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Bot, BotKey } from "../../models";
import { createBot } from "../../helpers/bot";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/bot";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError } from "../../utils/errors";
interface BotKey {
encryptedKey: string;
nonce: string;
encryptedKey: string;
nonce: string;
}
/**
@ -17,33 +25,31 @@ interface BotKey {
* @returns
*/
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
let bot;
try {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetBotByWorkspaceIdV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
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'
});
}
return res.status(200).send({
bot
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
});
};
/**
@ -53,56 +59,69 @@ 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 {
body: { botKey, isActive },
params: { botId }
} = await validateRequest(reqValidator.SetBotActiveStateV1, req);
bot = await Bot.findOneAndUpdate({
_id: req.bot._id
}, {
isActive
}, {
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'
});
}
return res.status(200).send({
bot
const bot = await Bot.findById(botId);
if (!bot) {
throw BadRequestError({ message: "Bot not found" });
}
const userId = req.user._id;
const { permission } = await getUserProjectPermissions(userId, bot.workspace.toString());
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
);
if (isActive) {
// bot state set to active -> share workspace key with bot
if (!botKey?.encryptedKey || !botKey?.nonce) {
return res.status(400).send({
message: "Failed to set bot state to active - missing bot key"
});
}
await BotKey.findOneAndUpdate(
{
workspace: bot.workspace
},
{
encryptedKey: botKey.encryptedKey,
nonce: botKey.nonce,
sender: userId,
bot: bot._id,
workspace: bot.workspace
},
{
upsert: true,
new: true
}
);
} else {
// case: bot state set to inactive -> delete bot's workspace key
await BotKey.deleteOne({
bot: bot._id
});
}
const updatedBot = await Bot.findOneAndUpdate(
{
_id: bot._id
},
{
isActive
},
{
new: true
}
);
if (!updatedBot) throw new Error("Failed to update bot active state");
return res.status(200).send({
bot
});
};

View File

@ -1,35 +1,39 @@
import * as authController from './authController';
import * as botController from './botController';
import * as integrationAuthController from './integrationAuthController';
import * as integrationController from './integrationController';
import * as keyController from './keyController';
import * as membershipController from './membershipController';
import * as membershipOrgController from './membershipOrgController';
import * as organizationController from './organizationController';
import * as passwordController from './passwordController';
import * as secretController from './secretController';
import * as serviceTokenController from './serviceTokenController';
import * as signupController from './signupController';
import * as stripeController from './stripeController';
import * as userActionController from './userActionController';
import * as userController from './userController';
import * as workspaceController from './workspaceController';
import * as authController from "./authController";
import * as botController from "./botController";
import * as integrationAuthController from "./integrationAuthController";
import * as integrationController from "./integrationController";
import * as keyController from "./keyController";
import * as membershipController from "./membershipController";
import * as membershipOrgController from "./membershipOrgController";
import * as organizationController from "./organizationController";
import * as passwordController from "./passwordController";
import * as secretController from "./secretController";
import * as serviceTokenController from "./serviceTokenController";
import * as signupController from "./signupController";
import * as userActionController from "./userActionController";
import * as userController from "./userController";
import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController";
import * as secretImpsController from "./secretImpsController";
export {
authController,
botController,
integrationAuthController,
integrationController,
keyController,
membershipController,
membershipOrgController,
organizationController,
passwordController,
secretController,
serviceTokenController,
signupController,
stripeController,
userActionController,
userController,
workspaceController
authController,
botController,
integrationAuthController,
integrationController,
keyController,
membershipController,
membershipOrgController,
organizationController,
passwordController,
secretController,
serviceTokenController,
signupController,
userActionController,
userController,
workspaceController,
secretScanningController,
webhookController,
secretImpsController
};

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,21 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
Integration
} from '../../models';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Folder, IWorkspace, Integration, IntegrationAuth } from "../../models";
import { EventService } from "../../services";
import { eventStartIntegration } from "../../events";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/syncSecretsToThirdPartyServices";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/integration";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Create/initialize an (empty) integration for integration authorization
@ -14,64 +24,117 @@ import { eventPushSecrets } from '../../events';
* @returns
*/
export const createIntegration = async (req: Request, res: Response) => {
let integration;
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,
const {
body: {
isActive,
sourceEnvironment,
secretPath,
app,
appId,
targetEnvironment,
targetEnvironmentId,
path,
appId,
owner,
region,
scope,
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
})
});
}
integrationAuthId,
targetEnvironment,
targetEnvironmentId,
metadata
}
} = await validateRequest(reqValidator.CreateIntegrationV1, req);
const integrationAuth = await IntegrationAuth.findById(integrationAuthId)
.populate<{ workspace: IWorkspace }>("workspace")
.select(
"+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt"
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create integration'
});
}
if (!integrationAuth) throw BadRequestError({ message: "Integration auth not found" });
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace._id.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Integrations
);
const folders = await Folder.findOne({
workspace: integrationAuth.workspace._id,
environment: sourceEnvironment
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Folder path doesn't exist"
});
}
}
// TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token
const integration = await new Integration({
workspace: integrationAuth.workspace._id,
environment: sourceEnvironment,
isActive,
app,
appId,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region,
scope,
secretPath,
integration: integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId),
metadata
}).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventStartIntegration({
workspaceId: integration.workspace,
environment: sourceEnvironment
})
});
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_INTEGRATION,
metadata: {
integrationId: integration._id.toString(),
integration: integration.integration,
environment: integration.environment,
secretPath,
url: integration.url,
app: integration.app,
appId: integration.appId,
targetEnvironment: integration.targetEnvironment,
targetEnvironmentId: integration.targetEnvironmentId,
targetService: integration.targetService,
targetServiceId: integration.targetServiceId,
path: integration.path,
region: integration.region
}
},
{
workspaceId: integration.workspace
}
);
return res.status(200).send({
integration,
integration
});
};
@ -82,86 +145,156 @@ 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 {
body: {
environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
} = req.body;
secretPath
},
params: { integrationId }
} = await validateRequest(reqValidator.UpdateIntegrationV1, req);
integration = await Integration.findOneAndUpdate(
{
_id: req.integration._id,
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner,
},
{
new: true,
}
);
const integration = await Integration.findById(integrationId);
if (!integration) throw BadRequestError({ message: "Integration not found" });
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment
}),
const { permission } = await getUserProjectPermissions(
req.user._id,
integration.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
);
const folders = await Folder.findOne({
workspace: integration.workspace,
environment
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Path for service token does not exist"
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to update integration",
}
const updatedIntegration = await Integration.findOneAndUpdate(
{
_id: integration._id
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner,
secretPath
},
{
new: true
}
);
if (updatedIntegration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventStartIntegration({
workspaceId: updatedIntegration.workspace,
environment
})
});
}
return res.status(200).send({
integration,
integration: updatedIntegration
});
};
/**
* Delete integration with id [integrationId] and deactivate bot if there are
* no integrations left
* Delete integration with id [integrationId]
* @param req
* @param res
* @returns
*/
export const deleteIntegration = async (req: Request, res: Response) => {
let integration;
try {
const { integrationId } = req.params;
const {
params: { integrationId }
} = await validateRequest(reqValidator.DeleteIntegrationV1, req);
integration = await Integration.findOneAndDelete({
_id: integrationId,
});
const integration = await Integration.findById(integrationId);
if (!integration) throw BadRequestError({ message: "Integration not found" });
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",
});
}
const { permission } = await getUserProjectPermissions(
req.user._id,
integration.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Integrations
);
const deletedIntegration = await Integration.findOneAndDelete({
_id: integrationId
});
if (!deletedIntegration) throw new Error("Failed to find integration");
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_INTEGRATION,
metadata: {
integrationId: integration._id.toString(),
integration: integration.integration,
environment: integration.environment,
secretPath: integration.secretPath,
url: integration.url,
app: integration.app,
appId: integration.appId,
targetEnvironment: integration.targetEnvironment,
targetEnvironmentId: integration.targetEnvironmentId,
targetService: integration.targetService,
targetServiceId: integration.targetServiceId,
path: integration.path,
region: integration.region
}
},
{
workspaceId: integration.workspace
}
);
return res.status(200).send({
integration,
integration
});
};
// Will trigger sync for all integrations within the given env and workspace id
export const manualSync = async (req: Request, res: Response) => {
const {
body: { workspaceId, environment }
} = await validateRequest(reqValidator.ManualSyncV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
);
syncSecretsToActiveIntegrationsQueue({
workspaceId,
environment
});
res.status(200).send();
};

View File

@ -1,7 +1,17 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Key } from '../../models';
import { findMembership } from '../../helpers/membership';
import { Types } from "mongoose";
import { Request, Response } from "express";
import { Key } from "../../models";
import { findMembership } from "../../helpers/membership";
import { EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/key";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
@ -11,38 +21,38 @@ import { findMembership } from '../../helpers/membership';
* @returns
*/
export const uploadKey = async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
const { key } = req.body;
const {
params: { workspaceId },
body: { key }
} = await validateRequest(reqValidator.UploadKeyV1, req);
// validate membership of receiver
const receiverMembership = await findMembership({
user: key.userId,
workspace: workspaceId
});
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
if (!receiverMembership) {
throw new Error('Failed receiver membership validation for workspace');
}
// validate membership of receiver
const receiverMembership = await findMembership({
user: key.userId,
workspace: workspaceId
});
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'
});
}
if (!receiverMembership) {
throw new Error("Failed receiver membership validation for workspace");
}
return res.status(200).send({
message: 'Successfully uploaded 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,31 +62,36 @@ 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 {
params: { workspaceId }
} = await validateRequest(reqValidator.GetLatestKeyV1, req);
// 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 = {};
const resObj: any = {};
if (latestKey.length > 0) {
resObj['latestKey'] = latestKey[0];
}
if (latestKey.length > 0) {
resObj["latestKey"] = latestKey[0];
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.GET_WORKSPACE_KEY,
metadata: {
keyId: latestKey[0]._id.toString()
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
}
return res.status(200).send(resObj);
};
return res.status(200).send(resObj);
};

View File

@ -1,13 +1,23 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import { Membership, MembershipOrg, User, Key } from '../../models';
import { Request, Response } from "express";
import { Types } from "mongoose";
import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models";
import { EventType } from "../../ee/models";
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
import { sendMail } from "../../helpers/nodemailer";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
import { getSiteURL } from "../../config";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/membership";
import {
findMembership,
deleteMembership as deleteMember
} from '../../helpers/membership';
import { sendMail } from '../../helpers/nodemailer';
import { ADMIN, MEMBER, ACCEPTED } from '../../variables';
import { getSiteURL } from '../../config';
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import Role from "../../ee/models/role";
import { BadRequestError } from "../../utils/errors";
import { InviteUserToWorkspaceV1 } from "../../validation/workspace";
/**
* Check that user is a member of workspace with id [workspaceId]
@ -16,29 +26,23 @@ import { getSiteURL } from '../../config';
* @returns
*/
export const validateMembership = async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.ValidateMembershipV1, req);
// 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'
});
return res.status(200).send({
message: "Workspace membership confirmed"
});
};
/**
@ -48,52 +52,50 @@ 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 {
params: { membershipId }
} = await validateRequest(reqValidator.DeleteMembershipV1, req);
// 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: IUser }>("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
});
const { permission } = await getUserProjectPermissions(
req.user._id,
membershipToDelete.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Member
);
if (!membership) {
throw new Error('Failed to validate workspace membership');
}
// delete workspace membership
const deletedMembership = await deleteMember({
membershipId: membershipToDelete._id.toString()
});
if (membership.role !== ADMIN) {
// user is not an admin member of the workspace
throw new Error('Insufficient role for deleting workspace membership');
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.REMOVE_WORKSPACE_MEMBER,
metadata: {
userId: membershipToDelete.user._id.toString(),
email: membershipToDelete.user.email
}
},
{
workspaceId: membershipToDelete.workspace
}
);
// 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'
});
}
return res.status(200).send({
deletedMembership
});
return res.status(200).send({
deletedMembership
});
};
/**
@ -103,53 +105,80 @@ 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 {
body: { role },
params: { membershipId }
} = await validateRequest(reqValidator.ChangeMembershipRoleV1, req);
if (![ADMIN, MEMBER].includes(role)) {
throw new Error('Failed to validate role');
}
// validate target membership
const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>(
"user"
);
// validate target membership
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');
}
const { permission } = await getUserProjectPermissions(
req.user._id,
membershipToChangeRole.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
// check if user is a member and admin of target membership's
// workspace
const membership = await findMembership({
user: req.user._id,
workspace: membershipToChangeRole.workspace
});
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
if (isCustomRole) {
const wsRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: membershipToChangeRole.workspace
});
if (!wsRole) throw BadRequestError({ message: "Role not found" });
const membership = await Membership.findByIdAndUpdate(membershipId, {
role: CUSTOM,
customRole: wsRole
});
return res.status(200).send({
membership
});
}
if (!membership) {
throw new Error('Failed to validate membership');
}
const membership = await Membership.findByIdAndUpdate(
membershipId,
{
$set: {
role
},
$unset: {
customRole: 1
}
},
{
new: true
}
);
if (membership.role !== ADMIN) {
// user is not an admin member of the workspace
throw new Error('Insufficient role for changing member roles');
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_USER_WORKSPACE_ROLE,
metadata: {
userId: membershipToChangeRole.user._id.toString(),
email: membershipToChangeRole.user.email,
oldRole: membershipToChangeRole.role,
newRole: role
}
},
{
workspaceId: membershipToChangeRole.workspace
}
);
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'
});
}
return res.status(200).send({
membership: membershipToChangeRole
});
return res.status(200).send({
membership
});
};
/**
@ -159,75 +188,86 @@ 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 {
params: { workspaceId },
body: { email }
} = await validateRequest(InviteUserToWorkspaceV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Member
);
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
}).populate<{ user: IUser }>("user");
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
});
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw new Error("Failed to find workspace");
// validate invitee's organization membership - ensure that only
// (accepted) organization members can be added to the workspace
const membershipOrg = await MembershipOrg.findOne({
user: invitee._id,
organization: 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
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: workspace.name,
callback_url: (await getSiteURL()) + "/login"
}
});
return res.status(200).send({
invitee,
latestKey
});
};
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.ADD_WORKSPACE_MEMBER,
metadata: {
userId: invitee._id.toString(),
email: invitee.email
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
invitee,
latestKey
});
};

View File

@ -1,14 +1,29 @@
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 { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
import { Types } from "mongoose";
import { Request, Response } from "express";
import { MembershipOrg, Organization, User } from "../../models";
import { SSOConfig } from "../../ee/models";
import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg";
import { createToken } from "../../helpers/auth";
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services";
import { EELicenseService } from "../../ee/services";
import { ACCEPTED, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables";
import * as reqValidator from "../../validation/membershipOrg";
import {
getJwtSignupLifetime,
getJwtSignupSecret,
getSiteURL,
getSmtpConfigured
} from "../../config";
import { validateUserEmail } from "../../validation";
import { validateRequest } from "../../helpers/validation";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Delete organization membership with id [membershipOrgId] from organization
@ -16,55 +31,39 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured
* @param res
* @returns
*/
export const deleteMembershipOrg = async (req: Request, res: Response) => {
let membershipOrgToDelete;
try {
const { membershipOrgId } = req.params;
export const deleteMembershipOrg = async (req: Request, _res: Response) => {
const {
params: { membershipOrgId }
} = await validateRequest(reqValidator.DelOrgMembershipv1, req);
// 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
});
const { permission, membership: membershipOrg } = await getUserOrgPermissions(
req.user._id,
membershipOrgToDelete.organization.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Member
);
if (!membershipOrg) {
throw new Error('Failed to validate organization membership');
}
// delete organization membership
await deleteMemberFromOrg({
membershipOrgId: membershipOrgToDelete._id.toString()
});
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');
}
await updateSubscriptionOrgQuantity({
organizationId: membershipOrg.organization.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'
});
}
return membershipOrgToDelete;
return membershipOrgToDelete;
};
/**
@ -74,22 +73,14 @@ export const deleteMembershipOrg = async (req: Request, res: Response) => {
* @returns
*/
export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// change role for (target) organization membership with id
// [membershipOrgId]
// change role for (target) organization membership with id
// [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'
// });
// }
let membershipToChangeRole;
return res.status(200).send({
membershipOrg: membershipToChangeRole
});
return res.status(200).send({
membershipOrg: membershipToChangeRole
});
};
/**
@ -100,111 +91,128 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
* @returns
*/
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}`;
let inviteeMembershipOrg, completeInviteLink;
const {
body: { inviteeEmail, organizationId }
} = await validateRequest(reqValidator.InviteUserToOrgv1, req);
// validate membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: organizationId
});
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Member
);
if (!membershipOrg) {
throw new Error('Failed to validate organization membership');
}
const host = req.headers.host;
const siteUrl = `${req.protocol}://${host}`;
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
invitee = await User.findOne({
email: inviteeEmail
}).select('+publicKey');
const ssoConfig = await SSOConfig.findOne({
organization: new Types.ObjectId(organizationId)
});
if (invitee) {
// case: invitee is an existing user
if (ssoConfig && ssoConfig.isActive) {
// case: SAML SSO is enabled for the organization
return res.status(400).send({
message: "Failed to invite member due to SAML SSO configured for organization"
});
}
inviteeMembershipOrg = await MembershipOrg.findOne({
user: invitee._id,
organization: organizationId
});
if (plan.memberLimit !== null) {
// case: limit imposed on number of members allowed
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
throw new Error(
'Failed to invite an existing member of the organization'
);
}
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."
});
}
}
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
});
const invitee = await User.findOne({
email: inviteeEmail
}).select("+publicKey");
if (!inviteeMembershipOrg) {
// case: invitee has never been invited before
if (invitee) {
// case: invitee is an existing user
await new MembershipOrg({
inviteEmail: inviteeEmail,
organization: organizationId,
role: MEMBER,
status: INVITED
}).save();
}
}
inviteeMembershipOrg = await MembershipOrg.findOne({
user: invitee._id,
organization: organizationId
});
const organization = await Organization.findOne({ _id: organizationId });
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
throw new Error("Failed to invite an existing member of the organization");
}
if (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
});
const token = await TokenService.createToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email: inviteeEmail,
organizationId: organization._id
});
if (!inviteeMembershipOrg) {
// case: invitee has never been invited before
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'
}
});
// validate that email is not disposable
validateUserEmail(inviteeEmail);
if (!(await getSmtpConfigured())) {
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`
}
}
await new MembershipOrg({
inviteEmail: inviteeEmail,
organization: organizationId,
role: MEMBER,
status: INVITED
}).save();
}
}
await updateSubscriptionOrgQuantity({ organizationId });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send organization invite'
});
}
const organization = await Organization.findOne({ _id: organizationId });
return res.status(200).send({
message: `Sent an invite link to ${req.body.inviteeEmail}`,
completeInviteLink
});
if (organization) {
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"
}
});
if (!(await getSmtpConfigured())) {
completeInviteLink = `${
siteUrl + "/signupinvite"
}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`;
}
}
await updateSubscriptionOrgQuantity({ organizationId });
return res.status(200).send({
message: `Sent an invite link to ${req.body.inviteeEmail}`,
completeInviteLink
});
};
/**
@ -215,70 +223,64 @@ 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;
user = await User.findOne({ email }).select('+publicKey');
const {
body: { organizationId, email, code }
} = await validateRequest(reqValidator.VerifyUserToOrgv1, req);
const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email,
status: INVITED,
organization: new Types.ObjectId(organizationId)
});
user = await User.findOne({ email }).select("+publicKey");
if (!membershipOrg)
throw new Error('Failed to find any invitations for email');
const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email,
status: INVITED,
organization: new Types.ObjectId(organizationId)
});
await TokenService.validateToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email,
organizationId: membershipOrg.organization,
token: code
});
if (!membershipOrg) throw new Error("Failed to find any invitations for email");
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 TokenService.validateToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email,
organizationId: membershipOrg.organization,
token: code
});
return res.status(200).send({
message: 'Successfully verified email',
user,
});
}
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();
if (!user) {
// initialize user account
user = await new User({
email
}).save();
}
await updateSubscriptionOrgQuantity({
organizationId
});
// 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'
});
}
return res.status(200).send({
message: "Successfully verified email",
user
});
}
return res.status(200).send({
message: 'Successfully verified email',
user,
token
});
if (!user) {
// initialize user account
user = await new User({
email
}).save();
}
// 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",
user,
token
});
};

View File

@ -1,39 +1,37 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import Stripe from 'stripe';
import { Request, Response } from "express";
import {
Membership,
MembershipOrg,
Organization,
Workspace,
IncidentContactOrg
} from '../../models';
import { createOrganization as create } from '../../helpers/organization';
import { addMembershipsOrg } from '../../helpers/membershipOrg';
import { OWNER, ACCEPTED } from '../../variables';
import _ from 'lodash';
import { getStripeSecretKey, getSiteURL } from '../../config';
IncidentContactOrg,
Membership,
MembershipOrg,
Organization,
Workspace
} from "../../models";
import { createOrganization as create } from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { ACCEPTED, ADMIN } from "../../variables";
import { getLicenseServerUrl, getSiteURL } from "../../config";
import { licenseServerKeyRequest } from "../../config/request";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/organization";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../ee/services/RoleService";
import { OrganizationNotFoundError } from "../../utils/errors";
import { ForbiddenError } from "@casl/ability";
export const getOrganizations = async (req: Request, res: Response) => {
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
});
return res.status(200).send({
organizations
});
};
/**
@ -44,37 +42,26 @@ 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 {
body: { organizationName }
} = await validateRequest(reqValidator.CreateOrgv1, req);
if (organizationName.length < 1) {
throw new Error('Organization names must be at least 1-character long');
}
// create organization and add user as member
const organization = await create({
email: req.user.email,
name: organizationName
});
// create organization and add user as member
organization = await create({
email: req.user.email,
name: organizationName
});
await addMembershipsOrg({
userIds: [req.user._id.toString()],
organizationId: organization._id.toString(),
roles: [ADMIN],
statuses: [ACCEPTED]
});
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'
});
}
return res.status(200).send({
organization
});
return res.status(200).send({
organization
});
};
/**
@ -84,20 +71,23 @@ 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 {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgv1, req);
return res.status(200).send({
organization
});
// ensure user has membership
await getUserOrgPermissions(req.user._id, organizationId);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
return res.status(200).send({
organization
});
};
/**
@ -107,24 +97,23 @@ 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 {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
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 { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Member
);
return res.status(200).send({
users
});
const users = await MembershipOrg.find({
organization: organizationId
}).populate("user", "+publicKey");
return res.status(200).send({
users
});
};
/**
@ -133,43 +122,39 @@ export const getOrganizationMembers = async (req: Request, res: Response) => {
* @param res
* @returns
*/
export const getOrganizationWorkspaces = async (
req: Request,
res: Response
) => {
let workspaces;
try {
const { organizationId } = req.params;
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgWorkspacesv1, req);
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
'_id'
)
).map((w) => w._id.toString())
);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Workspace
);
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 workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
"_id"
)
).map((w) => w._id.toString())
);
return res.status(200).send({
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,34 +164,33 @@ 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 {
params: { organizationId },
body: { name }
} = await validateRequest(reqValidator.ChangeOrgNamev1, req);
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 { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Settings
);
return res.status(200).send({
message: 'Successfully changed organization name',
organization
});
const organization = await Organization.findOneAndUpdate(
{
_id: organizationId
},
{
name
},
{
new: true
}
);
return res.status(200).send({
message: "Successfully changed organization name",
organization
});
};
/**
@ -215,28 +199,24 @@ export const changeOrganizationName = async (req: Request, res: Response) => {
* @param res
* @returns
*/
export const getOrganizationIncidentContacts = async (
req: Request,
res: Response
) => {
let incidentContactsOrg;
try {
const { organizationId } = req.params;
export const getOrganizationIncidentContacts = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgIncidentContactv1, req);
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 { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.IncidentAccount
);
return res.status(200).send({
incidentContactsOrg
});
const incidentContactsOrg = await IncidentContactOrg.find({
organization: organizationId
});
return res.status(200).send({
incidentContactsOrg
});
};
/**
@ -245,31 +225,27 @@ export const getOrganizationIncidentContacts = async (
* @param res
* @returns
*/
export const addOrganizationIncidentContact = async (
req: Request,
res: Response
) => {
let incidentContactOrg;
try {
const { organizationId } = req.params;
const { email } = req.body;
export const addOrganizationIncidentContact = async (req: Request, res: Response) => {
const {
params: { organizationId },
body: { email }
} = await validateRequest(reqValidator.CreateOrgIncideContact, req);
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 { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.IncidentAccount
);
return res.status(200).send({
incidentContactOrg
});
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
{ email, organization: organizationId },
{ email, organization: organizationId },
{ upsert: true, new: true }
);
return res.status(200).send({
incidentContactOrg
});
};
/**
@ -278,151 +254,137 @@ export const addOrganizationIncidentContact = async (
* @param res
* @returns
*/
export const deleteOrganizationIncidentContact = async (
req: Request,
res: Response
) => {
let incidentContactOrg;
try {
const { organizationId } = req.params;
const { email } = req.body;
export const deleteOrganizationIncidentContact = async (req: Request, res: Response) => {
const {
params: { organizationId },
body: { email }
} = await validateRequest(reqValidator.DelOrgIncideContact, req);
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 { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.IncidentAccount
);
return res.status(200).send({
message: 'Successfully deleted organization incident contact',
incidentContactOrg
});
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
email,
organization: organizationId
});
return res.status(200).send({
message: "Successfully deleted organization incident contact",
incidentContactOrg
});
};
/**
* Redirect user to (stripe) billing portal or add card page depending on
* Redirect user to billing portal or add card page depending on
* if there is a card on file
* @param req
* @param res
* @returns
*/
export const createOrganizationPortalSession = async (
req: Request,
res: Response
) => {
let session;
try {
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
export const createOrganizationPortalSession = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
// 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'
});
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Billing
);
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'
});
}
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { pmtMethods }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`
);
if (pmtMethods.length < 1) {
// case: organization has no payment method on file
// -> redirect to add payment method portal
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`,
{
success_url: (await getSiteURL()) + "/dashboard",
cancel_url: (await getSiteURL()) + "/dashboard"
}
);
return res.status(200).send({ url });
} else {
// case: organization has payment method on file
// -> redirect to billing portal
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/billing-portal`,
{
return_url: (await getSiteURL()) + "/dashboard"
}
);
return res.status(200).send({ url });
}
};
/**
* Return organization subscriptions
* @param req
* @param res
* @returns
*/
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'
});
}
return res.status(200).send({
subscriptions
});
};
/**
* Given a org id, return the projects each member of the org belongs to
* @param req
* @param res
* @returns
*/
export const getOrganizationMembersAndTheirWorkspaces = async (
req: Request,
res: Response
) => {
const { organizationId } = req.params;
export const getOrganizationMembersAndTheirWorkspaces = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
const workspacesSet = (
await Workspace.find(
{
organization: organizationId
},
'_id'
)
).map((w) => w._id.toString());
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Member
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Workspace
);
const memberships = (
await Membership.find({
workspace: { $in: workspacesSet }
}).populate('workspace')
);
const userToWorkspaceIds: any = {};
const workspacesSet = (
await Workspace.find(
{
organization: organizationId
},
"_id"
)
).map((w) => w._id.toString());
memberships.forEach(membership => {
const user = membership.user.toString();
if (userToWorkspaceIds[user]) {
userToWorkspaceIds[user].push(membership.workspace);
} else {
userToWorkspaceIds[user] = [membership.workspace];
}
});
const memberships = await Membership.find({
workspace: { $in: workspacesSet }
}).populate("workspace");
const userToWorkspaceIds: any = {};
return res.json(userToWorkspaceIds);
};
memberships.forEach((membership) => {
const user = membership.user.toString();
if (userToWorkspaceIds[user]) {
userToWorkspaceIds[user].push(membership.workspace);
} else {
userToWorkspaceIds[user] = [membership.workspace];
}
});
return res.json(userToWorkspaceIds);
};

View File

@ -1,113 +1,105 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Request, Response } from "express";
// 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 { TokenService } from '../../services';
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables';
import { BadRequestError } from '../../utils/errors';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config';
const jsrp = require("jsrp");
import * as bigintConversion from "bigint-conversion";
import { BackupPrivateKey, LoginSRPDetail, User } from "../../models";
import { clearTokens, createToken, sendMail } from "../../helpers";
import { TokenService } from "../../services";
import { TOKEN_EMAIL_PASSWORD_RESET } from "../../variables";
import { BadRequestError } from "../../utils/errors";
import {
getHttpsEnabled,
getJwtSignupLifetime,
getJwtSignupSecret,
getSiteURL
} from "../../config";
import { ActorType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
/**
* Password reset step 1: Send email verification link to email [email]
* Password reset step 1: Send email verification link to email [email]
* for account recovery.
* @param req
* @param res
* @returns
*/
export const emailPasswordReset = async (req: Request, res: Response) => {
let email: string;
try {
email = req.body.email;
const {
body: { email }
} = await validateRequest(reqValidator.EmailPasswordResetV1, req);
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"
});
}
return res.status(200).send({
message: `Sent an email for account recovery to ${email}`
});
}
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: "If an account exists with this email, a password reset link has been sent"
});
};
/**
* Password reset step 2: Verify email verification link sent to email [email]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
let user, token;
try {
const { email, code } = req.body;
const {
body: { email, code }
} = await validateRequest(reqValidator.EmailPasswordResetVerifyV1, req);
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"
});
}
// 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'
});
}
await TokenService.validateToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email,
token: code
});
return res.status(200).send({
message: 'Successfully verified email',
user,
token
});
}
// 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",
user,
token
});
};
/**
* Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
@ -116,44 +108,43 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
* @returns
*/
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');
// return salt, serverPublicKey as part of first step of SRP protocol
const {
body: { clientPublicKey }
} = await validateRequest(reqValidator.Srp1V1, req);
if (!user) throw new Error('Failed to find user');
const user = await User.findOne({
email: req.user.email
}).select("+salt +verifier");
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
if (!user) throw new Error("Failed to find user");
await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, {
email: req.user.email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false })
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
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'
});
}
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
});
}
);
};
/**
@ -165,80 +156,91 @@ 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 {
body: {
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
}
} = await validateRequest(reqValidator.ChangePasswordV1, req);
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
}
);
return res.status(200).send({
message: 'Successfully changed password'
});
}
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId);
}
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?'
});
}
// clear httpOnly cookie
res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: (await getHttpsEnabled()) as boolean
});
return res.status(200).send({
message: "Successfully changed password"
});
}
return res.status(400).send({
error: "Failed to change password. Try again?"
});
}
);
};
/**
@ -248,141 +250,120 @@ export const changePassword = async (req: Request, res: Response) => {
* @returns
*/
export const createBackupPrivateKey = async (req: Request, res: Response) => {
// create/change backup private key
// requires verifying [clientProof] as part of second step of SRP protocol
// as initiated in /srp1
// create/change backup private key
// requires verifying [clientProof] as part of second step of SRP protocol
// as initiated in /srp1
const {
body: { clientProof, encryptedPrivateKey, salt, verifier, iv, tag }
} = await validateRequest(reqValidator.CreateBackupPrivateKeyV1, req);
const user = await User.findOne({
email: req.user.email
}).select("+salt +verifier");
try {
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"
});
}
);
};
/**
* Return backup private key for user
* @param req
* @param res
* @returns
* @param req
* @param res
* @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
});
}
return res.status(200).send({
backupPrivateKey
});
};
export const resetPassword = async (req: Request, res: Response) => {
try {
const {
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
} = req.body;
const {
body: {
encryptedPrivateKey,
protectedKeyTag,
protectedKey,
protectedKeyIV,
salt,
verifier,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag
}
} = await validateRequest(reqValidator.ResetPasswordV1, req);
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'
});
}
return res.status(200).send({
message: "Successfully reset password"
});
};

View File

@ -1,31 +1,30 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Key, Secret } from '../../models';
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Key } from "../../models";
import {
v1PushSecrets as push,
pullSecrets as pull,
reformatPullSecrets
} from '../../helpers/secret';
import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../../events';
import { EventService } from '../../services';
import { TelemetryService } from '../../services';
pullSecrets as pull,
v1PushSecrets as push,
reformatPullSecrets
} from "../../helpers/secret";
import { pushKeys } from "../../helpers/key";
import { eventPushSecrets } from "../../events";
import { EventService } from "../../services";
import { TelemetryService } from "../../services";
interface PushSecret {
ciphertextKey: string;
ivKey: string;
tagKey: string;
hashKey: string;
ciphertextValue: string;
ivValue: string;
tagValue: string;
hashValue: string;
ciphertextComment: string;
ivComment: string;
tagComment: string;
hashComment: string;
type: 'shared' | 'personal';
ciphertextKey: string;
ivKey: string;
tagKey: string;
hashKey: string;
ciphertextValue: string;
ivValue: string;
tagValue: string;
hashValue: string;
ciphertextComment: string;
ivComment: string;
tagComment: string;
hashComment: string;
type: "shared" | "personal";
}
/**
@ -36,71 +35,59 @@ interface PushSecret {
* @returns
*/
export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
// 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
});
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
})
});
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath: "/"
})
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload workspace secrets'
});
}
return res.status(200).send({
message: 'Successfully uploaded workspace secrets'
});
return res.status(200).send({
message: "Successfully uploaded workspace secrets"
});
};
/**
@ -111,64 +98,56 @@ export const pushSecrets = async (req: Request, res: Response) => {
* @returns
*/
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;
let secrets;
// 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'
});
}
const key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.populate("sender", "+publicKey");
return res.status(200).send({
secrets,
key
});
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"
}
});
}
return res.status(200).send({
secrets,
key
});
};
/**
@ -180,61 +159,51 @@ export const pullSecrets = async (req: Request, res: Response) => {
* @returns
*/
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;
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');
}
// 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: req.serviceToken.user._id.toString(),
workspaceId,
environment,
channel: 'cli',
ipAddress: req.ip
});
const secrets = await pull({
userId: req.serviceToken.user._id.toString(),
workspaceId,
environment,
channel: "cli",
ipAddress: req.realIP
});
key = {
encryptedKey: req.serviceToken.encryptedKey,
nonce: req.serviceToken.nonce,
sender: {
publicKey: req.serviceToken.publicKey
},
receiver: req.serviceToken.user,
workspace: req.serviceToken.workspace
};
const 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'
}
});
}
} catch (err) {
Sentry.setUser({ email: req.serviceToken.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
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"
}
});
}
return res.status(200).send({
secrets: reformatPullSecrets({ secrets }),
key
});
};
return res.status(200).send({
secrets: reformatPullSecrets({ secrets }),
key
});
};

View File

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

View File

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

View File

@ -1,107 +1,745 @@
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 { ForbiddenError, subject } from "@casl/ability";
import { Request, Response } from "express";
import { Types } from "mongoose";
import { EventType, FolderVersion } from "../../ee/models";
import { EEAuditLogService, EESecretService } from "../../ee/services";
import { isValidScope } from "../../helpers/secrets";
import { validateRequest } from "../../helpers/validation";
import { Secret, ServiceTokenData } from "../../models";
import { Folder } from "../../models/folder";
import {
appendFolder,
deleteFolderById,
generateFolderId,
getAllFolderIds,
getFolderByPath,
getFolderWithPathFromId,
getParentFromFolderId,
validateFolderName
} from "../../services/FolderService";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/folders";
// TODO
// verify workspace id/environment
/**
* Create folder with name [folderName] for workspace with id [workspaceId]
* and environment [environment]
* @param req
* @param res
* @returns
*/
export const createFolder = async (req: Request, res: Response) => {
const { workspaceId, environment, folderName, parentFolderId } = req.body
/*
#swagger.summary = 'Create a folder'
#swagger.description = 'Create a new folder in a specified workspace and environment'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of the workspace where the folder will be created",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Environment where the folder will reside",
"example": "production"
},
"folderName": {
"type": "string",
"description": "Name of the folder to be created",
"example": "my_folder"
},
"parentFolderId": {
"type": "string",
"description": "ID of the parent folder under which this folder will be created. If not specified, it will be created at the root level.",
"example": "someParentFolderId"
}
},
"required": ["workspaceId", "environment", "folderName"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"folder": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "someFolderId"
},
"name": {
"type": "string",
"example": "my_folder"
}
},
"description": "Details of the created folder"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. For example, 'Folder name cannot contain spaces. Only underscore and dashes'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
body: { workspaceId, environment, folderName, parentFolderId }
} = await validateRequest(reqValidator.CreateFolderV1, req);
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" })
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (req.user) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const secretPath =
folders && parentFolderId
? getFolderWithPathFromId(folders.nodes, parentFolderId).folderPath
: "/";
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
// space has no folders initialized
if (!folders) {
if (req.authData.authPayload instanceof ServiceTokenData) {
// root check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/");
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const id = generateFolderId();
const folder = new Folder({
workspace: workspaceId,
environment,
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: new Types.ObjectId(workspaceId),
environment
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: id,
folderName,
folderPath: `root/${folderName}`
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.json({ folder: { id, name: folderName } });
}
const folder = appendFolder(folders.nodes, { folderName, parentFolderId });
await Folder.findByIdAndUpdate(folders._id, folders);
const { folder: parentFolder, folderPath: parentFolderPath } = getFolderWithPathFromId(
folders.nodes,
parentFolderId || "root"
);
if (req.authData.authPayload instanceof ServiceTokenData) {
// root check
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
environment,
parentFolderPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
let completePath = await getFolderPath(parentFolderId)
if (completePath == ROOT_FOLDER_PATH) {
completePath = ""
}
await Folder.findByIdAndUpdate(folders._id, folders);
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 folderVersion = new FolderVersion({
workspace: workspaceId,
environment: environment,
parent: parentFolderId,
path: normalizedCurrentPath
environment,
nodes: parentFolder
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolderId
});
if (existingFolder) {
return res.json(existingFolder)
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: folder.id,
folderName,
folderPath
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.json({ folder });
};
/**
* Update folder with id [folderId]
* @param req
* @param res
* @returns
*/
export const updateFolderById = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update a folder by ID'
#swagger.description = 'Update the name of a folder in a specified workspace and environment by its ID'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['folderId'] = {
"description": "ID of the folder to be updated",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of the workspace where the folder is located",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Environment where the folder is located",
"example": "production"
},
"name": {
"type": "string",
"description": "New name for the folder",
"example": "updated_folder_name"
}
},
"required": ["workspaceId", "environment", "name"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Successfully updated folder"
},
"folder": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "updated_folder_name"
},
"id": {
"type": "string",
"example": "someFolderId"
}
},
"description": "Details of the updated folder"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. Reasons can include 'The folder doesn't exist' or 'Folder name cannot contain spaces. Only underscore and dashes'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
body: { workspaceId, environment, name },
params: { folderId }
} = await validateRequest(reqValidator.UpdateFolderV1, req);
if (!validateFolderName(name)) {
throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes"
});
}
const newFolder = new Folder({
name: folderName,
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const parentFolder = getParentFromFolderId(folders.nodes, folderId);
if (!parentFolder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
if (req.user) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const secretPath = getFolderWithPathFromId(folders.nodes, parentFolder.id).folderPath;
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
const folder = parentFolder.children.find(({ id }) => id === folderId);
if (!folder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
if (req.authData.authPayload instanceof ServiceTokenData) {
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
// root check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const oldFolderName = folder.name;
parentFolder.version += 1;
folder.name = name;
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment: environment,
parent: parentFolderId,
path: normalizedCurrentPath,
parentPath: normalizedParentPath
environment,
nodes: parentFolder
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolder.id
});
await newFolder.save();
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
return res.json(newFolder)
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_FOLDER,
metadata: {
environment,
folderId: folder.id,
oldFolderName,
newFolderName: name,
folderPath
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.json({
message: "Successfully updated folder",
folder: { name: folder.name, id: folder.id }
});
};
/**
* Delete folder with id [folderId]
* @param req
* @param res
* @returns
*/
export const deleteFolder = async (req: Request, res: Response) => {
const { folderId } = req.params
const queue: any[] = [folderId];
/*
#swagger.summary = 'Delete a folder by ID'
#swagger.description = 'Delete the specified folder from a specified workspace and environment using its ID'
#swagger.security = [{
"apiKeyAuth": []
}]
const folder = await Folder.findById(folderId);
if (!folder) {
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]
});
while (queue.length > 0) {
const currentFolderId = queue.shift();
const childFolders = await Folder.find({ parent: currentFolderId });
for (const childFolder of childFolders) {
queue.push(childFolder._id);
#swagger.parameters['folderId'] = {
"description": "ID of the folder to be deleted",
"required": true,
"type": "string",
"in": "path"
}
await Secret.deleteMany({ folder: currentFolderId });
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of the workspace where the folder is located",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Environment where the folder is located",
"example": "production"
}
},
"required": ["workspaceId", "environment"]
}
}
}
}
await Folder.deleteOne({ _id: currentFolderId });
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "successfully deleted folders"
},
"folders": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "someFolderId"
},
"name": {
"type": "string",
"example": "someFolderName"
}
}
},
"description": "List of IDs and names of the deleted folders"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. Reasons can include 'The folder doesn't exist'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
params: { folderId },
body: { environment, workspaceId }
} = await validateRequest(reqValidator.DeleteFolderV1, req);
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
res.send()
}
// TODO: validate workspace
export const getFolderById = async (req: Request, res: Response) => {
const { folderId } = req.params
const folder = await Folder.findById(folderId);
if (!folder) {
throw BadRequestError({ message: "The folder doesn't exist" })
const delOp = deleteFolderById(folders.nodes, folderId);
if (!delOp) {
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]
const { deletedNode: delFolder, parent: parentFolder } = delOp;
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// check that user is a member of the workspace
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
parentFolder.version += 1;
const delFolderIds = getAllFolderIds(delFolder);
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
});
}
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolder.id
});
res.send({ folder })
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_FOLDER,
metadata: {
environment,
folderId,
folderName: delFolder.name,
folderPath: secretPath
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
res.send({ message: "successfully deleted folders", folders: delFolderIds });
};
/**
* Get folders for workspace with id [workspaceId] and environment [environment]
* considering [parentFolderId] and [parentFolderPath]
* @param req
* @param res
* @returns
*/
export const getFolders = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Retrieve folders based on specific conditions'
#swagger.description = 'Fetches folders from the specified workspace and environment, optionally providing either a parentFolderId or a parentFolderPath to narrow down results'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of the workspace from which the folders are to be fetched",
"required": true,
"type": "string",
"in": "query"
}
#swagger.parameters['environment'] = {
"description": "Environment where the folder is located",
"required": true,
"type": "string",
"in": "query"
}
#swagger.parameters['parentFolderId'] = {
"description": "ID of the parent folder",
"required": false,
"type": "string",
"in": "query"
}
#swagger.parameters['parentFolderPath'] = {
"description": "Path of the parent folder, like /folder1/folder2",
"required": false,
"type": "string",
"in": "query"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"folders": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "someFolderId"
},
"name": {
"type": "string",
"example": "someFolderName"
}
}
},
"description": "List of folders"
},
"dir": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "parentFolderName"
},
"id": {
"type": "string",
"example": "parentFolderId"
}
}
},
"description": "List of directories"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. For instance, 'The folder doesn't exist'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
query: { workspaceId, environment, parentFolderId, parentFolderPath }
} = await validateRequest(reqValidator.GetFoldersV1, req);
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (req.user) await getUserProjectPermissions(req.user._id, workspaceId);
if (!folders) {
res.send({ folders: [], dir: [] });
return;
}
// if instead of parentFolderId given a path like /folder1/folder2
if (parentFolderPath) {
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
environment,
parentFolderPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folder = getFolderByPath(folders.nodes, parentFolderPath);
if (!folder) {
res.send({ folders: [], dir: [] });
return;
}
// dir is not needed at present as this is only used in overview section of secrets
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir: [{ name: folder.name, id: folder.id }]
});
}
if (!parentFolderId) {
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/");
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const rootFolders = folders.nodes.children.map(({ id, name }) => ({
id,
name
}));
res.send({ folders: rootFolders });
return;
}
const { folder, folderPath, dir } = getFolderWithPathFromId(folders.nodes, parentFolderId);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, folderPath);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir
});
};

View File

@ -1,7 +1,7 @@
import { Request, Response } from 'express';
import { ServiceToken } from '../../models';
import { createToken } from '../../helpers/auth';
import { getJwtServiceSecret } from '../../config';
import { Request, Response } from "express";
import { ServiceToken } from "../../models";
import { createToken } from "../../helpers/auth";
import { getJwtServiceSecret } from "../../config";
/**
* Return service token on request
@ -11,7 +11,7 @@ import { getJwtServiceSecret } from '../../config';
*/
export const getServiceToken = async (req: Request, res: Response) => {
return res.status(200).send({
serviceToken: req.serviceToken
serviceToken: req.serviceToken,
});
};
@ -31,13 +31,13 @@ export const createServiceToken = async (req: Request, res: Response) => {
expiresIn,
publicKey,
encryptedKey,
nonce
nonce,
} = req.body;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
throw new Error("Failed to validate environment");
}
// compute access token expiration date
@ -52,24 +52,24 @@ export const createServiceToken = async (req: Request, res: Response) => {
expiresAt,
publicKey,
encryptedKey,
nonce
nonce,
}).save();
token = createToken({
payload: {
serviceTokenId: serviceToken._id.toString(),
workspaceId
workspaceId,
},
expiresIn: expiresIn,
secret: await getJwtServiceSecret()
secret: await getJwtServiceSecret(),
});
} catch (err) {
return res.status(400).send({
message: 'Failed to create service token'
message: "Failed to create service token",
});
}
return res.status(200).send({
token
token,
});
};

View File

@ -1,13 +1,17 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { User } from '../../models';
import { Request, Response } from "express";
import { AuthMethod, User } from "../../models";
import { checkEmailVerification, sendEmailVerification } from "../../helpers/signup";
import { createToken } from "../../helpers/auth";
import { BadRequestError } from "../../utils/errors";
import {
sendEmailVerification,
checkEmailVerification,
} from '../../helpers/signup';
import { createToken } from '../../helpers/auth';
import { BadRequestError } from '../../utils/errors';
import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
getInviteOnlySignup,
getJwtSignupLifetime,
getJwtSignupSecret,
getSmtpConfigured
} from "../../config";
import { validateUserEmail } from "../../validation";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
/**
* Signup step 1: Initialize account for user under email [email] and send a verification code
@ -17,40 +21,28 @@ import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpC
* @returns
*/
export const beginEmailSignup = async (req: Request, res: Response) => {
let email: string;
try {
email = req.body.email;
const {
body: { email }
} = await validateRequest(reqValidator.BeginEmailSignUpV1, req);
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." })
}
}
// 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 });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to send email verification code'
});
}
// send send verification email
await sendEmailVerification({ email });
return res.status(200).send({
message: `Sent an email verification code to ${email}`
});
return res.status(200).send({
message: `Sent an email verification code to ${email}`
});
};
/**
@ -61,52 +53,57 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
* @returns
*/
export const verifyEmailSignup = async (req: Request, res: Response) => {
let user, token;
try {
const { email, code } = req.body;
let user;
const {
body: { email, code }
} = await validateRequest(reqValidator.VerifyEmailSignUpV1, req);
// 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"
});
}
// verify email
if (await getSmtpConfigured()) {
await checkEmailVerification({
email,
code
});
}
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 (!user) {
user = await new User({
email
}).save();
}
// verify email
if (await getSmtpConfigured()) {
await checkEmailVerification({
email,
code
});
}
// 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'
});
}
if (!user) {
user = await new User({
email,
authMethods: [AuthMethod.EMAIL]
}).save();
}
return res.status(200).send({
message: 'Successfuly verified email',
user,
token
});
// generate temporary signup token
const token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret()
});
return res.status(200).send({
message: "Successfuly verified email",
user,
token
});
};

View File

@ -1,41 +0,0 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import Stripe from 'stripe';
import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
/**
* Handle service provisioning/un-provisioning via Stripe
* @param req
* @param res
* @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'
});
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'
});
}
switch (event.type) {
case '':
break;
default:
}
return res.json({ received: true });
};

View File

@ -1,6 +1,7 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { UserAction } from '../../models';
import { Request, Response } from "express";
import { validateRequest } from "../../helpers/validation";
import { UserAction } from "../../models";
import * as reqValidator from "../../validation/action";
/**
* Add user action [action]
@ -9,35 +10,27 @@ import { UserAction } from '../../models';
* @returns
*/
export const addUserAction = async (req: Request, res: Response) => {
// add/record new action [action] for user with id [req.user._id]
// add/record new action [action] for user with id [req.user._id]
const {
body: { action }
} = await validateRequest(reqValidator.AddUserActionV1, req);
let userAction;
try {
const { action } = req.body;
const userAction = await UserAction.findOneAndUpdate(
{
user: req.user._id,
action
},
{ user: req.user._id, action },
{
new: true,
upsert: true
}
);
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'
});
}
return res.status(200).send({
message: 'Successfully recorded user action',
userAction
});
return res.status(200).send({
message: "Successfully recorded user action",
userAction
});
};
/**
@ -47,24 +40,17 @@ export const addUserAction = async (req: Request, res: Response) => {
* @returns
*/
export const getUserAction = async (req: Request, res: Response) => {
// get user action [action] for user with id [req.user._id]
let userAction;
try {
const action: string = req.query.action as string;
// get user action [action] for user with id [req.user._id]
const {
query: { action }
} = await validateRequest(reqValidator.GetUserActionV1, req);
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
});
return res.status(200).send({
userAction
});
};

View File

@ -1,4 +1,4 @@
import { Request, Response } from 'express';
import { Request, Response } from "express";
/**
* Return user on request
@ -8,6 +8,6 @@ import { Request, Response } from 'express';
*/
export const getUser = async (req: Request, res: Response) => {
return res.status(200).send({
user: req.user
user: req.user,
});
};

View File

@ -0,0 +1,239 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { client, getRootEncryptionKey } from "../../config";
import { Webhook } from "../../models";
import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService";
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64 } from "../../variables";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/webhooks";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
export const createWebhook = async (req: Request, res: Response) => {
const {
body: { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath }
} = await validateRequest(reqValidator.CreateWebhookV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Webhooks
);
const webhook = new Webhook({
workspace: workspaceId,
environment,
secretPath,
url: webhookUrl,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64
});
if (webhookSecretKey) {
const rootEncryptionKey = await getRootEncryptionKey();
const { ciphertext, iv, tag } = client.encryptSymmetric(webhookSecretKey, rootEncryptionKey);
webhook.iv = iv;
webhook.tag = tag;
webhook.encryptedSecretKey = ciphertext;
}
await webhook.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_WEBHOOK,
metadata: {
webhookId: webhook._id.toString(),
environment,
secretPath,
webhookUrl,
isDisabled: false
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
webhook,
message: "successfully created webhook"
});
};
export const updateWebhook = async (req: Request, res: Response) => {
const {
body: { isDisabled },
params: { webhookId }
} = await validateRequest(reqValidator.UpdateWebhookV1, req);
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
const { permission } = await getUserProjectPermissions(
req.user._id,
webhook.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Webhooks
);
if (typeof isDisabled !== undefined) {
webhook.isDisabled = isDisabled;
}
await webhook.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_WEBHOOK_STATUS,
metadata: {
webhookId: webhook._id.toString(),
environment: webhook.environment,
secretPath: webhook.secretPath,
webhookUrl: webhook.url,
isDisabled
}
},
{
workspaceId: webhook.workspace
}
);
return res.status(200).send({
webhook,
message: "successfully updated webhook"
});
};
export const deleteWebhook = async (req: Request, res: Response) => {
const {
params: { webhookId }
} = await validateRequest(reqValidator.DeleteWebhookV1, req);
let webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw ResourceNotFoundError({ message: "Webhook not found!!" });
}
const { permission } = await getUserProjectPermissions(
req.user._id,
webhook.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Webhooks
);
webhook = await Webhook.findByIdAndDelete(webhookId);
if (!webhook) {
throw ResourceNotFoundError({ message: "Webhook not found!!" });
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_WEBHOOK,
metadata: {
webhookId: webhook._id.toString(),
environment: webhook.environment,
secretPath: webhook.secretPath,
webhookUrl: webhook.url,
isDisabled: webhook.isDisabled
}
},
{
workspaceId: webhook.workspace
}
);
return res.status(200).send({
message: "successfully removed webhook"
});
};
export const testWebhook = async (req: Request, res: Response) => {
const {
params: { webhookId }
} = await validateRequest(reqValidator.TestWebhookV1, req);
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
const { permission } = await getUserProjectPermissions(
req.user._id,
webhook.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks
);
try {
await triggerWebhookRequest(
webhook,
getWebhookPayload(
"test",
webhook.workspace.toString(),
webhook.environment,
webhook.secretPath
)
);
await Webhook.findByIdAndUpdate(webhookId, {
lastStatus: "success",
lastRunErrorMessage: null
});
} catch (err) {
await Webhook.findByIdAndUpdate(webhookId, {
lastStatus: "failed",
lastRunErrorMessage: (err as Error).message
});
return res.status(400).send({
message: "Failed to receive response",
error: (err as Error).message
});
}
return res.status(200).send({
message: "Successfully received response"
});
};
export const listWebhooks = async (req: Request, res: Response) => {
const {
query: { environment, workspaceId, secretPath }
} = await validateRequest(reqValidator.ListWebhooksV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks
);
const optionalFilters: Record<string, string> = {};
if (environment) optionalFilters.environment = environment as string;
if (secretPath) optionalFilters.secretPath = secretPath as string;
const webhooks = await Webhook.find({
workspace: new Types.ObjectId(workspaceId as string),
...optionalFilters
});
return res.status(200).send({
webhooks
});
};

View File

@ -1,21 +1,32 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import {
Workspace,
Membership,
MembershipOrg,
IUser,
Integration,
IntegrationAuth,
IUser,
Membership,
Organization,
ServiceToken,
ServiceTokenData,
Workspace
} from "../../models";
import {
createWorkspace as create,
deleteWorkspace as deleteWork,
} from "../../helpers/workspace";
import { createWorkspace as create, deleteWorkspace as deleteWork } from "../../helpers/workspace";
import { EELicenseService } from "../../ee/services";
import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables";
import { OrganizationNotFoundError } from "../../utils/errors";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
/**
* Return public keys of members of workspace with id [workspaceId]
@ -24,30 +35,29 @@ import { ADMIN } from "../../variables";
* @returns
*/
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
let publicKeys;
try {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspacePublicKeysV1, req);
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 { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
);
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,
publicKeys
});
};
@ -58,23 +68,22 @@ 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 {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV1, req);
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 { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
);
const users = await Membership.find({
workspace: workspaceId
}).populate("user", "+publicKey");
return res.status(200).send({
users,
users
});
};
@ -85,23 +94,14 @@ 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,
workspaces
});
};
@ -112,23 +112,16 @@ 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 {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceV1, req);
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,
workspace
});
};
@ -140,45 +133,54 @@ 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 {
body: { organizationId, workspaceName }
} = await validateRequest(reqValidator.CreateWorkspaceV1, req);
// 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",
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Workspace
);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (plan.workspaceLimit !== null) {
// case: limit imposed on number of workspaces allowed
if (plan.workspacesUsed >= plan.workspaceLimit) {
// case: number of workspaces used exceeds the number of workspaces allowed
return res.status(400).send({
message:
"Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
});
}
}
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: new Types.ObjectId(organizationId)
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN]
});
return res.status(200).send({
workspace,
workspace
});
};
@ -189,23 +191,23 @@ export const createWorkspace = async (req: Request, res: Response) => {
* @returns
*/
export const deleteWorkspace = async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.DeleteWorkspaceV1, req);
// 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",
});
}
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Workspace
);
// delete workspace
await deleteWork({
id: workspaceId
});
return res.status(200).send({
message: "Successfully deleted workspace",
message: "Successfully deleted workspace"
});
};
@ -216,33 +218,32 @@ 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 {
params: { workspaceId },
body: { name }
} = await validateRequest(reqValidator.ChangeWorkspaceNameV1, req);
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 { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Workspace
);
const workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId
},
{
name
},
{
new: true
}
);
return res.status(200).send({
message: "Successfully changed workspace name",
workspace,
workspace
});
};
@ -253,23 +254,21 @@ 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 {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceIntegrationsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
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,
integrations
});
};
@ -279,27 +278,23 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
* @param res
* @returns
*/
export const getWorkspaceIntegrationAuthorizations = async (
req: Request,
res: Response
) => {
let authorizations;
try {
const { workspaceId } = req.params;
export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceIntegrationAuthorizationsV1, req);
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 { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
const authorizations = await IntegrationAuth.find({
workspace: workspaceId
});
return res.status(200).send({
authorizations,
authorizations
});
};
@ -309,27 +304,24 @@ export const getWorkspaceIntegrationAuthorizations = async (
* @param res
* @returns
*/
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",
});
}
export const getWorkspaceServiceTokens = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceServiceTokensV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.ServiceTokens
);
// ?? FIX.
const serviceTokens = await ServiceToken.find({
user: req.user._id,
workspace: workspaceId
});
return res.status(200).send({
serviceTokens,
serviceTokens
});
};

View File

@ -1,104 +0,0 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
APIKeyData
} from '../../models';
import { getSaltRounds } from '../../config';
/**
* Return API key data for user with id [req.user_id]
* @param req
* @param res
* @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'
});
}
return res.status(200).send({
apiKeyData
});
}
/**
* Create new API key data for user with id [req.user._id]
* @param req
* @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'
});
}
return res.status(200).send({
apiKey,
apiKeyData
});
}
/**
* Delete API key data with id [apiKeyDataId].
* @param req
* @param res
* @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'
});
}
return res.status(200).send({
apiKeyData
});
}

View File

@ -1,28 +1,22 @@
/* 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 } 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 { Request, Response } from "express";
import jwt from "jsonwebtoken";
import * as bigintConversion from "bigint-conversion";
const jsrp = require("jsrp");
import { LoginSRPDetail, User } from "../../models";
import { createToken, issueAuthTokens } 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 { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables";
import { getUserAgentType } from "../../utils/posthog"; // TODO: move this
import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module 'jsonwebtoken' {
declare module "jsonwebtoken" {
export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string;
}
@ -35,47 +29,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 }, {
await LoginSRPDetail.findOneAndReplace(
{ email: email },
{
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false });
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,195 +73,185 @@ 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: getUserAgentType(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?"
});
}
);
};
/**
* Send MFA token to email [email]
* @param req
* @param res
* @param req
* @param res
*/
export const sendMfaToken = async (req: Request, res: Response) => {
try {
const { email } = req.body;
const {
body: { email }
} = await validateRequest(reqValidator.SendMfaTokenV2, req);
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'
message: "Successfully sent new MFA code"
});
}
};
/**
* Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
* MFA token [mfaToken] is valid
* @param req
* @param res
* @param req
* @param res
*/
export const verifyMfaToken = async (req: Request, res: Response) => {
const { email, mfaToken } = req.body;
const {
body: { email, mfaToken }
} = await validateRequest(reqValidator.VerifyMfaTokenV2, req);
await TokenService.validateToken({
type: TOKEN_EMAIL_MFA,
@ -284,24 +261,32 @@ 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');
if (!user) throw new Error("Failed to find user");
await LoginSRPDetail.deleteOne({ userId: user.id });
await checkUserDevice({
user,
ip: req.ip,
userAgent: req.headers['user-agent'] ?? ''
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, {
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
@ -336,7 +321,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
encryptedPrivateKey: user.encryptedPrivateKey as string,
iv: user.iv as string,
tag: user.tag as string
}
};
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
resObj.protectedKey = user.protectedKey;
@ -349,12 +334,13 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
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: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP
}));
return res.status(200).send(resObj);
}
};

View File

@ -1,61 +1,236 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
Integration,
Membership,
Secret,
ServiceToken,
Workspace,
Integration,
ServiceTokenData,
Membership,
} from '../../models';
import { SecretVersion } from '../../ee/models';
import { BadRequestError } from '../../utils/errors';
import _ from 'lodash';
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
Workspace
} from "../../models";
import { EventType, SecretVersion } from "../../ee/models";
import { EEAuditLogService, EELicenseService } from "../../ee/services";
import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors";
import _ from "lodash";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/environments";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Create new workspace environment named [environmentName] under workspace with id
* Create new workspace environment named [environmentName]
* with slug [environmentSlug] under workspace with id
* @param req
* @param res
* @returns
*/
export const createWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
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');
}
export const createWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Create environment'
#swagger.description = 'Create environment'
#swagger.security = [{
"apiKeyAuth": []
}]
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',
});
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
/*
#swagger.summary = 'Create environment'
#swagger.description = 'Create environment'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environmentName": {
"type": "string",
"description": "Name of the environment",
"example": "development"
},
"environmentSlug": {
"type": "string",
"description": "Slug of the environment",
"example": "dev-environment"
}
},
"required": ["environmentName", "environmentSlug"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Successfully created new environment"
},
"workspace": {
"type": "string",
"example": "someWorkspaceId"
},
"environment": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "someEnvironmentName"
},
"slug": {
"type": "string",
"example": "someEnvironmentSlug"
}
}
}
},
"description": "Response after creating a new environment"
}
}
}
}
*/
const {
params: { workspaceId },
body: { environmentName, environmentSlug }
} = await validateRequest(reqValidator.CreateWorkspaceEnvironmentV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Environments
);
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) throw WorkspaceNotFoundError();
const plan = await EELicenseService.getPlan(workspace.organization);
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."
});
}
}
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, new Types.ObjectId(workspaceId));
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_ENVIRONMENT,
metadata: {
name: environmentName,
slug: environmentSlug
}
},
{
workspaceId: workspace._id
}
);
return res.status(200).send({
message: 'Successfully created new environment',
message: "Successfully created new environment",
workspace: workspaceId,
environment: {
name: environmentName,
slug: environmentSlug,
},
slug: environmentSlug
}
});
};
/**
* Swaps the ordering of two environments in the database. This is purely for aesthetic purposes.
* @param req
* @param res
* @returns
*/
export const reorderWorkspaceEnvironments = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { environmentName, environmentSlug, otherEnvironmentSlug, otherEnvironmentName }
} = await validateRequest(reqValidator.ReorderWorkspaceEnvironmentsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Environments
);
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw BadRequestError({ message: "Couldn't load workspace" });
}
const environmentIndex = workspace.environments.findIndex(
(env) => env.name === environmentName && env.slug === environmentSlug
);
const otherEnvironmentIndex = workspace.environments.findIndex(
(env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug
);
if (environmentIndex === -1 || otherEnvironmentIndex === -1) {
throw BadRequestError({ message: "environment or otherEnvironment couldn't be found" });
}
// swap the order of the environments
[workspace.environments[environmentIndex], workspace.environments[otherEnvironmentIndex]] = [
workspace.environments[otherEnvironmentIndex],
workspace.environments[environmentIndex]
];
await workspace.save();
return res.status(200).send({
message: "Successfully reordered environments",
workspace: workspaceId
});
};
@ -66,88 +241,173 @@ export const createWorkspaceEnvironment = async (
* @param res
* @returns
*/
export const renameWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
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.');
}
export const renameWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Rename workspace environment'
#swagger.description = 'Rename a specific environment within a workspace'
// 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',
});
#swagger.parameters['workspaceId'] = {
"description": "ID of the workspace",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environmentName": {
"type": "string",
"description": "New name for the environment",
"example": "Staging-Renamed"
},
"environmentSlug": {
"type": "string",
"description": "New slug for the environment",
"example": "staging-renamed"
},
"oldEnvironmentSlug": {
"type": "string",
"description": "Current slug of the environment to rename",
"example": "staging-old"
}
},
"required": ["environmentName", "environmentSlug", "oldEnvironmentSlug"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Successfully update environment"
},
"workspace": {
"type": "string",
"example": "someWorkspaceId"
},
"environment": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "Staging-Renamed"
},
"slug": {
"type": "string",
"example": "staging-renamed"
}
}
}
},
"description": "Details of the renamed environment"
}
}
}
}
*/
const {
params: { workspaceId },
body: { environmentName, environmentSlug, oldEnvironmentSlug }
} = await validateRequest(reqValidator.UpdateWorkspaceEnvironmentV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Environments
);
// user should pass both new slug and env name
if (!environmentSlug || !environmentName) {
throw new Error("Invalid environment given.");
}
// 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");
}
const oldEnvironment = workspace.environments[envIndex];
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 }] }
);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_ENVIRONMENT,
metadata: {
oldName: oldEnvironment.name,
newName: environmentName,
oldSlug: oldEnvironment.slug,
newSlug: environmentSlug.toLowerCase()
}
},
{
workspaceId: workspace._id
}
);
return res.status(200).send({
message: 'Successfully update environment',
message: "Successfully update environment",
workspace: workspaceId,
environment: {
name: environmentName,
slug: environmentSlug,
},
slug: environmentSlug
}
});
};
@ -157,106 +417,238 @@ export const renameWorkspaceEnvironment = async (
* @param res
* @returns
*/
export const deleteWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
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');
export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete workspace environment'
#swagger.description = 'Delete a specific environment from a workspace'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of the workspace",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environmentSlug": {
"type": "string",
"description": "Slug of the environment to delete",
"example": "dev-environment"
}
},
"required": ["environmentSlug"]
}
}
}
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === environmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Successfully deleted environment"
},
"workspace": {
"type": "string",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"example": "dev-environment"
}
},
"description": "Response after deleting an environment from a workspace"
}
}
}
}
*/
const {
params: { workspaceId },
body: { environmentSlug }
} = await validateRequest(reqValidator.DeleteWorkspaceEnvironmentV2, req);
workspace.environments.splice(envIndex, 1);
await workspace.save();
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Environments
);
// 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");
}
return res.status(200).send({
message: 'Successfully deleted environment',
const envIndex = workspace?.environments.findIndex(({ slug }) => slug === environmentSlug);
if (envIndex === -1) {
throw new Error("Invalid environment given");
}
const oldEnvironment = workspace.environments[envIndex];
workspace.environments.splice(envIndex, 1);
await workspace.save();
// clean up
await Secret.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
environment: environmentSlug
});
await SecretVersion.deleteMany({
workspace: workspaceId,
environment: environmentSlug
});
// await ServiceToken.deleteMany({
// workspace: workspaceId,
// environment: environmentSlug,
// });
const result = await ServiceTokenData.updateMany(
{ workspace: workspaceId },
{ $pull: { scopes: { environment: environmentSlug } } }
);
if (result.modifiedCount > 0) {
await ServiceTokenData.deleteMany({ workspace: workspaceId, scopes: { $size: 0 } });
}
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug
});
await Membership.updateMany(
{ workspace: workspaceId },
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
);
await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId));
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_ENVIRONMENT,
metadata: {
name: oldEnvironment.name,
slug: oldEnvironment.slug
}
},
{
workspaceId: workspace._id
}
);
return res.status(200).send({
message: "Successfully deleted environment",
workspace: workspaceId,
environment: environmentSlug
});
};
// TODO(akhilmhdh) after rbac this can be completely removed
export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Get all accessible environments of a workspace'
#swagger.description = 'Fetch all environments that the user has access to in a specified workspace'
#swagger.security = [{
"apiKeyAuth": []
}]
export const getAllAccessibleEnvironmentsOfWorkspace = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const workspacesUserIsMemberOf = await Membership.findOne({
workspace: workspaceId,
user: req.user
})
#swagger.parameters['workspaceId'] = {
"description": "ID of the workspace",
"required": true,
"type": "string",
"in": "path"
}
if (!workspacesUserIsMemberOf) {
throw BadRequestError()
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"accessibleEnvironments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "Development"
},
"slug": {
"type": "string",
"example": "development"
},
"isWriteDenied": {
"type": "boolean",
"example": false
},
"isReadDenied": {
"type": "boolean",
"example": false
}
}
}
}
},
"description": "List of environments the user has access to in the specified workspace"
}
}
}
}
*/
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetAllAccessibileEnvironmentsOfWorkspaceV2, req);
const accessibleEnvironments: any = []
const deniedPermission = workspacesUserIsMemberOf.deniedPermissions
const { membership: workspacesUserIsMemberOf } = await getUserProjectPermissions(
req.user._id,
workspaceId
);
const relatedWorkspace = await Workspace.findById(workspaceId)
const accessibleEnvironments: any = [];
const deniedPermission = workspacesUserIsMemberOf.deniedPermissions;
const relatedWorkspace = await Workspace.findById(workspaceId);
if (!relatedWorkspace) {
throw BadRequestError()
throw BadRequestError();
}
relatedWorkspace.environments.forEach(environment => {
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS })
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS })
relatedWorkspace.environments.forEach((environment) => {
const isReadBlocked = _.some(deniedPermission, {
environmentSlug: environment.slug,
ability: PERMISSION_READ_SECRETS
});
const isWriteBlocked = _.some(deniedPermission, {
environmentSlug: environment.slug,
ability: PERMISSION_WRITE_SECRETS
});
if (isReadBlocked && isWriteBlocked) {
return
return;
} else {
accessibleEnvironments.push({
name: environment.name,
slug: environment.slug,
isWriteDenied: isWriteBlocked,
isReadDenied: isReadBlocked
})
});
}
})
});
res.json({ accessibleEnvironments })
res.json({ accessibleEnvironments });
};

View File

@ -1,15 +1,14 @@
import * as authController from './authController';
import * as signupController from './signupController';
import * as usersController from './usersController';
import * as organizationsController from './organizationsController';
import * as workspaceController from './workspaceController';
import * as serviceTokenDataController from './serviceTokenDataController';
import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController';
import * as secretsController from './secretsController';
import * as serviceAccountsController from './serviceAccountsController';
import * as environmentController from './environmentController';
import * as tagController from './tagController';
import * as authController from "./authController";
import * as signupController from "./signupController";
import * as usersController from "./usersController";
import * as organizationsController from "./organizationsController";
import * as workspaceController from "./workspaceController";
import * as serviceTokenDataController from "./serviceTokenDataController";
import * as secretController from "./secretController";
import * as secretsController from "./secretsController";
import * as serviceAccountsController from "./serviceAccountsController";
import * as environmentController from "./environmentController";
import * as tagController from "./tagController";
export {
authController,
@ -18,10 +17,9 @@ export {
organizationsController,
workspaceController,
serviceTokenDataController,
apiKeyDataController,
secretController,
secretsController,
serviceAccountsController,
environmentController,
tagController
tagController,
}

View File

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

View File

@ -1,25 +1,36 @@
import to from "await-to-js";
import { Request, Response } from "express";
import mongoose, { Types } from "mongoose";
import Secret, { ISecret } from "../../models/secret";
import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCreate, SanitizedSecretModify } from "../../types/secret";
import {
CreateSecretRequestBody,
ModifySecretRequestBody,
SanitizedSecretForCreate,
SanitizedSecretModify
} from "../../types/secret";
const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { TelemetryService } from '../../services';
import { User } from "../../models";
import { AccountNotFoundError } from '../../utils/errors';
import {
ValidationError as RouteValidationError,
UnauthorizedRequestError
} from "../../utils/errors";
import { AnyBulkWriteOperation } from "mongodb";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
SECRET_PERSONAL,
SECRET_SHARED
} from "../../variables";
import { TelemetryService } from "../../services";
import { ISecret, Secret, User } from "../../models";
import { AccountNotFoundError } from "../../utils/errors";
/**
* Create secret for workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
* @param req
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environment } = req.params
const { workspaceId, environment } = req.params;
const sanitizedSecret: SanitizedSecretForCreate = {
secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
secretKeyIV: secretToCreate.secretKeyIV,
@ -36,46 +47,44 @@ export const createSecret = async (req: Request, res: Response) => {
workspace: new Types.ObjectId(workspaceId),
environment,
type: secretToCreate.type,
user: new Types.ObjectId(req.user._id)
}
user: new Types.ObjectId(req.user._id),
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
};
const [error, secret] = await to(Secret.create(sanitizedSecret).then())
if (error instanceof ValidationError) {
throw RouteValidationError({ message: error.message, stack: error.stack })
}
const secret = await new Secret(sanitizedSecret).save();
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
event: "secrets added",
distinctId: req.user.email,
properties: {
numberOfSecrets: 1,
workspaceId,
environment,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
res.status(200).send({
secret
})
}
});
};
/**
* Create many secrets for workspace wiht id [workspaceId] and environment [environment]
* @param req
* @param res
* Create many secrets for workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
*/
export const createSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environment } = req.params
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
const { workspaceId, environment } = req.params;
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [];
secretsToCreate.forEach(rawSecret => {
secretsToCreate.forEach((rawSecret) => {
const safeUpdateFields: SanitizedSecretForCreate = {
secretKeyCiphertext: rawSecret.secretKeyCiphertext,
secretKeyIV: rawSecret.secretKeyIV,
@ -92,141 +101,132 @@ export const createSecrets = async (req: Request, res: Response) => {
workspace: new Types.ObjectId(workspaceId),
environment,
type: rawSecret.type,
user: new Types.ObjectId(req.user._id)
}
user: new Types.ObjectId(req.user._id),
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
};
sanitizedSecretesToCreate.push(safeUpdateFields)
})
sanitizedSecretesToCreate.push(safeUpdateFields);
});
const [bulkCreateError, secrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then())
if (bulkCreateError) {
if (bulkCreateError instanceof ValidationError) {
throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack })
}
throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack })
}
const secrets = await Secret.insertMany(sanitizedSecretesToCreate);
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
event: "secrets added",
distinctId: req.user.email,
properties: {
numberOfSecrets: (secretsToCreate ?? []).length,
workspaceId,
environment,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
res.status(200).send({
secrets
})
}
});
};
/**
* Delete secrets in workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
* @param req
* @param res
*/
export const deleteSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params
const secretIdsToDelete: string[] = req.body.secretIds
const { workspaceId, environmentName } = req.params;
const secretIdsToDelete: string[] = req.body.secretIds;
const [secretIdsUserCanDeleteError, secretIdsUserCanDelete] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
if (secretIdsUserCanDeleteError) {
throw InternalServerError({ message: `Unable to fetch secrets you own: [error=${secretIdsUserCanDeleteError.message}]` })
}
const secretIdsUserCanDelete = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
const secretsUserCanDeleteSet: Set<string> = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString()));
const deleteOperationsToPerform: AnyBulkWriteOperation<ISecret>[] = []
const secretsUserCanDeleteSet: Set<string> = new Set(
secretIdsUserCanDelete.map((objectId) => objectId._id.toString())
);
const deleteOperationsToPerform: AnyBulkWriteOperation<ISecret>[] = [];
let numSecretsDeleted = 0;
secretIdsToDelete.forEach(secretIdToDelete => {
secretIdsToDelete.forEach((secretIdToDelete) => {
if (secretsUserCanDeleteSet.has(secretIdToDelete)) {
const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } }
deleteOperationsToPerform.push(deleteOperation)
const deleteOperation = {
deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } }
};
deleteOperationsToPerform.push(deleteOperation);
numSecretsDeleted++;
} else {
throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" })
throw RouteValidationError({
message: "You cannot delete secrets that you do not have access to"
});
}
})
});
const [bulkDeleteError, bulkDelete] = await to(Secret.bulkWrite(deleteOperationsToPerform).then())
if (bulkDeleteError) {
if (bulkDeleteError instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkDeleteError.stack })
}
throw InternalServerError()
}
await Secret.bulkWrite(deleteOperationsToPerform);
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
event: "secrets deleted",
distinctId: req.user.email,
properties: {
numberOfSecrets: numSecretsDeleted,
environment: environmentName,
workspaceId,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
res.status(200).send()
}
res.status(200).send();
};
/**
* Delete secret with id [secretId]
* @param req
* @param req
* @param res
*/
export const deleteSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
await Secret.findByIdAndDelete(req._secret._id)
await Secret.findByIdAndDelete(req._secret._id);
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
event: "secrets deleted",
distinctId: req.user.email,
properties: {
numberOfSecrets: 1,
workspaceId: req._secret.workspace.toString(),
environment: req._secret.environment,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
res.status(200).send({
secret: req._secret
})
}
});
};
/**
* Update secrets for workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const updateSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params
const { workspaceId, environmentName } = req.params;
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
if (secretIdsUserCanModifyError) {
throw InternalServerError({ message: "Unable to fetch secrets you own" })
}
const secretIdsUserCanModify = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
const secretsUserCanModifySet: Set<string> = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString()));
const updateOperationsToPerform: any = []
const secretsUserCanModifySet: Set<string> = new Set(
secretIdsUserCanModify.map((objectId) => objectId._id.toString())
);
const updateOperationsToPerform: any = [];
secretsModificationsRequested.forEach(userModifiedSecret => {
secretsModificationsRequested.forEach((userModifiedSecret) => {
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext,
@ -240,57 +240,54 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext,
secretCommentIV: userModifiedSecret.secretCommentIV,
secretCommentTag: userModifiedSecret.secretCommentTag,
secretCommentHash: userModifiedSecret.secretCommentHash,
}
secretCommentHash: userModifiedSecret.secretCommentHash
};
const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: sanitizedSecret } } }
updateOperationsToPerform.push(updateOperation)
const updateOperation = {
updateOne: {
filter: { _id: userModifiedSecret._id, workspace: workspaceId },
update: { $inc: { version: 1 }, $set: sanitizedSecret }
}
};
updateOperationsToPerform.push(updateOperation);
} else {
throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" })
throw UnauthorizedRequestError({
message: "You do not have permission to modify one or more of the requested secrets"
});
}
})
});
const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(updateOperationsToPerform).then())
if (bulkModificationInfoError) {
if (bulkModificationInfoError instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkModificationInfoError.stack })
}
throw InternalServerError()
}
await Secret.bulkWrite(updateOperationsToPerform);
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
event: "secrets modified",
distinctId: req.user.email,
properties: {
numberOfSecrets: (secretsModificationsRequested ?? []).length,
environment: environmentName,
workspaceId,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
return res.status(200).send()
}
return res.status(200).send();
};
/**
* Update a secret within workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const updateSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params
const { workspaceId, environmentName } = req.params;
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
const [secretIdUserCanModifyError, secretIdUserCanModify] = await to(Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
if (secretIdUserCanModifyError && !secretIdUserCanModify) {
throw BadRequestError()
}
await Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
@ -304,45 +301,55 @@ export const updateSecret = async (req: Request, res: Response) => {
secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext,
secretCommentIV: secretModificationsRequested.secretCommentIV,
secretCommentTag: secretModificationsRequested.secretCommentTag,
secretCommentHash: secretModificationsRequested.secretCommentHash,
}
secretCommentHash: secretModificationsRequested.secretCommentHash
};
const [error, singleModificationUpdate] = await to(Secret.updateOne({ _id: secretModificationsRequested._id, workspace: workspaceId }, { $inc: { version: 1 }, $set: sanitizedSecret }).then())
if (error instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: error.stack })
}
const singleModificationUpdate = await Secret.updateOne(
{ _id: secretModificationsRequested._id, workspace: workspaceId },
{ $inc: { version: 1 }, $set: sanitizedSecret }
)
.catch((error) => {
if (error instanceof ValidationError) {
throw RouteValidationError({
message: "Unable to apply modifications, please try again",
stack: error.stack
});
}
throw error;
});
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
event: "secrets modified",
distinctId: req.user.email,
properties: {
numberOfSecrets: 1,
environment: environmentName,
workspaceId,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
return res.status(200).send(singleModificationUpdate)
}
return res.status(200).send(singleModificationUpdate);
};
/**
* Return secrets for workspace with id [workspaceId], environment [environment] and user
* with id [req.user._id]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const { environment } = req.query;
const { workspaceId } = req.params;
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
let userEmail: string | undefined = undefined // used for posthog
let userId: Types.ObjectId | undefined = undefined; // used for getting personal secrets for user
let userEmail: string | undefined = undefined; // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
@ -350,47 +357,47 @@ export const getSecrets = async (req: Request, res: Response) => {
if (req.serviceTokenData) {
userId = req.serviceTokenData.user;
const user = await User.findById(req.serviceTokenData.user, 'email');
const user = await User.findById(req.serviceTokenData.user, "email");
if (!user) throw AccountNotFoundError();
userEmail = user.email;
}
const [err, secrets] = await to(Secret.find(
{
workspace: workspaceId,
environment,
$or: [{ user: userId }, { user: { $exists: false } }],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
).then())
if (err) {
throw RouteValidationError({ message: "Failed to get secrets, please try again", stack: err.stack })
}
const secrets = await Secret.find({
workspace: workspaceId,
environment,
$or: [{ user: userId }, { user: { $exists: false } }],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
})
.catch((err) => {
throw RouteValidationError({
message: "Failed to get secrets, please try again",
stack: err.stack
});
})
if (postHogClient) {
postHogClient.capture({
event: 'secrets pulled',
event: "secrets pulled",
distinctId: userEmail,
properties: {
numberOfSecrets: (secrets ?? []).length,
environment,
workspaceId,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
return res.json(secrets)
}
return res.json(secrets);
};
/**
* Return secret with id [secretId]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getSecret = async (req: Request, res: Response) => {
// if (postHogClient) {
@ -410,4 +417,4 @@ export const getSecret = async (req: Request, res: Response) => {
return res.status(200).send({
secret: req._secret
});
}
};

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,18 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
import { Request, Response } from "express";
import { Types } from "mongoose";
import crypto from "crypto";
import bcrypt from "bcrypt";
import {
ServiceAccount,
ServiceAccountKey,
ServiceAccountOrganizationPermission,
ServiceAccountWorkspacePermission
} from '../../models';
ServiceAccountWorkspacePermission,
} from "../../models";
import {
CreateServiceAccountDto
} from '../../interfaces/serviceAccounts/dto';
import { BadRequestError, ServiceAccountNotFoundError } from '../../utils/errors';
import { getSaltRounds } from '../../config';
CreateServiceAccountDto,
} from "../../interfaces/serviceAccounts/dto";
import { BadRequestError, ServiceAccountNotFoundError } from "../../utils/errors";
import { getSaltRounds } from "../../config";
/**
* Return service account tied to the request (service account) client
@ -21,13 +21,13 @@ import { getSaltRounds } from '../../config';
*/
export const getCurrentServiceAccount = async (req: Request, res: Response) => {
const serviceAccount = await ServiceAccount.findById(req.serviceAccount._id);
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
throw ServiceAccountNotFoundError({ message: "Failed to find service account" });
}
return res.status(200).send({
serviceAccount
serviceAccount,
});
}
@ -38,15 +38,15 @@ export const getCurrentServiceAccount = async (req: Request, res: Response) => {
*/
export const getServiceAccountById = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
throw ServiceAccountNotFoundError({ message: "Failed to find service account" });
}
return res.status(200).send({
serviceAccount
serviceAccount,
});
}
@ -71,9 +71,9 @@ export const createServiceAccount = async (req: Request, res: Response) => {
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
const secret = crypto.randomBytes(16).toString('base64');
const secret = crypto.randomBytes(16).toString("base64");
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
// create service account
const serviceAccount = await new ServiceAccount({
name,
@ -82,23 +82,23 @@ export const createServiceAccount = async (req: Request, res: Response) => {
publicKey,
lastUsed: new Date(),
expiresAt,
secretHash
}).save();
secretHash,
}).save()
const serviceAccountObj = serviceAccount.toObject();
delete serviceAccountObj.secretHash;
delete (serviceAccountObj as any).secretHash;
// provision default org-level permission for service account
await new ServiceAccountOrganizationPermission({
serviceAccount: serviceAccount._id
serviceAccount: serviceAccount._id,
}).save();
const secretId = Buffer.from(serviceAccount._id.toString(), 'hex').toString('base64');
const secretId = Buffer.from(serviceAccount._id.toString(), "hex").toString("base64");
return res.status(200).send({
serviceAccountAccessKey: `sa.${secretId}.${secret}`,
serviceAccount: serviceAccountObj
serviceAccount: serviceAccountObj,
});
}
@ -111,21 +111,21 @@ export const createServiceAccount = async (req: Request, res: Response) => {
export const changeServiceAccountName = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const { name } = req.body;
const serviceAccount = await ServiceAccount.findOneAndUpdate(
{
_id: new Types.ObjectId(serviceAccountId)
_id: new Types.ObjectId(serviceAccountId),
},
{
name
name,
},
{
new: true
new: true,
}
);
return res.status(200).send({
serviceAccount
serviceAccount,
});
}
@ -140,15 +140,15 @@ export const addServiceAccountKey = async (req: Request, res: Response) => {
const {
workspaceId,
encryptedKey,
nonce
nonce,
} = req.body;
const serviceAccountKey = await new ServiceAccountKey({
encryptedKey,
nonce,
sender: req.user._id,
serviceAccount: req.serviceAccount._d,
workspace: new Types.ObjectId(workspaceId)
workspace: new Types.ObjectId(workspaceId),
}).save();
return serviceAccountKey;
@ -161,11 +161,11 @@ export const addServiceAccountKey = async (req: Request, res: Response) => {
*/
export const getServiceAccountWorkspacePermissions = async (req: Request, res: Response) => {
const serviceAccountWorkspacePermissions = await ServiceAccountWorkspacePermission.find({
serviceAccount: req.serviceAccount._id
}).populate('workspace');
serviceAccount: req.serviceAccount._id,
}).populate("workspace");
return res.status(200).send({
serviceAccountWorkspacePermissions
serviceAccountWorkspacePermissions,
});
}
@ -182,48 +182,48 @@ export const addServiceAccountWorkspacePermission = async (req: Request, res: Re
read = false,
write = false,
encryptedKey,
nonce
nonce,
} = req.body;
if (!req.membership.workspace.environments.some((e: { name: string; slug: string }) => e.slug === environment)) {
return res.status(400).send({
message: 'Failed to validate workspace environment'
message: "Failed to validate workspace environment",
});
}
const existingPermission = await ServiceAccountWorkspacePermission.findOne({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId),
environment
environment,
});
if (existingPermission) throw BadRequestError({ message: 'Failed to add workspace permission to service account due to already-existing ' });
if (existingPermission) throw BadRequestError({ message: "Failed to add workspace permission to service account due to already-existing " });
const serviceAccountWorkspacePermission = await new ServiceAccountWorkspacePermission({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId),
environment,
read,
write
write,
}).save();
const existingServiceAccountKey = await ServiceAccountKey.findOne({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId)
workspace: new Types.ObjectId(workspaceId),
});
if (!existingServiceAccountKey) {
await new ServiceAccountKey({
encryptedKey,
nonce,
sender: req.user._id,
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId)
workspace: new Types.ObjectId(workspaceId),
}).save();
}
return res.status(200).send({
serviceAccountWorkspacePermission
serviceAccountWorkspacePermission,
});
}
@ -240,19 +240,19 @@ export const deleteServiceAccountWorkspacePermission = async (req: Request, res:
const { serviceAccount, workspace } = serviceAccountWorkspacePermission;
const count = await ServiceAccountWorkspacePermission.countDocuments({
serviceAccount,
workspace
workspace,
});
if (count === 0) {
await ServiceAccountKey.findOneAndDelete({
serviceAccount,
workspace
workspace,
});
}
}
return res.status(200).send({
serviceAccountWorkspacePermission
serviceAccountWorkspacePermission,
});
}
@ -269,20 +269,20 @@ export const deleteServiceAccount = async (req: Request, res: Response) => {
if (serviceAccount) {
await ServiceAccountKey.deleteMany({
serviceAccount: serviceAccount._id
serviceAccount: serviceAccount._id,
});
await ServiceAccountOrganizationPermission.deleteMany({
serviceAccount: new Types.ObjectId(serviceAccountId)
serviceAccount: new Types.ObjectId(serviceAccountId),
});
await ServiceAccountWorkspacePermission.deleteMany({
serviceAccount: new Types.ObjectId(serviceAccountId)
serviceAccount: new Types.ObjectId(serviceAccountId),
});
}
return res.status(200).send({
serviceAccount
serviceAccount,
});
}
@ -294,13 +294,13 @@ export const deleteServiceAccount = async (req: Request, res: Response) => {
*/
export const getServiceAccountKeys = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const serviceAccountKeys = await ServiceAccountKey.find({
serviceAccount: req.serviceAccount._id,
...(workspaceId ? { workspace: new Types.ObjectId(workspaceId) } : {})
...(workspaceId ? { workspace: new Types.ObjectId(workspaceId) } : {}),
});
return res.status(200).send({
serviceAccountKeys
serviceAccountKeys,
});
}

View File

@ -1,30 +1,29 @@
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 { ServiceTokenData } from "../../models";
import { getSaltRounds } from "../../config";
import { BadRequestError } from "../../utils/errors";
import { ActorType, EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/serviceTokenData";
import {
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';
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { Types } from "mongoose";
/**
* Return service token data associated with service token on request
* @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 +36,152 @@ 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")
.lean();
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 {
const {
body: { workspaceId, permissions, tag, encryptedKey, scopes, name, expiresIn, iv }
} = await validateRequest(reqValidator.CreateServiceTokenV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.ServiceTokens
);
const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
let user;
if (req.authData.actor.type === ActorType.USER) {
user = req.authData.authPayload._id;
}
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
user,
scopes,
lastUsed: new Date(),
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
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}`;
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SERVICE_TOKEN,
metadata: {
name,
workspaceId,
environment,
encryptedKey,
iv,
tag,
expiresIn,
permissions
} = req.body;
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
let expiresAt;
if (expiresIn) {
expiresAt = new Date()
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
scopes
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
let user, serviceAccount;
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
user = req.authData.authPayload._id;
}
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();
// 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
});
}
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 {
params: { serviceTokenDataId }
} = await validateRequest(reqValidator.DeleteServiceTokenV2, req);
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
let serviceTokenData = await ServiceTokenData.findById(serviceTokenDataId);
if (!serviceTokenData) throw BadRequestError({ message: "Service token not found" });
const { permission } = await getUserProjectPermissions(
req.user._id,
serviceTokenData.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.ServiceTokens
);
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
if (!serviceTokenData)
return res.status(200).send({
serviceTokenData
message: "Failed to delete service token"
});
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_SERVICE_TOKEN,
metadata: {
name: serviceTokenData.name,
scopes: serviceTokenData?.scopes
}
},
{
workspaceId: serviceTokenData.workspace
}
);
return res.status(200).send({
serviceTokenData
});
};

View File

@ -1,14 +1,14 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { User, MembershipOrg } from '../../models';
import { completeAccount } from '../../helpers/user';
import { Request, Response } from "express";
import { MembershipOrg, User } from "../../models";
import { completeAccount } from "../../helpers/user";
import {
initializeDefaultOrg
} from '../../helpers/signup';
import { issueAuthTokens } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables';
import request from '../../config/request';
import { getLoopsApiKey, getHttpsEnabled } from '../../config';
initializeDefaultOrg,
} from "../../helpers/signup";
import { issueAuthTokens } from "../../helpers/auth";
import { ACCEPTED, INVITED } from "../../variables";
import { standardRequest } from "../../config/request";
import { getHttpsEnabled, getLoopsApiKey } from "../../config";
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
/**
* Complete setting up user by adding their personal and auth information as part of the
@ -18,129 +18,136 @@ import { getLoopsApiKey, getHttpsEnabled } from '../../config';
* @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
}: {
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;
let user;
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
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED
},
{
user,
status: ACCEPTED
}
);
// 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(),
});
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id.toString()
});
// update organization membership statuses that are
// invited to completed with user attached
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED,
},
{
user,
status: ACCEPTED,
}
);
token = tokens.token;
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
});
// sending a welcome email to new users
if (await getLoopsApiKey()) {
await request.post("https://app.loops.so/api/v1/events/send", {
"email": email,
"eventName": "Sign Up",
"firstName": firstName,
"lastName": lastName
}, {
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + (await getLoopsApiKey())
},
});
}
const 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'
});
}
// 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(),
});
return res.status(200).send({
message: 'Successfully set up account',
message: "Successfully set up account",
user,
token
token,
});
};
@ -152,99 +159,104 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
* @returns
*/
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;
let user;
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');
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(),
});
});
// update organization membership statuses that are
// invited to completed with user attached
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;
const 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',
message: "Successfully set up account",
user,
token
token,
});
};
};

View File

@ -1,72 +1,82 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { ForbiddenError } from "@casl/ability";
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Secret, Tag } from "../../models";
import { BadRequestError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation";
import {
Membership, Secret,
} from '../../models';
import Tag, { ITag } from '../../models/tag';
import { Builder } from "builder-pattern"
import to from 'await-to-js';
import { BadRequestError, UnauthorizedRequestError } from '../../utils/errors';
import { MongoError } from 'mongodb';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import * as reqValidator from "../../validation/tags";
export const createWorkspaceTag = async (req: Request, res: Response) => {
const { workspaceId } = req.params
const { name, slug } = req.body
const sanitizedTagToCreate = Builder<ITag>()
.name(name)
.workspace(new Types.ObjectId(workspaceId))
.slug(slug)
.user(new Types.ObjectId(req.user._id))
.build();
const {
body: { name, slug },
params: { workspaceId }
} = await validateRequest(reqValidator.CreateWorkspaceTagsV2, req);
const [err, createdTag] = await to(Tag.create(sanitizedTagToCreate))
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Tags
);
if (err) {
if ((err as MongoError).code === 11000) {
throw BadRequestError({ message: "Tags must be unique in a workspace" })
}
const tagToCreate = {
name,
workspace: new Types.ObjectId(workspaceId),
slug,
user: new Types.ObjectId(req.user._id)
};
throw err
}
const createdTag = await new Tag(tagToCreate).save();
res.json(createdTag)
}
res.json(createdTag);
};
export const deleteWorkspaceTag = async (req: Request, res: Response) => {
const { tagId } = req.params
const {
params: { tagId }
} = await validateRequest(reqValidator.DeleteWorkspaceTagsV2, req);
const tagFromDB = await Tag.findById(tagId)
if (!tagFromDB) {
throw BadRequestError()
}
const tagFromDB = await Tag.findById(tagId);
if (!tagFromDB) {
throw BadRequestError();
}
// can only delete if the request user is one that belongs to the same workspace as the tag
const membership = await Membership.findOne({
user: req.user,
workspace: tagFromDB.workspace
});
const { permission } = await getUserProjectPermissions(
req.user._id,
tagFromDB.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Tags
);
if (!membership) {
UnauthorizedRequestError({ message: 'Failed to validate membership' });
}
const result = await Tag.findByIdAndDelete(tagId);
const result = await Tag.findByIdAndDelete(tagId);
// remove the tag from secrets
await Secret.updateMany({ tags: { $in: [tagId] } }, { $pull: { tags: tagId } });
// remove the tag from secrets
await Secret.updateMany(
{ tags: { $in: [tagId] } },
{ $pull: { tags: tagId } }
);
res.json(result);
}
res.json(result);
};
export const getWorkspaceTags = async (req: Request, res: Response) => {
const { workspaceId } = req.params
const workspaceTags = await Tag.find({ workspace: workspaceId })
return res.json({
workspaceTags
})
}
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceTagsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Tags
);
const workspaceTags = await Tag.find({
workspace: new Types.ObjectId(workspaceId)
});
return res.json({
workspaceTags
});
};

View File

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

View File

@ -1,41 +1,39 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Key, Membership, ServiceTokenData, Workspace } from "../../models";
import {
Workspace,
Secret,
Membership,
MembershipOrg,
Integration,
IntegrationAuth,
Key,
IUser,
ServiceToken,
ServiceTokenData
} from '../../models';
pullSecrets as pull,
v2PushSecrets as push,
reformatPullSecrets
} from "../../helpers/secret";
import { pushKeys } from "../../helpers/key";
import { EventService, TelemetryService } from "../../services";
import { eventPushSecrets } from "../../events";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation";
import {
v2PushSecrets as push,
pullSecrets as pull,
reformatPullSecrets
} from '../../helpers/secret';
import { pushKeys } from '../../helpers/key';
import { TelemetryService, EventService } from '../../services';
import { eventPushSecrets } from '../../events';
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
interface V2PushSecret {
type: string; // personal or shared
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
type: string; // personal or shared
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
}
/**
@ -46,71 +44,63 @@ interface V2PushSecret {
* @returns
*/
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;
// upload (encrypted) secrets to workspace with id [workspaceId]
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
})
});
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath: "/"
})
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload workspace secrets'
});
}
return res.status(200).send({
message: 'Successfully uploaded workspace secrets'
});
return res.status(200).send({
message: "Successfully uploaded workspace secrets"
});
};
/**
@ -121,65 +111,57 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
* @returns
*/
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;
let secrets;
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
});
return res.status(200).send({
secrets
});
};
export const getWorkspaceKey = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Return encrypted project key'
#swagger.description = 'Return encrypted project key'
@ -207,61 +189,53 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
}
}
*/
let key;
try {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceKeyV2, req);
key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
}).populate('sender', '+publicKey');
const 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);
}
export const getWorkspaceServiceTokenData = async (
req: Request,
res: Response
) => {
let serviceTokenData;
try {
const { workspaceId } = req.params;
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.GET_WORKSPACE_KEY,
metadata: {
keyId: key._id.toString()
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
serviceTokenData = await ServiceTokenData
.find({
workspace: workspaceId
})
.select('+encryptedKey +iv +tag');
return res.status(200).json(key);
};
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace service token data'
});
}
export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
return res.status(200).send({
serviceTokenData
});
}
const serviceTokenData = await ServiceTokenData.find({
workspace: workspaceId
}).select("+encryptedKey +iv +tag");
return res.status(200).send({
serviceTokenData
});
};
/**
* Return memberships for workspace with id [workspaceId]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Return project memberships'
#swagger.description = 'Return project memberships'
@ -294,34 +268,33 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
}
}
*/
let memberships;
try {
const { workspaceId } = req.params;
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV2, req);
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 { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
);
return res.status(200).send({
memberships
});
}
const memberships = await Membership.find({
workspace: workspaceId
}).populate("user", "+publicKey");
return res.status(200).send({
memberships
});
};
/**
* Update role of membership with id [membershipId] to role [role]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const updateWorkspaceMembership = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Update project membership'
#swagger.description = 'Update project membership'
@ -374,42 +347,40 @@ 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'
});
}
return res.status(200).send({
membership
});
}
const {
params: { workspaceId, membershipId },
body: { role }
} = await validateRequest(reqValidator.UpdateWorkspaceMembershipsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
const membership = await Membership.findByIdAndUpdate(
membershipId,
{
role
},
{
new: true
}
);
return res.status(200).send({
membership
});
};
/**
* Delete workspace membership with id [membershipId]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Delete project membership'
#swagger.description = 'Delete project membership'
@ -445,32 +416,29 @@ 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'
});
}
return res.status(200).send({
membership
});
}
const {
params: { workspaceId, membershipId }
} = await validateRequest(reqValidator.DeleteWorkspaceMembershipsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Member
);
const membership = await Membership.findByIdAndDelete(membershipId);
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
});
};
/**
* Change autoCapitilzation Rule of workspace
@ -479,32 +447,31 @@ 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 {
params: { workspaceId },
body: { autoCapitalization }
} = await validateRequest(reqValidator.ToggleAutoCapitalizationV2, req);
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 { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Settings
);
return res.status(200).send({
message: 'Successfully changed autoCapitalization setting',
workspace
});
};
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,238 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
import * as bigintConversion from "bigint-conversion";
const jsrp = require("jsrp");
import { LoginSRPDetail, User } from "../../models";
import { createToken, issueAuthTokens, 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 { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables";
import { getUserAgentType } from "../../utils/posthog"; // TODO: move this
import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config";
import { AuthMethod } from "../../models/user";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module "jsonwebtoken" {
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
userId: string;
email: string;
authProvider: AuthMethod;
isUserCompleted: boolean;
}
}
/**
* 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) => {
const {
body: { email, clientPublicKey, providerAuthToken }
} = await validateRequest(reqValidator.Login1V3, req);
const user = await User.findOne({
email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await validateProviderAuthToken({
email,
providerAuthToken
});
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace(
{
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
});
}
);
};
/**
* 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) => {
if (!req.headers["user-agent"])
throw InternalServerError({ message: "User-Agent header is required" });
const {
body: { email, providerAuthToken, clientProof }
} = await validateRequest(reqValidator.Login2V3, req);
const user = await User.findOne({
email
}).select(
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user");
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await validateProviderAuthToken({
email,
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: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP
}));
return res.status(200).send(response);
}
return res.status(400).send({
message: "Failed to authenticate. Try again?"
});
}
);
};

View File

@ -1,7 +1,11 @@
import * as secretsController from './secretsController';
import * as workspacesController from './workspacesController';
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,704 @@
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 { EventService, SecretService } from "../../services";
import { eventPushSecrets } from "../../events";
import { BotService } from "../../services";
import { containsGlobPatterns, isValidScope, repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { Folder, IServiceTokenData } from "../../models";
import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secrets";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
import { validateServiceTokenDataClientForWorkspace } from "../../validation";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
/**
* 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 validatedData = await validateRequest(reqValidator.GetSecretsRawV3, req);
let {
query: { secretPath, environment, workspaceId }
} = validatedData;
const {
query: { folderId, include_imports: includeImports }
} = validatedData;
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
authData: req.authData
});
// if the service token has single scope, it will get all secrets for that scope by default
const serviceTokenDetails: IServiceTokenData = req?.serviceTokenData;
if (
serviceTokenDetails &&
serviceTokenDetails.scopes.length == 1 &&
!containsGlobPatterns(serviceTokenDetails.scopes[0].secretPath)
) {
const scope = serviceTokenDetails.scopes[0];
secretPath = scope.secretPath;
environment = scope.environment;
workspaceId = serviceTokenDetails.workspace.toString();
}
return res.status(200).send({
secrets
});
}
if (folderId && folderId !== "root") {
const folder = await Folder.findOne({ workspace: workspaceId, environment });
if (!folder) throw BadRequestError({ message: "Folder not found" });
/**
* Get 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 type = req.query.type as 'shared' | 'personal' | undefined;
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
}
const secret = await SecretService.getSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData
});
return res.status(200).send({
secret
});
}
if (!environment || !workspaceId)
throw BadRequestError({ message: "Missing environment or workspace id" });
/**
* 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
} = 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
} : {})
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
}
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId,
secretPath,
authData: req.authData
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
});
if (includeImports) {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root";
// if folder exist get it and replace folderid with new one
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath as string);
if (!folder) {
throw BadRequestError({ message: "Folder not found" });
}
folderId = folder.id;
}
const importedSecrets = await getAllImportedSecrets(
workspaceId,
environment,
folderId,
permissionCheckFn
);
return res.status(200).send({
secret: secretWithoutBlindIndex
secrets: secrets.map((secret) =>
repackageSecretToRaw({
secret,
key
})
),
imports: importedSecrets.map((el) => ({
...el,
secrets: el.secrets.map((secret) => repackageSecretToRaw({ secret, key }))
}))
});
}
}
return res.status(200).send({
secrets: secrets.map((secret) => {
const rep = repackageSecretToRaw({
secret,
key
});
return rep;
})
});
};
/**
* Return secret with name [secretName] in plaintext
* @param req
* @param res
*/
export const getSecretByNameRaw = async (req: Request, res: Response) => {
const {
query: { secretPath, environment, workspaceId, type },
params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
}
const secret = await SecretService.getSecret({
secretName,
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] in plaintext
* @param req
* @param res
*/
export const createSecretRaw = async (req: Request, res: Response) => {
const {
params: { secretName },
body: { secretPath, environment, workspaceId, type, secretValue, secretComment }
} = await validateRequest(reqValidator.CreateSecretRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
});
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,
secretPath
})
});
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 {
params: { secretName },
body: { secretValue, environment, secretPath, type, workspaceId }
} = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req);
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
})
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
return res.status(200).send({
secret
});
}
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
});
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretValue,
key
});
const secret = await SecretService.updateSecret({
secretName,
workspaceId: new Types.ObjectId(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,
secretPath
})
});
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 {
params: { secretName },
body: { environment, secretPath, type, workspaceId }
} = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const { secret } = await SecretService.deleteSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretPath
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
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 validatedData = await validateRequest(reqValidator.GetSecretsV3, req);
const {
query: { environment, workspaceId, include_imports: includeImports, folderId }
} = validatedData;
let {
query: { secretPath }
} = validatedData;
if (folderId && folderId !== "root") {
const folder = await Folder.findOne({ workspace: workspaceId, environment });
if (!folder) throw BadRequestError({ message: "Folder not found" });
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
}
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
}
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId,
secretPath,
authData: req.authData
});
if (includeImports) {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root";
// if folder exist get it and replace folderid with new one
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath as string);
if (!folder) {
throw BadRequestError({ message: "Folder not found" });
}
folderId = folder.id;
}
const importedSecrets = await getAllImportedSecrets(
workspaceId,
environment,
folderId,
permissionCheckFn
);
return res.status(200).send({
secrets,
imports: importedSecrets
});
}
return res.status(200).send({
secrets
});
};
/**
* Return secret with name [secretName]
* @param req
* @param res
*/
export const getSecretByName = async (req: Request, res: Response) => {
const {
query: { secretPath, environment, workspaceId, type },
params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
}
const secret = await SecretService.getSecret({
secretName,
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 {
body: {
workspaceId,
secretPath,
environment,
metadata,
type,
secretKeyIV,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext
},
params: { secretName }
} = await validateRequest(reqValidator.CreateSecretV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
metadata
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
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 {
body: {
secretValueCiphertext,
secretValueTag,
secretValueIV,
type,
environment,
secretPath,
workspaceId
},
params: { secretName }
} = await validateRequest(reqValidator.UpdateSecretByNameV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const secret = await SecretService.updateSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
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 {
body: { type, environment, secretPath, workspaceId },
params: { secretName }
} = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
return res.status(200).send({
secret
});
}
const { secret } = await SecretService.deleteSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretPath
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
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 { MembershipOrg, User } from "../../models";
import { completeAccount } from "../../helpers/user";
import { initializeDefaultOrg } from "../../helpers/signup";
import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth";
import { ACCEPTED, INVITED } from "../../variables";
import { standardRequest } from "../../config/request";
import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config";
import { BadRequestError } from "../../utils/errors";
import { TelemetryService } from "../../services";
import { AuthMethod } from "../../models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
/**
* Complete setting up user by adding their personal and auth information as part of the
* signup flow
* @param req
* @param res
* @returns
*/
export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token;
try {
const {
body: {
email,
publicKey,
salt,
lastName,
verifier,
firstName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
organizationName,
providerAuthToken,
attributionSource,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag
}
} = await validateRequest(reqValidator.CompletedAccountSignupV3, req);
user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: "Failed to complete account for complete user"
});
}
if (providerAuthToken) {
await validateProviderAuthToken({
email,
providerAuthToken
});
} else {
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>(
req.headers["authorization"]?.split(" ", 2)
) ?? [null, null];
if (AUTH_TOKEN_TYPE === null) {
throw BadRequestError({ message: "Missing Authorization Header in the request header." });
}
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
throw BadRequestError({
message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
});
}
if (AUTH_TOKEN_VALUE === null) {
throw BadRequestError({
message: "Missing Authorization Body in the request header"
});
}
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
const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) =>
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod)
);
if (!hasSamlEnabled) {
// TODO: modify this part
// initialize default organization and workspace
await initializeDefaultOrg({
organizationName,
user
});
}
// 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 ? { attributionSource } : {})
}
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to complete account setup"
});
}
return res.status(200).send({
message: "Successfully set up account",
user,
token
});
};

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,11 +1,12 @@
import { Request, Response } from "express";
import { Membership, Workspace } from "../../../models";
import { IUser, Membership, Workspace } from "../../../models";
import { EventType } from "../../../ee/models";
import { IMembershipPermission } from "../../../models/membership";
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
import { ADMIN, MEMBER } from "../../../variables/organization";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../../variables';
import { Builder } from "builder-pattern"
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../../variables";
import _ from "lodash";
import { EEAuditLogService } from "../../services";
export const denyMembershipPermissions = async (req: Request, res: Response) => {
const { membershipId } = req.params;
@ -15,10 +16,10 @@ export const denyMembershipPermissions = async (req: Request, res: Response) =>
throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" })
}
return Builder<IMembershipPermission>()
.environmentSlug(permission.environmentSlug)
.ability(permission.ability)
.build();
return {
environmentSlug: permission.environmentSlug,
ability: permission.ability
}
})
const sanitizedMembershipPermissionsUnique = _.uniqWith(sanitizedMembershipPermissions, _.isEqual)
@ -39,7 +40,7 @@ export const denyMembershipPermissions = async (req: Request, res: Response) =>
throw BadRequestError({ message: "Something went wrong when locating the related workspace" })
}
const uniqueEnvironmentSlugs = new Set(_.uniq(_.map(relatedWorkspace.environments, 'slug')));
const uniqueEnvironmentSlugs = new Set(_.uniq(_.map(relatedWorkspace.environments, "slug")));
sanitizedMembershipPermissionsUnique.forEach(permission => {
if (!uniqueEnvironmentSlugs.has(permission.environmentSlug)) {
@ -52,13 +53,34 @@ export const denyMembershipPermissions = async (req: Request, res: Response) =>
{ _id: membershipToModify._id },
{ $set: { deniedPermissions: sanitizedMembershipPermissionsUnique } },
{ new: true }
)
).populate<{ user: IUser }>("user");
if (!updatedMembershipWithPermissions) {
throw BadRequestError({ message: "The resource has been removed before it can be modified" })
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS,
metadata: {
userId: updatedMembershipWithPermissions.user._id.toString(),
email: updatedMembershipWithPermissions.user.email,
deniedPermissions: updatedMembershipWithPermissions.deniedPermissions.map(({
environmentSlug,
ability
}) => ({
environmentSlug,
ability
}))
}
},
{
workspaceId: updatedMembershipWithPermissions.workspace
}
);
res.send({
permissionsDenied: updatedMembershipWithPermissions.deniedPermissions
permissionsDenied: updatedMembershipWithPermissions.deniedPermissions,
})
}

View File

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

View File

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

View File

@ -1,16 +1,25 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Secret } from '../../../models';
import { SecretVersion } from '../../models';
import { EESecretService } from '../../services';
import { ForbiddenError, subject } from "@casl/ability";
import { Request, Response } from "express";
import { validateRequest } from "../../../helpers/validation";
import { Folder, Secret } from "../../../models";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../services/ProjectRoleService";
import { BadRequestError } from "../../../utils/errors";
import * as reqValidator from "../../../validation";
import { SecretVersion } from "../../models";
import { EESecretService } from "../../services";
import { getFolderWithPathFromId } from "../../../services/FolderService";
/**
* Return secret versions for secret with id [secretId]
* @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 +64,42 @@ import { EESecretService } from '../../services';
}
}
*/
let secretVersions;
try {
const { secretId } = req.params;
const {
params: { secretId },
query: { offset, limit }
} = await validateRequest(reqValidator.GetSecretVersionsV1, req);
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
secretVersions = await SecretVersion.find({
secret: secretId
})
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
const secret = await Secret.findById(secretId);
if (!secret) {
throw BadRequestError({ message: "Failed to find secret" });
}
} 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 { permission } = await getUserProjectPermissions(req.user._id, secret.workspace.toString());
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback
);
const secretVersions = await SecretVersion.find({
secret: secretId
})
.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,91 +147,117 @@ 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,
} = oldSecretVersion;
// update secret
secret = await Secret.findByIdAndUpdate(
secretId,
{
$inc: {
version: 1
},
workspace,
type,
user,
environment,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
},
{
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
}).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
});
}
const {
params: { secretId },
body: { version }
} = await validateRequest(reqValidator.RollbackSecretVersionV1, req);
const toBeUpdatedSec = await Secret.findById(secretId);
if (!toBeUpdatedSec) {
throw BadRequestError({ message: "Failed to find secret" });
}
const { permission } = await getUserProjectPermissions(
req.user._id,
toBeUpdatedSec.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRollback
);
// validate secret version
const oldSecretVersion = await SecretVersion.findOne({
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;
let secretPath = "/";
const folders = await Folder.findOne({ workspace, environment });
if (folders)
secretPath = getFolderWithPathFromId(folders.nodes, folder || "root")?.folderPath || "/";
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment, secretPath })
);
// update secret
const secret = await Secret.findByIdAndUpdate(
secretId,
{
$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,61 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { SecretSnapshot } from '../../models';
import { ForbiddenError } from "@casl/ability";
import { Request, Response } from "express";
import { validateRequest } from "../../../helpers/validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../services/ProjectRoleService";
import * as reqValidator from "../../../validation/secretSnapshot";
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 {
params: { secretSnapshotId }
} = await validateRequest(reqValidator.GetSecretSnapshotV1, req);
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");
} 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
});
}
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
const { permission } = await getUserProjectPermissions(
req.user._id,
secretSnapshot.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback
);
const folderId = secretSnapshot.folderId;
// to show only the folder required secrets
secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter(
({ folder }) => folder === folderId
);
secretSnapshot.folderVersion = secretSnapshot?.folderVersion?.nodes?.children?.map(
({ id, name }) => ({
id,
name
})
) as any;
return res.status(200).send({
secretSnapshot
});
};

View File

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

View File

@ -1,41 +0,0 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import Stripe from 'stripe';
import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
/**
* Handle service provisioning/un-provisioning via Stripe
* @param req
* @param res
* @returns
*/
export const handleWebhook = async (req: Request, res: Response) => {
let event;
try {
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'
});
}
switch (event.type) {
case '':
break;
default:
}
return res.json({ received: true });
};

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,17 @@
import { Types } from 'mongoose';
import { Action } from '../models';
import { Types } from "mongoose";
import { Action } from "../models";
import {
getLatestNSecretSecretVersionIds,
getLatestSecretVersionIds,
getLatestNSecretSecretVersionIds
} from '../helpers/secretVersion';
} from "../helpers/secretVersion";
import {
ACTION_ADD_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_READ_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_UPDATE_SECRETS,
} from '../../variables';
} from "../../variables";
/**
* Create an (audit) action for updating secrets
@ -26,7 +26,7 @@ const createActionUpdateSecret = async ({
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds
secretIds,
}: {
name: string;
userId?: Types.ObjectId;
@ -37,11 +37,11 @@ const createActionUpdateSecret = async ({
}) => {
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
secretIds,
n: 2
n: 2,
}))
.map((s) => ({
oldSecretVersion: s.versions[0]._id,
newSecretVersion: s.versions[1]._id
newSecretVersion: s.versions[1]._id,
}));
const action = await new Action({
@ -51,8 +51,8 @@ const createActionUpdateSecret = async ({
serviceTokenData: serviceTokenDataId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
}
secretVersions: latestSecretVersions,
},
}).save();
return action;
@ -72,7 +72,7 @@ const createActionSecret = async ({
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds
secretIds,
}: {
name: string;
userId?: Types.ObjectId;
@ -84,10 +84,10 @@ const createActionSecret = async ({
// case: action is adding, deleting, or reading secrets
// -> add new secret versions
const latestSecretVersions = (await getLatestSecretVersionIds({
secretIds
secretIds,
}))
.map((s) => ({
newSecretVersion: s.versionId
newSecretVersion: s.versionId,
}));
const action = await new Action({
@ -97,8 +97,8 @@ const createActionSecret = async ({
serviceTokenData: serviceTokenDataId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
}
secretVersions: latestSecretVersions,
},
}).save();
return action;
@ -116,7 +116,7 @@ const createActionClient = ({
name,
userId,
serviceAccountId,
serviceTokenDataId
serviceTokenDataId,
}: {
name: string;
userId?: Types.ObjectId;
@ -127,7 +127,7 @@ const createActionClient = ({
name,
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId
serviceTokenData: serviceTokenDataId,
}).save();
return action;
@ -162,27 +162,27 @@ const createActionHelper = async ({
case ACTION_LOGOUT:
action = await createActionClient({
name,
userId
userId,
});
break;
case ACTION_ADD_SECRETS:
case ACTION_READ_SECRETS:
case ACTION_DELETE_SECRETS:
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
if (!workspaceId || !secretIds) throw new Error("Missing required params workspace id or secret ids to create action secret");
action = await createActionSecret({
name,
userId,
workspaceId,
secretIds
secretIds,
});
break;
case ACTION_UPDATE_SECRETS:
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
if (!workspaceId || !secretIds) throw new Error("Missing required params workspace id or secret ids to create action secret");
action = await createActionUpdateSecret({
name,
userId,
workspaceId,
secretIds
secretIds,
});
break;
}
@ -191,5 +191,5 @@ const createActionHelper = async ({
}
export {
createActionHelper
createActionHelper,
};

View File

@ -1,7 +1,7 @@
import { Types } from 'mongoose';
import { Types } from "mongoose";
import _ from "lodash";
import { Membership } from "../../models";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })

View File

@ -1,8 +1,8 @@
import { Types } from 'mongoose';
import { Types } from "mongoose";
import {
IAction,
Log,
IAction
} from '../models';
} from "../models";
/**
* Create an (audit) log
@ -21,7 +21,7 @@ const createLogHelper = async ({
workspaceId,
actions,
channel,
ipAddress
ipAddress,
}: {
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
@ -39,12 +39,12 @@ const createLogHelper = async ({
actionNames: actions.map((a) => a.name),
actions,
channel,
ipAddress
ipAddress,
}).save();
return log;
}
export {
createLogHelper
createLogHelper,
}

View File

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

View File

@ -1,6 +1,11 @@
import { Types } from "mongoose";
import { Secret, ISecret } from "../../models";
import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models";
import { Secret } from "../../models";
import {
FolderVersion,
ISecretVersion,
SecretSnapshot,
SecretVersion,
} 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;
@ -93,52 +115,8 @@ const markDeletedSecretVersionsHelper = async ({
);
};
/**
* Initialize secret versioning by setting previously unversioned
* secrets to version 1 and begin populating secret versions.
*/
const initSecretVersioningHelper = async () => {
await Secret.updateMany(
{ version: { $exists: false } },
{ $set: { version: 1 } }
);
const unversionedSecrets: ISecret[] = await Secret.aggregate([
{
$lookup: {
from: "secretversions",
localField: "_id",
foreignField: "secret",
as: "versions",
},
},
{
$match: {
versions: { $size: 0 },
},
},
]);
if (unversionedSecrets.length > 0) {
await addSecretVersionsHelper({
secretVersions: unversionedSecrets.map(
(s, idx) =>
new SecretVersion({
...s,
secret: s._id,
version: s.version ? s.version : 1,
isDeleted: false,
workspace: s.workspace,
environment: s.environment,
})
),
});
}
};
export {
takeSecretSnapshotHelper,
addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
initSecretVersioningHelper,
};

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

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

View File

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

View File

@ -1,9 +1,9 @@
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError, SecretSnapshotNotFoundError } from '../../utils/errors';
import { SecretSnapshot } from '../models';
import { NextFunction, Request, Response } from "express";
import { SecretSnapshotNotFoundError } from "../../utils/errors";
import { SecretSnapshot } from "../models";
import {
validateMembership
} from '../../helpers/membership';
validateMembership,
} from "../../helpers/membership";
/**
* Validate if user on request has proper membership for secret snapshot
@ -15,7 +15,7 @@ import {
const requireSecretSnapshotAuth = ({
acceptedRoles,
}: {
acceptedRoles: Array<'admin' | 'member'>;
acceptedRoles: Array<"admin" | "member">;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const { secretSnapshotId } = req.params;
@ -24,14 +24,14 @@ const requireSecretSnapshotAuth = ({
if (!secretSnapshot) {
return next(SecretSnapshotNotFoundError({
message: 'Failed to find secret snapshot'
message: "Failed to find secret snapshot",
}));
}
await validateMembership({
userId: req.user._id,
workspaceId: secretSnapshot.workspace,
acceptedRoles
acceptedRoles,
});
req.secretSnapshot = secretSnapshot as any;

View File

@ -1,12 +1,12 @@
import { Schema, model, Types } from 'mongoose';
import { Schema, Types, model } from "mongoose";
import {
ACTION_ADD_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS,
ACTION_DELETE_SECRETS
} from '../../variables';
ACTION_UPDATE_SECRETS,
} from "../../variables";
export interface IAction {
name: string;
@ -30,42 +30,40 @@ const actionSchema = new Schema<IAction>(
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS,
ACTION_DELETE_SECRETS
]
ACTION_DELETE_SECRETS,
],
},
user: {
type: Schema.Types.ObjectId,
ref: 'User'
ref: "User",
},
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
ref: "ServiceAccount",
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: 'ServiceTokenData'
ref: "ServiceTokenData",
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace'
ref: "Workspace",
},
payload: {
secretVersions: [{
oldSecretVersion: {
type: Schema.Types.ObjectId,
ref: 'SecretVersion'
ref: "SecretVersion",
},
newSecretVersion: {
type: Schema.Types.ObjectId,
ref: 'SecretVersion'
}
}]
}
ref: "SecretVersion",
},
}],
},
}, {
timestamps: true
timestamps: true,
}
);
const Action = model<IAction>('Action', actionSchema);
export default Action;
export const Action = model<IAction>("Action", actionSchema);

View File

@ -0,0 +1,76 @@
import { Schema, Types, model } from "mongoose";
import {
ActorType,
EventType,
UserAgentType
} from "./enums";
import {
Actor,
Event
} from "./types";
export interface IAuditLog {
actor: Actor;
organization: Types.ObjectId;
workspace: Types.ObjectId;
ipAddress: string;
event: Event;
userAgent: string;
userAgentType: UserAgentType;
expiresAt: Date;
}
const auditLogSchema = new Schema<IAuditLog>(
{
actor: {
type: {
type: String,
enum: ActorType,
required: true
},
metadata: {
type: Schema.Types.Mixed
}
},
organization: {
type: Schema.Types.ObjectId,
required: false
},
workspace: {
type: Schema.Types.ObjectId,
required: false
},
ipAddress: {
type: String,
required: true
},
event: {
type: {
type: String,
enum: EventType,
required: true
},
metadata: {
type: Schema.Types.Mixed
}
},
userAgent: {
type: String,
required: true
},
userAgentType: {
type: String,
enum: UserAgentType,
required: true
},
expiresAt: {
type: Date,
expires: 0
}
},
{
timestamps: true
}
);
export const AuditLog = model<IAuditLog>("AuditLog", auditLogSchema);

View File

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

View File

@ -0,0 +1,3 @@
export * from "./auditLog";
export * from "./enums";
export * from "./types";

View File

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

View File

@ -0,0 +1,58 @@
import { Schema, Types, model } 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,
}
);
export const FolderVersion = model<TFolderRootVersionSchema>(
"FolderVersion",
folderRootVersionSchema
);

View File

@ -0,0 +1,34 @@
import { Schema, Types, model } from "mongoose";
type GitAppInstallationSession = {
id: string;
sessionId: string;
organization: Types.ObjectId;
user: Types.ObjectId;
}
const gitAppInstallationSession = new Schema<GitAppInstallationSession>({
id: {
required: true,
type: String,
},
sessionId: {
type: String,
required: true,
unique: true
},
organization: {
type: Schema.Types.ObjectId,
required: true,
unique: true
},
user: {
type: Schema.Types.ObjectId,
ref: "User"
}
});
const GitAppInstallationSession = model<GitAppInstallationSession>("git_app_installation_session", gitAppInstallationSession);
export default GitAppInstallationSession;

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