Compare commits

...

1177 Commits

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

View File

@@ -9,6 +9,7 @@ JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200 JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201
# JWT lifetime # JWT lifetime
# Optional lifetimes for JWT tokens expressed in seconds or a string # Optional lifetimes for JWT tokens expressed in seconds or a string
@@ -16,6 +17,7 @@ JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
JWT_AUTH_LIFETIME= JWT_AUTH_LIFETIME=
JWT_REFRESH_LIFETIME= JWT_REFRESH_LIFETIME=
JWT_SIGNUP_LIFETIME= JWT_SIGNUP_LIFETIME=
JWT_PROVIDER_AUTH_LIFETIME=
# MongoDB # MongoDB
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref # Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
@@ -23,6 +25,9 @@ JWT_SIGNUP_LIFETIME=
# Required # Required
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
# Redis
REDIS_URL=redis://redis:6379
# Optional credentials for MongoDB container instance and Mongo-Express # Optional credentials for MongoDB container instance and Mongo-Express
MONGO_USERNAME=root MONGO_USERNAME=root
MONGO_PASSWORD=example MONGO_PASSWORD=example
@@ -45,11 +50,13 @@ CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY= CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB= CLIENT_ID_GITHUB=
CLIENT_ID_GITLAB= CLIENT_ID_GITLAB=
CLIENT_ID_BITBUCKET=
CLIENT_SECRET_HEROKU= CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL= CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY= CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB= CLIENT_SECRET_GITHUB=
CLIENT_SECRET_GITLAB= CLIENT_SECRET_GITLAB=
CLIENT_SECRET_BITBUCKET=
CLIENT_SLUG_VERCEL= CLIENT_SLUG_VERCEL=
# Sentry (optional) for monitoring errors # Sentry (optional) for monitoring errors
@@ -59,10 +66,6 @@ SENTRY_DSN=
# Ignore - Not applicable for self-hosted version # Ignore - Not applicable for self-hosted version
POSTHOG_HOST= POSTHOG_HOST=
POSTHOG_PROJECT_API_KEY= POSTHOG_PROJECT_API_KEY=
STRIPE_SECRET_KEY=
STRIPE_PUBLISHABLE_KEY= CLIENT_ID_GOOGLE=
STRIPE_WEBHOOK_SECRET= CLIENT_SECRET_GOOGLE=
STRIPE_PRODUCT_STARTER=
STRIPE_PRODUCT_TEAM=
STRIPE_PRODUCT_PRO=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=

View File

@@ -8,7 +8,7 @@ assignees: ''
--- ---
### Feature description ### 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 it be useful?
Why would this feature be useful for Infisical users? 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_URL=mongodb://test:example@mongo:27017/?authSource=admin
- MONGO_USERNAME=test - MONGO_USERNAME=test
- MONGO_PASSWORD=example - MONGO_PASSWORD=example
- ENCRYPTION_KEY=a984ecdf82ec779e55dbcc21303a900f
networks: networks:
- infisical-test - 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: frontend:
enabled: true enabled: true
name: frontend name: frontend
@@ -6,7 +13,7 @@ frontend:
secrets.infisical.com/auto-reload: "true" secrets.infisical.com/auto-reload: "true"
replicaCount: 2 replicaCount: 2
image: image:
repository: infisical/frontend repository: infisical/staging_deployment_frontend
tag: "latest" tag: "latest"
pullPolicy: Always pullPolicy: Always
kubeSecretRef: managed-secret-frontend kubeSecretRef: managed-secret-frontend
@@ -25,7 +32,7 @@ backend:
secrets.infisical.com/auto-reload: "true" secrets.infisical.com/auto-reload: "true"
replicaCount: 2 replicaCount: 2
image: image:
repository: infisical/backend repository: infisical/staging_deployment_backend
tag: "latest" tag: "latest"
pullPolicy: Always pullPolicy: Always
kubeSecretRef: managed-backend-secret kubeSecretRef: managed-backend-secret
@@ -51,8 +58,8 @@ mongodbConnection:
ingress: ingress:
enabled: true enabled: true
annotations: # annotations:
kubernetes.io/ingress.class: "nginx" # kubernetes.io/ingress.class: "nginx"
# cert-manager.io/issuer: letsencrypt-nginx # cert-manager.io/issuer: letsencrypt-nginx
hostName: gamma.infisical.com ## <- Replace with your own domain hostName: gamma.infisical.com ## <- Replace with your own domain
frontend: 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 name: Build, Publish and Deploy to Gamma
on: on: [workflow_dispatch]
push:
tags:
- "infisical/v*.*.*"
jobs: jobs:
backend-image: backend-image:
name: Build backend image name: Build backend image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source - name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies - name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production run: npm ci --only-production
working-directory: backend working-directory: backend
- name: 🧪 Run tests # - name: 🧪 Run tests
run: npm run test:ci # run: npm run test:ci
working-directory: backend # working-directory: backend
- name: Save commit hashes for tag - name: Save commit hashes for tag
id: commit id: commit
uses: pr-mpt/actions-commit-hash@v2 uses: pr-mpt/actions-commit-hash@v2
@@ -57,18 +51,14 @@ jobs:
push: true push: true
context: backend context: backend
tags: | tags: |
infisical/backend:${{ steps.commit.outputs.short }} infisical/staging_deployment_backend:${{ steps.commit.outputs.short }}
infisical/backend:latest infisical/staging_deployment_backend:latest
infisical/backend:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
frontend-image: frontend-image:
name: Build frontend image name: Build frontend image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source - name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Save commit hashes for tag - name: Save commit hashes for tag
@@ -90,12 +80,12 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
project: 64mmf0n610 project: 64mmf0n610
context: frontend context: frontend
tags: infisical/frontend:test tags: infisical/staging_deployment_frontend:test
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
- name: ⏻ Spawn frontend container - name: ⏻ Spawn frontend container
run: | run: |
docker run -d --rm --name infisical-frontend-test infisical/frontend:test docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test
- name: 🧪 Test frontend image - name: 🧪 Test frontend image
run: | run: |
./.github/resources/healthcheck.sh infisical-frontend-test ./.github/resources/healthcheck.sh infisical-frontend-test
@@ -110,9 +100,8 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend context: frontend
tags: | tags: |
infisical/frontend:${{ steps.commit.outputs.short }} infisical/staging_deployment_frontend:${{ steps.commit.outputs.short }}
infisical/frontend:latest infisical/staging_deployment_frontend:latest
infisical/frontend:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
@@ -146,7 +135,7 @@ jobs:
- name: Download helm values to file and upgrade gamma deploy - name: Download helm values to file and upgrade gamma deploy
run: | run: |
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml 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 if [[ $(helm status infisical) == *"FAILED"* ]]; then
echo "Helm upgrade failed" echo "Helm upgrade failed"
exit 1 exit 1

View File

@@ -1,11 +1,17 @@
name: Release standalone docker image name: Release standalone docker image
on: [workflow_dispatch] on:
push:
tags:
- "infisical/v*.*.*"
jobs: jobs:
infisical-standalone: infisical-standalone:
name: Build infisical standalone image name: Build infisical standalone image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source - name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
@@ -64,5 +70,6 @@ jobs:
tags: | tags: |
infisical/infisical:latest infisical/infisical:latest
infisical/infisical:${{ steps.commit.outputs.short }} infisical/infisical:${{ steps.commit.outputs.short }}
infisical/infisical:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
file: Dockerfile.standalone-infisical file: Dockerfile.standalone-infisical

View File

@@ -1,10 +1,16 @@
name: Release Docker image for K8 operator name: Release Docker image for K8 operator
on: [workflow_dispatch] on:
push:
tags:
- "infisical-k8-operator/v*.*.*"
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - uses: actions/checkout@v2
- name: 🔧 Set up QEMU - name: 🔧 Set up QEMU
@@ -26,4 +32,6 @@ jobs:
context: k8-operator context: k8-operator
push: true push: true
platforms: linux/amd64,linux/arm64 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 node_modules
.env .env
.env.dev .env.dev
.env.gamma
.env.prod .env.prod
.env.infisical .env.infisical
@@ -56,3 +57,6 @@ yarn-error.log*
# Infisical init # Infisical init
.infisical.json .infisical.json
# Editor specific
.vscode/*

View File

@@ -108,6 +108,22 @@ brews:
zsh_completion.install "completions/infisical.zsh" => "_infisical" zsh_completion.install "completions/infisical.zsh" => "_infisical"
fish_completion.install "completions/infisical.fish" fish_completion.install "completions/infisical.fish"
man1.install "manpages/infisical.1.gz" 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: nfpms:
- id: infisical - id: infisical

View File

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

1
.infisicalignore Normal file
View File

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

View File

@@ -1,5 +0,0 @@
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.16.3
hooks:
- id: gitleaks

View File

@@ -1,6 +0,0 @@
- id: infisical-scan
name: Scan for hardcoded secrets
description: Will scan for hardcoded secrets using Infisical CLI
entry: infisical scan git-changes --verbose --redact --staged
language: golang
pass_filenames: false

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,40 @@
{ {
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"], "plugins": [
"@typescript-eslint",
"unused-imports"
],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended" "plugin:@typescript-eslint/recommended"
], ],
"rules": { "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 WORKDIR /app
ENV npm_config_cache /home/node/.npm
COPY package*.json ./ COPY package*.json ./
RUN npm ci --only-production RUN npm ci --only-production && npm cache clean --force
COPY --from=build /app . 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 \ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js CMD node healthcheck.js
EXPOSE 4000 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_LIFETIME: string;
JWT_SIGNUP_SECRET: string; JWT_SIGNUP_SECRET: string;
MONGO_URL: string; MONGO_URL: string;
NODE_ENV: 'development' | 'staging' | 'testing' | 'production'; NODE_ENV: "development" | "staging" | "testing" | "production";
VERBOSE_ERROR_OUTPUT: string; VERBOSE_ERROR_OUTPUT: string;
LOKI_HOST: string; LOKI_HOST: string;
CLIENT_ID_HEROKU: string; CLIENT_ID_HEROKU: string;
@@ -39,12 +39,6 @@ declare global {
SMTP_PASSWORD: string; SMTP_PASSWORD: string;
SMTP_FROM_ADDRESS: string; SMTP_FROM_ADDRESS: string;
SMTP_FROM_NAME: 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; TELEMETRY_ENABLED: string;
LICENSE_KEY: string; LICENSE_KEY: string;
} }

View File

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

17227
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,89 +1,100 @@
import InfisicalClient from 'infisical-node'; import InfisicalClient from "infisical-node";
export const client = new InfisicalClient({ export const client = new InfisicalClient({
token: process.env.INFISICAL_TOKEN! token: process.env.INFISICAL_TOKEN!,
}); });
export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000; export const getPort = async () => (await client.getSecret("PORT")).secretValue || 4000;
export const getEncryptionKey = async () => { export const getEncryptionKey = async () => {
const secretValue = (await client.getSecret('ENCRYPTION_KEY')).secretValue; const secretValue = (await client.getSecret("ENCRYPTION_KEY")).secretValue;
return secretValue === '' ? undefined : secretValue; return secretValue === "" ? undefined : secretValue;
} }
export const getRootEncryptionKey = async () => { export const getRootEncryptionKey = async () => {
const secretValue = (await client.getSecret('ROOT_ENCRYPTION_KEY')).secretValue; const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue;
return secretValue === '' ? undefined : secretValue; return secretValue === "" ? undefined : secretValue;
} }
export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue === 'true' export const getInviteOnlySignup = async () => (await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true"
export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10; export const getSaltRounds = async () => parseInt((await client.getSecret("SALT_ROUNDS")).secretValue) || 10;
export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d'; export const getJwtAuthLifetime = async () => (await client.getSecret("JWT_AUTH_LIFETIME")).secretValue || "10d";
export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue; export const getJwtAuthSecret = async () => (await client.getSecret("JWT_AUTH_SECRET")).secretValue;
export const getJwtMfaLifetime = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m'; export const getJwtMfaLifetime = async () => (await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m";
export const getJwtMfaSecret = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m'; export const getJwtMfaSecret = async () => (await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m";
export const getJwtRefreshLifetime = async () => (await client.getSecret('JWT_REFRESH_LIFETIME')).secretValue || '90d'; export const getJwtRefreshLifetime = async () => (await client.getSecret("JWT_REFRESH_LIFETIME")).secretValue || "90d";
export const getJwtRefreshSecret = async () => (await client.getSecret('JWT_REFRESH_SECRET')).secretValue; export const getJwtRefreshSecret = async () => (await client.getSecret("JWT_REFRESH_SECRET")).secretValue;
export const getJwtServiceSecret = async () => (await client.getSecret('JWT_SERVICE_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 getJwtSignupLifetime = async () => (await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m";
export const getJwtSignupSecret = async () => (await client.getSecret('JWT_SIGNUP_SECRET')).secretValue; export const getJwtProviderAuthSecret = async () => (await client.getSecret("JWT_PROVIDER_AUTH_SECRET")).secretValue;
export const getMongoURL = async () => (await client.getSecret('MONGO_URL')).secretValue; export const getJwtProviderAuthLifetime = async () => (await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m";
export const getNodeEnv = async () => (await client.getSecret('NODE_ENV')).secretValue || 'production'; export const getJwtSignupSecret = async () => (await client.getSecret("JWT_SIGNUP_SECRET")).secretValue;
export const getVerboseErrorOutput = async () => (await client.getSecret('VERBOSE_ERROR_OUTPUT')).secretValue === 'true' && true; export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue;
export const getLokiHost = async () => (await client.getSecret('LOKI_HOST')).secretValue; export const getNodeEnv = async () => (await client.getSecret("NODE_ENV")).secretValue || "production";
export const getClientIdAzure = async () => (await client.getSecret('CLIENT_ID_AZURE')).secretValue; export const getVerboseErrorOutput = async () => (await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true;
export const getClientIdHeroku = async () => (await client.getSecret('CLIENT_ID_HEROKU')).secretValue; export const getLokiHost = async () => (await client.getSecret("LOKI_HOST")).secretValue;
export const getClientIdVercel = async () => (await client.getSecret('CLIENT_ID_VERCEL')).secretValue; export const getClientIdAzure = async () => (await client.getSecret("CLIENT_ID_AZURE")).secretValue;
export const getClientIdNetlify = async () => (await client.getSecret('CLIENT_ID_NETLIFY')).secretValue; export const getClientIdHeroku = async () => (await client.getSecret("CLIENT_ID_HEROKU")).secretValue;
export const getClientIdGitHub = async () => (await client.getSecret('CLIENT_ID_GITHUB')).secretValue; export const getClientIdVercel = async () => (await client.getSecret("CLIENT_ID_VERCEL")).secretValue;
export const getClientIdGitLab = async () => (await client.getSecret('CLIENT_ID_GITLAB')).secretValue; export const getClientIdNetlify = async () => (await client.getSecret("CLIENT_ID_NETLIFY")).secretValue;
export const getClientSecretAzure = async () => (await client.getSecret('CLIENT_SECRET_AZURE')).secretValue; export const getClientIdGitHub = async () => (await client.getSecret("CLIENT_ID_GITHUB")).secretValue;
export const getClientSecretHeroku = async () => (await client.getSecret('CLIENT_SECRET_HEROKU')).secretValue; export const getClientIdGitLab = async () => (await client.getSecret("CLIENT_ID_GITLAB")).secretValue;
export const getClientSecretVercel = async () => (await client.getSecret('CLIENT_SECRET_VERCEL')).secretValue; export const getClientIdBitBucket = async () => (await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
export const getClientSecretNetlify = async () => (await client.getSecret('CLIENT_SECRET_NETLIFY')).secretValue; export const getClientIdGCPSecretManager = async () => (await client.getSecret("CLIENT_ID_GCP_SECRET_MANAGER")).secretValue;
export const getClientSecretGitHub = async () => (await client.getSecret('CLIENT_SECRET_GITHUB')).secretValue; export const getClientSecretAzure = async () => (await client.getSecret("CLIENT_SECRET_AZURE")).secretValue;
export const getClientSecretGitLab = async () => (await client.getSecret('CLIENT_SECRET_GITLAB')).secretValue; export const getClientSecretHeroku = async () => (await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue;
export const getClientSlugVercel = async () => (await client.getSecret('CLIENT_SLUG_VERCEL')).secretValue; export const getClientSecretVercel = async () => (await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue;
export const getPostHogHost = async () => (await client.getSecret('POSTHOG_HOST')).secretValue || 'https://app.posthog.com'; export const getClientSecretNetlify = async () => (await client.getSecret("CLIENT_SECRET_NETLIFY")).secretValue;
export const getPostHogProjectApiKey = async () => (await client.getSecret('POSTHOG_PROJECT_API_KEY')).secretValue || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; export const getClientSecretGitHub = async () => (await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue;
export const getSentryDSN = async () => (await client.getSecret('SENTRY_DSN')).secretValue; export const getClientSecretGitLab = async () => (await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue;
export const getSiteURL = async () => (await client.getSecret('SITE_URL')).secretValue; export const getClientSecretBitBucket = async () => (await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
export const getSmtpHost = async () => (await client.getSecret('SMTP_HOST')).secretValue; export const getClientSecretGCPSecretManager = async () => (await client.getSecret("CLIENT_SECRET_GCP_SECRET_MANAGER")).secretValue;
export const getSmtpSecure = async () => (await client.getSecret('SMTP_SECURE')).secretValue === 'true' || false; export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
export const getSmtpPort = async () => parseInt((await client.getSecret('SMTP_PORT')).secretValue) || 587;
export const getSmtpUsername = async () => (await client.getSecret('SMTP_USERNAME')).secretValue; export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIENT_ID_GOOGLE_LOGIN")).secretValue;
export const getSmtpPassword = async () => (await client.getSecret('SMTP_PASSWORD')).secretValue; export const getClientSecretGoogleLogin = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
export const getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue; export const getClientIdGitHubLogin = async () => (await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical'; 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 () => { export const getLicenseKey = async () => {
const secretValue = (await client.getSecret('LICENSE_KEY')).secretValue; const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue;
return secretValue === '' ? undefined : secretValue; return secretValue === "" ? undefined : secretValue;
} }
export const getLicenseServerKey = async () => { export const getLicenseServerKey = async () => {
const secretValue = (await client.getSecret('LICENSE_SERVER_KEY')).secretValue; const secretValue = (await client.getSecret("LICENSE_SERVER_KEY")).secretValue;
return secretValue === '' ? undefined : secretValue; return secretValue === "" ? undefined : secretValue;
} }
export const getLicenseServerUrl = async () => (await client.getSecret('LICENSE_SERVER_URL')).secretValue || 'https://portal.infisical.com'; export const getLicenseServerUrl = async () => (await client.getSecret("LICENSE_SERVER_URL")).secretValue || "https://portal.infisical.com";
// TODO: deprecate from here export const getTelemetryEnabled = async () => (await client.getSecret("TELEMETRY_ENABLED")).secretValue !== "false" && true;
export const getStripeProductStarter = async () => (await client.getSecret('STRIPE_PRODUCT_STARTER')).secretValue; export const getLoopsApiKey = async () => (await client.getSecret("LOOPS_API_KEY")).secretValue;
export const getStripeProductPro = async () => (await client.getSecret('STRIPE_PRODUCT_PRO')).secretValue; export const getSmtpConfigured = async () => (await client.getSecret("SMTP_HOST")).secretValue == "" || (await client.getSecret("SMTP_HOST")).secretValue == undefined ? false : true
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 getHttpsEnabled = async () => { export const getHttpsEnabled = async () => {
if ((await getNodeEnv()) != "production") { if ((await getNodeEnv()) != "production") {
// no https for anything other than prod // no https for anything other than prod
return false 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 // default when no value present
return true return true
} }
return (await client.getSecret('HTTPS_ENABLED')).secretValue === 'true' && true return (await client.getSecret("HTTPS_ENABLED")).secretValue === "true" && true
} }

View File

@@ -1,16 +1,16 @@
import axios from 'axios'; import axios from "axios";
import axiosRetry from 'axios-retry'; import axiosRetry from "axios-retry";
import { import {
getLicenseServerKeyAuthToken,
setLicenseServerKeyAuthToken,
getLicenseKeyAuthToken, getLicenseKeyAuthToken,
setLicenseKeyAuthToken getLicenseServerKeyAuthToken,
} from './storage'; setLicenseKeyAuthToken,
setLicenseServerKeyAuthToken,
} from "./storage";
import { import {
getLicenseKey, getLicenseKey,
getLicenseServerKey, getLicenseServerKey,
getLicenseServerUrl getLicenseServerUrl,
} from './index'; } from "./index";
// should have JWT to interact with the license server // should have JWT to interact with the license server
export const licenseServerKeyRequest = axios.create(); export const licenseServerKeyRequest = axios.create();
@@ -35,8 +35,8 @@ export const refreshLicenseServerKeyToken = async () => {
`${licenseServerUrl}/api/auth/v1/license-server-login`, {}, `${licenseServerUrl}/api/auth/v1/license-server-login`, {},
{ {
headers: { headers: {
'X-API-KEY': licenseServerKey "X-API-KEY": licenseServerKey,
} },
} }
); );
@@ -53,8 +53,8 @@ export const refreshLicenseKeyToken = async () => {
`${licenseServerUrl}/api/auth/v1/license-login`, {}, `${licenseServerUrl}/api/auth/v1/license-login`, {},
{ {
headers: { headers: {
'X-API-KEY': licenseKey "X-API-KEY": licenseKey,
} },
} }
); );
@@ -86,7 +86,7 @@ licenseServerKeyRequest.interceptors.response.use((response) => {
// refresh // refresh
const token = await refreshLicenseServerKeyToken(); const token = await refreshLicenseServerKeyToken();
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; axios.defaults.headers.common["Authorization"] = "Bearer " + token;
return licenseServerKeyRequest(originalRequest); return licenseServerKeyRequest(originalRequest);
} }
@@ -116,7 +116,7 @@ licenseKeyRequest.interceptors.response.use((response) => {
// refresh // refresh
const token = await refreshLicenseKeyToken(); const token = await refreshLicenseKeyToken();
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; axios.defaults.headers.common["Authorization"] = "Bearer " + token;
return licenseKeyRequest(originalRequest); return licenseKeyRequest(originalRequest);
} }

View File

@@ -5,7 +5,7 @@ const MemoryLicenseServerKeyTokenStorage = () => {
setToken: (token: string) => { setToken: (token: string) => {
authToken = token; authToken = token;
}, },
getToken: () => authToken getToken: () => authToken,
}; };
}; };
@@ -16,7 +16,7 @@ const MemoryLicenseKeyTokenStorage = () => {
setToken: (token: string) => { setToken: (token: string) => {
authToken = token; authToken = token;
}, },
getToken: () => authToken getToken: () => authToken,
}; };
}; };

View File

@@ -1,29 +1,29 @@
import * as Sentry from '@sentry/node'; import { Request, Response } from "express";
import { Request, Response } from 'express'; import jwt from "jsonwebtoken";
import jwt from 'jsonwebtoken'; import * as bigintConversion from "bigint-conversion";
import * as bigintConversion from 'bigint-conversion';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp'); const jsrp = require("jsrp");
import { User, LoginSRPDetail } from '../../models'; import { LoginSRPDetail, TokenVersion, User } from "../../models";
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth'; import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth";
import { checkUserDevice } from '../../helpers/user'; import { checkUserDevice } from "../../helpers/user";
import { ACTION_LOGIN, ACTION_LOGOUT } from "../../variables";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { EELogService } from "../../ee/services";
import { getUserAgentType } from "../../utils/posthog";
import { import {
ACTION_LOGIN, getHttpsEnabled,
ACTION_LOGOUT
} from '../../variables';
import { BadRequestError } from '../../utils/errors';
import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getJwtRefreshSecret,
getJwtAuthLifetime, getJwtAuthLifetime,
getJwtAuthSecret, getJwtAuthSecret,
getHttpsEnabled getJwtRefreshSecret
} from '../../config'; } 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 { export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string; userId: string;
refreshVersion?: number;
} }
} }
@@ -34,47 +34,42 @@ declare module 'jsonwebtoken' {
* @returns * @returns
*/ */
export const login1 = async (req: Request, res: Response) => { export const login1 = async (req: Request, res: Response) => {
try { const {
const { body: { email, clientPublicKey }
email, } = await validateRequest(reqValidator.Login1V1, req);
clientPublicKey
}: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email email
}).select('+salt +verifier'); }).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(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier verifier: user.verifier
}, },
async () => { async () => {
// generate server-side public key // generate server-side public key
const serverPublicKey = server.getPublicKey(); const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: email }, { await LoginSRPDetail.findOneAndReplace(
{ email: email },
{
email: email, email: email,
clientPublicKey: clientPublicKey, clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt), serverBInt: bigintConversion.bigintToBuf(server.bInt)
}, { upsert: true, returnNewDocument: false }) },
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({ return res.status(200).send({
serverPublicKey, serverPublicKey,
salt: user.salt salt: user.salt
}); });
} }
); );
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
}; };
/** /**
@@ -85,84 +80,88 @@ export const login1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const login2 = async (req: Request, res: Response) => { export const login2 = async (req: Request, res: Response) => {
try { const {
const { email, clientProof } = req.body; body: { email, clientProof }
const user = await User.findOne({ } = await validateRequest(reqValidator.Login2V1, req);
email
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
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) { const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email });
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(); if (!loginSRPDetailFromDB) {
server.init( return BadRequestError(
{ Error(
salt: user.salt, "It looks like some details from the first login are not found. Please try login one again"
verifier: user.verifier, )
b: loginSRPDetailFromDB.serverBInt );
}, }
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys const server = new jsrp.server();
if (server.checkClientProof(clientProof)) { server.init(
// issue tokens {
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
await checkUserDevice({ // compare server and client shared keys
user, if (server.checkClientProof(clientProof)) {
ip: req.ip, // issue tokens
userAgent: req.headers['user-agent'] ?? ''
});
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 const tokens = await issueAuthTokens({
res.cookie('jid', tokens.refreshToken, { userId: user._id,
httpOnly: true, ip: req.realIP,
path: '/', userAgent: req.headers["user-agent"] ?? ""
sameSite: 'strict', });
secure: await getHttpsEnabled()
});
const loginAction = await EELogService.createAction({ // store (refresh) token in httpOnly cookie
name: ACTION_LOGIN, res.cookie("jid", tokens.refreshToken, {
userId: user._id 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, userId: user._id,
actions: [loginAction], actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']), channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.ip ipAddress: req.realIP
}); }));
// return (access) token in response // return (access) token in response
return res.status(200).send({ return res.status(200).send({
token: tokens.token, token: tokens.token,
publicKey: user.publicKey, publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey, encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv, iv: user.iv,
tag: user.tag tag: user.tag
});
}
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
}); });
} }
);
} catch (err) { return res.status(400).send({
Sentry.setUser(null); message: "Failed to authenticate. Try again?"
Sentry.captureException(err); });
return res.status(400).send({ }
message: 'Failed to authenticate. Try again?' );
});
}
}; };
/** /**
@@ -172,41 +171,51 @@ export const login2 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const logout = async (req: Request, res: Response) => { export const logout = async (req: Request, res: Response) => {
try { if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
await clearTokens({ await clearTokens(req.authData.tokenVersionId);
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'
});
} }
// 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({ 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) => { export const checkAuth = async (req: Request, res: Response) => {
return res.status(200).send({ 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 req
* @param res * @param res
* @returns * @returns
*/ */
export const getNewToken = async (req: Request, res: Response) => { export const getNewToken = async (req: Request, res: Response) => {
try { const refreshToken = req.cookies.jid;
const refreshToken = req.cookies.jid;
if (!refreshToken) { if (!refreshToken)
throw new Error('Failed to find token in request cookies'); throw BadRequestError({
} message: "Failed to find refresh token in request cookies"
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(refreshToken, await getJwtRefreshSecret())
);
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
if (!user) throw new Error('Failed to authenticate unfound user');
if (!user?.publicKey)
throw new Error('Failed to authenticate not fully set up account');
const token = createToken({
payload: {
userId: decodedToken.userId
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret()
}); });
return res.status(200).send({ const decodedToken = <jwt.UserIDJwtPayload>jwt.verify(refreshToken, await getJwtRefreshSecret());
token
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); if (decodedToken.refreshVersion !== tokenVersion.refreshVersion)
Sentry.captureException(err); throw BadRequestError({
return res.status(400).send({ message: "Failed to validate refresh token"
message: 'Invalid request'
}); });
}
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 { Request, Response } from "express";
import { Types } from 'mongoose'; import { Types } from "mongoose";
import * as Sentry from '@sentry/node'; import { Bot, BotKey } from "../../models";
import { Bot, BotKey } from '../../models'; import { createBot } from "../../helpers/bot";
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 { interface BotKey {
encryptedKey: string; encryptedKey: string;
nonce: string; nonce: string;
} }
/** /**
@@ -17,33 +25,31 @@ interface BotKey {
* @returns * @returns
*/ */
export const getBotByWorkspaceId = async (req: Request, res: Response) => { export const getBotByWorkspaceId = async (req: Request, res: Response) => {
let bot; const {
try { params: { workspaceId }
const { workspaceId } = req.params; } = 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({ let bot = await Bot.findOne({
workspace: workspaceId workspace: workspaceId
}); });
if (!bot) { if (!bot) {
// case: bot doesn't exist for workspace with id [workspaceId] // case: bot doesn't exist for workspace with id [workspaceId]
// -> create a new bot and return it // -> create a new bot and return it
bot = await createBot({ bot = await createBot({
name: 'Infisical Bot', name: "Infisical Bot",
workspaceId: new Types.ObjectId(workspaceId) 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
}); });
}
return res.status(200).send({
bot
});
}; };
/** /**
@@ -53,56 +59,69 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const setBotActiveState = async (req: Request, res: Response) => { export const setBotActiveState = async (req: Request, res: Response) => {
let bot; const {
try { body: { botKey, isActive },
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body; params: { botId }
} = await validateRequest(reqValidator.SetBotActiveStateV1, req);
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
});
}
bot = await Bot.findOneAndUpdate({ const bot = await Bot.findById(botId);
_id: req.bot._id if (!bot) {
}, { throw BadRequestError({ message: "Bot not found" });
isActive }
}, { const userId = req.user._id;
new: true
}); const { permission } = await getUserProjectPermissions(userId, bot.workspace.toString());
ForbiddenError.from(permission).throwUnlessCan(
if (!bot) throw new Error('Failed to update bot active state'); ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
} catch (err) { );
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); if (isActive) {
return res.status(400).send({ // bot state set to active -> share workspace key with bot
message: 'Failed to update bot active state' if (!botKey?.encryptedKey || !botKey?.nonce) {
}); return res.status(400).send({
} message: "Failed to set bot state to active - missing bot key"
});
return res.status(200).send({ }
bot
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 authController from "./authController";
import * as botController from './botController'; import * as botController from "./botController";
import * as integrationAuthController from './integrationAuthController'; import * as integrationAuthController from "./integrationAuthController";
import * as integrationController from './integrationController'; import * as integrationController from "./integrationController";
import * as keyController from './keyController'; import * as keyController from "./keyController";
import * as membershipController from './membershipController'; import * as membershipController from "./membershipController";
import * as membershipOrgController from './membershipOrgController'; import * as membershipOrgController from "./membershipOrgController";
import * as organizationController from './organizationController'; import * as organizationController from "./organizationController";
import * as passwordController from './passwordController'; import * as passwordController from "./passwordController";
import * as secretController from './secretController'; import * as secretController from "./secretController";
import * as serviceTokenController from './serviceTokenController'; import * as serviceTokenController from "./serviceTokenController";
import * as signupController from './signupController'; import * as signupController from "./signupController";
import * as stripeController from './stripeController'; import * as userActionController from "./userActionController";
import * as userActionController from './userActionController'; import * as userController from "./userController";
import * as userController from './userController'; import * as workspaceController from "./workspaceController";
import * as workspaceController from './workspaceController'; import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController";
import * as secretImpsController from "./secretImpsController";
export { export {
authController, authController,
botController, botController,
integrationAuthController, integrationAuthController,
integrationController, integrationController,
keyController, keyController,
membershipController, membershipController,
membershipOrgController, membershipOrgController,
organizationController, organizationController,
passwordController, passwordController,
secretController, secretController,
serviceTokenController, serviceTokenController,
signupController, signupController,
stripeController, userActionController,
userActionController, userController,
userController, workspaceController,
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 { Request, Response } from "express";
import { Types } from 'mongoose'; import { Types } from "mongoose";
import * as Sentry from '@sentry/node'; import { Folder, IWorkspace, Integration, IntegrationAuth } from "../../models";
import { import { EventService } from "../../services";
Integration import { eventStartIntegration } from "../../events";
} from '../../models'; import { getFolderByPath } from "../../services/FolderService";
import { EventService } from '../../services'; import { BadRequestError } from "../../utils/errors";
import { eventPushSecrets } from '../../events'; 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 * Create/initialize an (empty) integration for integration authorization
@@ -14,64 +24,115 @@ import { eventPushSecrets } from '../../events';
* @returns * @returns
*/ */
export const createIntegration = async (req: Request, res: Response) => { export const createIntegration = async (req: Request, res: Response) => {
let integration; const {
body: {
try {
const {
integrationAuthId,
app,
appId,
isActive,
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region
} = req.body;
// TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token
integration = await new Integration({
workspace: req.integrationAuth.workspace._id,
environment: sourceEnvironment,
isActive, isActive,
sourceEnvironment,
secretPath,
app, app,
appId, path,
targetEnvironment, appId,
targetEnvironmentId, owner,
region,
targetService, targetService,
targetServiceId, targetServiceId,
owner, integrationAuthId,
path, targetEnvironment,
region, targetEnvironmentId,
integration: req.integrationAuth.integration, metadata
integrationAuth: new Types.ObjectId(integrationAuthId) }
}).save(); } = await validateRequest(reqValidator.CreateIntegrationV1, req);
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment: sourceEnvironment
})
});
}
} catch (err) { const integrationAuth = await IntegrationAuth.findById(integrationAuthId)
Sentry.setUser({ email: req.user.email }); .populate<{ workspace: IWorkspace }>("workspace")
Sentry.captureException(err); .select(
return res.status(400).send({ "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt"
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,
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({ return res.status(200).send({
integration, integration
}); });
}; };
@@ -82,86 +143,156 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateIntegration = async (req: Request, res: Response) => { export const updateIntegration = async (req: Request, res: Response) => {
let integration;
// TODO: add integration-specific validation to ensure that each // TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration] // integration has the correct fields populated in [Integration]
try { const {
const { body: {
environment, environment,
isActive, isActive,
app, app,
appId, appId,
targetEnvironment, targetEnvironment,
owner, // github-specific integration param owner, // github-specific integration param
} = req.body; secretPath
},
params: { integrationId }
} = await validateRequest(reqValidator.UpdateIntegrationV1, req);
integration = await Integration.findOneAndUpdate( const integration = await Integration.findById(integrationId);
{ if (!integration) throw BadRequestError({ message: "Integration not found" });
_id: req.integration._id,
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner,
},
{
new: true,
}
);
if (integration) { const { permission } = await getUserProjectPermissions(
// trigger event - push secrets req.user._id,
EventService.handleEvent({ integration.workspace.toString()
event: eventPushSecrets({ );
workspaceId: integration.workspace, ForbiddenError.from(permission).throwUnlessCan(
environment 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); const updatedIntegration = await Integration.findOneAndUpdate(
return res.status(400).send({ {
message: "Failed to update integration", _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({ return res.status(200).send({
integration, integration: updatedIntegration
}); });
}; };
/** /**
* Delete integration with id [integrationId] and deactivate bot if there are * Delete integration with id [integrationId]
* no integrations left
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteIntegration = async (req: Request, res: Response) => { export const deleteIntegration = async (req: Request, res: Response) => {
let integration; const {
try { params: { integrationId }
const { integrationId } = req.params; } = await validateRequest(reqValidator.DeleteIntegrationV1, req);
integration = await Integration.findOneAndDelete({ const integration = await Integration.findById(integrationId);
_id: integrationId, if (!integration) throw BadRequestError({ message: "Integration not found" });
});
if (!integration) throw new Error("Failed to find integration"); const { permission } = await getUserProjectPermissions(
} catch (err) { req.user._id,
Sentry.setUser({ email: req.user.email }); integration.workspace.toString()
Sentry.captureException(err); );
return res.status(400).send({ ForbiddenError.from(permission).throwUnlessCan(
message: "Failed to delete integration", 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({ 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 { Types } from "mongoose";
import * as Sentry from '@sentry/node'; import { Request, Response } from "express";
import { Key } from '../../models'; import { Key } from "../../models";
import { findMembership } from '../../helpers/membership'; 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 * Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
@@ -11,38 +21,38 @@ import { findMembership } from '../../helpers/membership';
* @returns * @returns
*/ */
export const uploadKey = async (req: Request, res: Response) => { export const uploadKey = async (req: Request, res: Response) => {
try { const {
const { workspaceId } = req.params; params: { workspaceId },
const { key } = req.body; body: { key }
} = await validateRequest(reqValidator.UploadKeyV1, req);
// validate membership of receiver const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const receiverMembership = await findMembership({ ForbiddenError.from(permission).throwUnlessCan(
user: key.userId, ProjectPermissionActions.Edit,
workspace: workspaceId ProjectPermissionSub.Member
}); );
if (!receiverMembership) { // validate membership of receiver
throw new Error('Failed receiver membership validation for workspace'); const receiverMembership = await findMembership({
} user: key.userId,
workspace: workspaceId
});
await new Key({ if (!receiverMembership) {
encryptedKey: key.encryptedKey, throw new Error("Failed receiver membership validation for workspace");
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'
});
}
return res.status(200).send({ await new Key({
message: 'Successfully uploaded key to workspace' 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 * @returns
*/ */
export const getLatestKey = async (req: Request, res: Response) => { export const getLatestKey = async (req: Request, res: Response) => {
let latestKey; const {
try { params: { workspaceId }
const { workspaceId } = req.params; } = await validateRequest(reqValidator.GetLatestKeyV1, req);
// get latest key // get latest key
latestKey = await Key.find({ const latestKey = await Key.find({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.limit(1) .limit(1)
.populate('sender', '+publicKey'); .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'
});
}
const resObj: any = {}; const resObj: any = {};
if (latestKey.length > 0) { if (latestKey.length > 0) {
resObj['latestKey'] = latestKey[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 { Request, Response } from 'express'; import { Types } from "mongoose";
import { Membership, MembershipOrg, User, Key } from '../../models'; 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 { import {
findMembership, ProjectPermissionActions,
deleteMembership as deleteMember ProjectPermissionSub,
} from '../../helpers/membership'; getUserProjectPermissions
import { sendMail } from '../../helpers/nodemailer'; } from "../../ee/services/ProjectRoleService";
import { ADMIN, MEMBER, ACCEPTED } from '../../variables'; import { ForbiddenError } from "@casl/ability";
import { getSiteURL } from '../../config'; 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] * Check that user is a member of workspace with id [workspaceId]
@@ -16,29 +26,23 @@ import { getSiteURL } from '../../config';
* @returns * @returns
*/ */
export const validateMembership = async (req: Request, res: Response) => { export const validateMembership = async (req: Request, res: Response) => {
try { const {
const { workspaceId } = req.params; params: { workspaceId }
} = await validateRequest(reqValidator.ValidateMembershipV1, req);
// validate membership // validate membership
const membership = await findMembership({ const membership = await findMembership({
user: req.user._id, user: req.user._id,
workspace: workspaceId workspace: workspaceId
}); });
if (!membership) { if (!membership) {
throw new Error('Failed to validate 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'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Workspace membership confirmed' message: "Workspace membership confirmed"
}); });
}; };
/** /**
@@ -48,52 +52,50 @@ export const validateMembership = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteMembership = async (req: Request, res: Response) => { export const deleteMembership = async (req: Request, res: Response) => {
let deletedMembership; const {
try { params: { membershipId }
const { membershipId } = req.params; } = await validateRequest(reqValidator.DeleteMembershipV1, req);
// check if membership to delete exists // check if membership to delete exists
const membershipToDelete = await Membership.findOne({ const membershipToDelete = await Membership.findOne({
_id: membershipId _id: membershipId
}).populate('user'); }).populate<{ user: IUser }>("user");
if (!membershipToDelete) { if (!membershipToDelete) {
throw new Error( throw new Error("Failed to delete workspace membership that doesn't exist");
"Failed to delete workspace membership that doesn't exist" }
);
}
// check if user is a member and admin of the workspace const { permission } = await getUserProjectPermissions(
// whose membership we wish to delete req.user._id,
const membership = await Membership.findOne({ membershipToDelete.workspace.toString()
user: req.user._id, );
workspace: membershipToDelete.workspace ForbiddenError.from(permission).throwUnlessCan(
}); ProjectPermissionActions.Delete,
ProjectPermissionSub.Member
);
if (!membership) { // delete workspace membership
throw new Error('Failed to validate workspace membership'); const deletedMembership = await deleteMember({
} membershipId: membershipToDelete._id.toString()
});
if (membership.role !== ADMIN) { await EEAuditLogService.createAuditLog(
// user is not an admin member of the workspace req.authData,
throw new Error('Insufficient role for deleting workspace membership'); {
} type: EventType.REMOVE_WORKSPACE_MEMBER,
metadata: {
userId: membershipToDelete.user._id.toString(),
email: membershipToDelete.user.email
}
},
{
workspaceId: membershipToDelete.workspace
}
);
// delete workspace membership return res.status(200).send({
deletedMembership = await deleteMember({ deletedMembership
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
});
}; };
/** /**
@@ -103,53 +105,80 @@ export const deleteMembership = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changeMembershipRole = async (req: Request, res: Response) => { export const changeMembershipRole = async (req: Request, res: Response) => {
let membershipToChangeRole; const {
try { body: { role },
const { membershipId } = req.params; params: { membershipId }
const { role } = req.body; } = await validateRequest(reqValidator.ChangeMembershipRoleV1, req);
if (![ADMIN, MEMBER].includes(role)) { // validate target membership
throw new Error('Failed to validate role'); const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>(
} "user"
);
// validate target membership if (!membershipToChangeRole) {
membershipToChangeRole = await findMembership({ throw new Error("Failed to find membership to change role");
_id: membershipId }
});
if (!membershipToChangeRole) { const { permission } = await getUserProjectPermissions(
throw new Error('Failed to find membership to change role'); 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 const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
// workspace if (isCustomRole) {
const membership = await findMembership({ const wsRole = await Role.findOne({
user: req.user._id, slug: role,
workspace: membershipToChangeRole.workspace 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) { const membership = await Membership.findByIdAndUpdate(
throw new Error('Failed to validate membership'); membershipId,
} {
$set: {
role
},
$unset: {
customRole: 1
}
},
{
new: true
}
);
if (membership.role !== ADMIN) { await EEAuditLogService.createAuditLog(
// user is not an admin member of the workspace req.authData,
throw new Error('Insufficient role for changing member roles'); {
} 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; return res.status(200).send({
await membershipToChangeRole.save(); membership
} 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
});
}; };
/** /**
@@ -159,75 +188,86 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const inviteUserToWorkspace = async (req: Request, res: Response) => { export const inviteUserToWorkspace = async (req: Request, res: Response) => {
let invitee, latestKey; const {
try { params: { workspaceId },
const { workspaceId } = req.params; body: { email }
const { email }: { email: string } = req.body; } = await validateRequest(InviteUserToWorkspaceV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Member
);
invitee = await User.findOne({ const invitee = await User.findOne({
email email
}).select('+publicKey'); }).select("+publicKey");
if (!invitee || !invitee?.publicKey) if (!invitee || !invitee?.publicKey) throw new Error("Failed to validate invitee");
throw new Error('Failed to validate invitee');
// validate invitee's workspace membership - ensure member isn't // validate invitee's workspace membership - ensure member isn't
// already a member of the workspace // already a member of the workspace
const inviteeMembership = await Membership.findOne({ const inviteeMembership = await Membership.findOne({
user: invitee._id, user: invitee._id,
workspace: workspaceId workspace: workspaceId
}); }).populate<{ user: IUser }>("user");
if (inviteeMembership) if (inviteeMembership) throw new Error("Failed to add existing member of workspace");
throw new Error('Failed to add existing member of workspace');
// validate invitee's organization membership - ensure that only const workspace = await Workspace.findById(workspaceId);
// (accepted) organization members can be added to the workspace if (!workspace) throw new Error("Failed to find workspace");
const membershipOrg = await MembershipOrg.findOne({ // validate invitee's organization membership - ensure that only
user: invitee._id, // (accepted) organization members can be added to the workspace
organization: req.membership.workspace.organization, const membershipOrg = await MembershipOrg.findOne({
status: ACCEPTED user: invitee._id,
}); organization: workspace.organization,
status: ACCEPTED
});
if (!membershipOrg) if (!membershipOrg) throw new Error("Failed to validate invitee's organization membership");
throw new Error("Failed to validate invitee's organization membership");
// get latest key // get latest key
latestKey = await Key.findOne({ const latestKey = await Key.findOne({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.populate('sender', '+publicKey'); .populate("sender", "+publicKey");
// create new workspace membership // create new workspace membership
const m = await new Membership({ await new Membership({
user: invitee._id, user: invitee._id,
workspace: workspaceId, workspace: workspaceId,
role: MEMBER role: MEMBER
}).save(); }).save();
await sendMail({ await sendMail({
template: 'workspaceInvitation.handlebars', template: "workspaceInvitation.handlebars",
subjectLine: 'Infisical workspace invitation', subjectLine: "Infisical workspace invitation",
recipients: [invitee.email], recipients: [invitee.email],
substitutions: { substitutions: {
inviterFirstName: req.user.firstName, inviterFirstName: req.user.firstName,
inviterEmail: req.user.email, inviterEmail: req.user.email,
workspaceName: req.membership.workspace.name, workspaceName: workspace.name,
callback_url: (await getSiteURL()) + '/login' 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'
});
}
return res.status(200).send({ await EEAuditLogService.createAuditLog(
invitee, req.authData,
latestKey {
}); 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 { Types } from "mongoose";
import { Request, Response } from 'express'; import { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import { MembershipOrg, Organization, User } from "../../models";
import { MembershipOrg, Organization, User } from '../../models'; import { SSOConfig } from "../../ee/models";
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg'; import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg";
import { createToken } from '../../helpers/auth'; import { createToken } from "../../helpers/auth";
import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
import { sendMail } from '../../helpers/nodemailer'; import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from '../../services'; import { TokenService } from "../../services";
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables'; import { EELicenseService } from "../../ee/services";
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config'; 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 * Delete organization membership with id [membershipOrgId] from organization
@@ -16,55 +31,39 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured
* @param res * @param res
* @returns * @returns
*/ */
export const deleteMembershipOrg = async (req: Request, res: Response) => { export const deleteMembershipOrg = async (req: Request, _res: Response) => {
let membershipOrgToDelete; const {
try { params: { membershipOrgId }
const { membershipOrgId } = req.params; } = await validateRequest(reqValidator.DelOrgMembershipv1, req);
// check if organization membership to delete exists // check if organization membership to delete exists
membershipOrgToDelete = await MembershipOrg.findOne({ const membershipOrgToDelete = await MembershipOrg.findOne({
_id: membershipOrgId _id: membershipOrgId
}).populate('user'); }).populate("user");
if (!membershipOrgToDelete) { if (!membershipOrgToDelete) {
throw new Error( throw new Error("Failed to delete organization membership that doesn't exist");
"Failed to delete organization membership that doesn't exist" }
);
}
// check if user is a member and admin of the organization const { permission, membership: membershipOrg } = await getUserOrgPermissions(
// whose membership we wish to delete req.user._id,
const membershipOrg = await MembershipOrg.findOne({ membershipOrgToDelete.organization.toString()
user: req.user._id, );
organization: membershipOrgToDelete.organization ForbiddenError.from(permission).throwUnlessCan(
}); OrgPermissionActions.Delete,
OrgPermissionSubjects.Member
);
if (!membershipOrg) { // delete organization membership
throw new Error('Failed to validate organization membership'); await deleteMemberFromOrg({
} membershipOrgId: membershipOrgToDelete._id.toString()
});
if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) { await updateSubscriptionOrgQuantity({
// user is not an admin member of the organization organizationId: membershipOrg.organization.toString()
throw new Error('Insufficient role for deleting organization membership'); });
}
// delete organization membership return membershipOrgToDelete;
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;
}; };
/** /**
@@ -74,22 +73,14 @@ export const deleteMembershipOrg = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changeMembershipOrgRole = async (req: Request, res: Response) => { export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// change role for (target) organization membership with id // change role for (target) organization membership with id
// [membershipOrgId] // [membershipOrgId]
let membershipToChangeRole; let membershipToChangeRole;
// try {
// } catch (err) {
// Sentry.setUser({ email: req.user.email });
// Sentry.captureException(err);
// return res.status(400).send({
// message: 'Failed to change organization membership role'
// });
// }
return res.status(200).send({ return res.status(200).send({
membershipOrg: membershipToChangeRole membershipOrg: membershipToChangeRole
}); });
}; };
/** /**
@@ -100,112 +91,128 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const inviteUserToOrganization = async (req: Request, res: Response) => { export const inviteUserToOrganization = async (req: Request, res: Response) => {
let invitee, inviteeMembershipOrg, completeInviteLink; let inviteeMembershipOrg, completeInviteLink;
try { const {
const { organizationId, inviteeEmail } = req.body; body: { inviteeEmail, organizationId }
const host = req.headers.host; } = await validateRequest(reqValidator.InviteUserToOrgv1, req);
const siteUrl = `${req.protocol}://${host}`;
// validate membership const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
const membershipOrg = await MembershipOrg.findOne({ ForbiddenError.from(permission).throwUnlessCan(
user: req.user._id, OrgPermissionActions.Create,
organization: organizationId OrgPermissionSubjects.Member
}); );
if (!membershipOrg) { const host = req.headers.host;
throw new Error('Failed to validate organization membership'); const siteUrl = `${req.protocol}://${host}`;
} const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
invitee = await User.findOne({ const ssoConfig = await SSOConfig.findOne({
email: inviteeEmail organization: new Types.ObjectId(organizationId)
}).select('+publicKey'); });
if (invitee) { if (ssoConfig && ssoConfig.isActive) {
// case: invitee is an existing user // 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({ if (plan.memberLimit !== null) {
user: invitee._id, // case: limit imposed on number of members allowed
organization: organizationId
});
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) { if (plan.membersUsed >= plan.memberLimit) {
throw new Error( // case: number of members used exceeds the number of members allowed
'Failed to invite an existing member of the organization' return res.status(400).send({
); message:
} "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
});
}
}
if (!inviteeMembershipOrg) { const invitee = await User.findOne({
email: inviteeEmail
await new MembershipOrg({ }).select("+publicKey");
user: invitee,
inviteEmail: inviteeEmail,
organization: organizationId,
role: MEMBER,
status: INVITED
}).save();
}
} else {
// check if invitee has been invited before
inviteeMembershipOrg = await MembershipOrg.findOne({
inviteEmail: inviteeEmail,
organization: organizationId
});
if (!inviteeMembershipOrg) { if (invitee) {
// case: invitee has never been invited before // case: invitee is an existing user
await new MembershipOrg({ inviteeMembershipOrg = await MembershipOrg.findOne({
inviteEmail: inviteeEmail, user: invitee._id,
organization: organizationId, organization: organizationId
role: MEMBER, });
status: INVITED
}).save();
}
}
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({ if (!inviteeMembershipOrg) {
type: TOKEN_EMAIL_ORG_INVITATION, // case: invitee has never been invited before
email: inviteeEmail,
organizationId: organization._id
});
await sendMail({ // validate that email is not disposable
template: 'organizationInvitation.handlebars', validateUserEmail(inviteeEmail);
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())) { await new MembershipOrg({
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}` inviteEmail: inviteeEmail,
} organization: organizationId,
} role: MEMBER,
status: INVITED
}).save();
}
}
await updateSubscriptionOrgQuantity({ organizationId }); const organization = await Organization.findOne({ _id: organizationId });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send organization invite'
});
}
return res.status(200).send({ if (organization) {
message: `Sent an invite link to ${req.body.inviteeEmail}`, const token = await TokenService.createToken({
completeInviteLink 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
});
}; };
/** /**
@@ -216,74 +223,64 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const verifyUserToOrganization = async (req: Request, res: Response) => { export const verifyUserToOrganization = async (req: Request, res: Response) => {
let user, token; let user;
try {
const {
email,
organizationId,
code
} = req.body;
user = await User.findOne({ email }).select('+publicKey'); const {
body: { organizationId, email, code }
} = await validateRequest(reqValidator.VerifyUserToOrgv1, req);
const membershipOrg = await MembershipOrg.findOne({ user = await User.findOne({ email }).select("+publicKey");
inviteEmail: email,
status: INVITED,
organization: new Types.ObjectId(organizationId)
});
if (!membershipOrg) const membershipOrg = await MembershipOrg.findOne({
throw new Error('Failed to find any invitations for email'); inviteEmail: email,
status: INVITED,
organization: new Types.ObjectId(organizationId)
});
await TokenService.validateToken({ if (!membershipOrg) throw new Error("Failed to find any invitations for email");
type: TOKEN_EMAIL_ORG_INVITATION,
email,
organizationId: membershipOrg.organization,
token: code
});
if (user && user?.publicKey) { await TokenService.validateToken({
// case: user has already completed account type: TOKEN_EMAIL_ORG_INVITATION,
// membership can be approved and redirected to login/dashboard email,
membershipOrg.status = ACCEPTED; organizationId: membershipOrg.organization,
await membershipOrg.save(); token: code
});
await updateSubscriptionOrgQuantity({
organizationId
});
return res.status(200).send({ if (user && user?.publicKey) {
message: 'Successfully verified email', // case: user has already completed account
user, // membership can be approved and redirected to login/dashboard
}); membershipOrg.status = ACCEPTED;
} await membershipOrg.save();
if (!user) { await updateSubscriptionOrgQuantity({
// initialize user account organizationId
user = await new User({ });
email
}).save();
}
// generate temporary signup token return res.status(200).send({
token = createToken({ message: "Successfully verified email",
payload: { user
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({ if (!user) {
message: 'Successfully verified email', // initialize user account
user, user = await new User({
token 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 { Request, Response } from 'express';
import Stripe from 'stripe';
import { import {
Membership, IncidentContactOrg,
MembershipOrg, Membership,
Organization, MembershipOrg,
Workspace, Organization,
IncidentContactOrg Workspace
} from '../../models'; } from "../../models";
import { createOrganization as create } from '../../helpers/organization'; import { createOrganization as create } from "../../helpers/organization";
import { addMembershipsOrg } from '../../helpers/membershipOrg'; import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { OWNER, ACCEPTED } from '../../variables'; import { ACCEPTED, ADMIN } from "../../variables";
import _ from 'lodash'; import { getLicenseServerUrl, getSiteURL } from "../../config";
import { getStripeSecretKey, 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) => { export const getOrganizations = async (req: Request, res: Response) => {
let organizations; const organizations = (
try { await MembershipOrg.find({
organizations = ( user: req.user._id,
await MembershipOrg.find({ status: ACCEPTED
user: req.user._id, }).populate("organization")
status: ACCEPTED ).map((m) => m.organization);
}).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'
});
}
return res.status(200).send({ return res.status(200).send({
organizations organizations
}); });
}; };
/** /**
@@ -44,37 +42,26 @@ export const getOrganizations = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createOrganization = async (req: Request, res: Response) => { export const createOrganization = async (req: Request, res: Response) => {
let organization; const {
try { body: { organizationName }
const { organizationName } = req.body; } = await validateRequest(reqValidator.CreateOrgv1, req);
if (organizationName.length < 1) { // create organization and add user as member
throw new Error('Organization names must be at least 1-character long'); const organization = await create({
} email: req.user.email,
name: organizationName
});
// create organization and add user as member await addMembershipsOrg({
organization = await create({ userIds: [req.user._id.toString()],
email: req.user.email, organizationId: organization._id.toString(),
name: organizationName roles: [ADMIN],
}); statuses: [ACCEPTED]
});
await addMembershipsOrg({ return res.status(200).send({
userIds: [req.user._id.toString()], organization
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
});
}; };
/** /**
@@ -84,20 +71,23 @@ export const createOrganization = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getOrganization = async (req: Request, res: Response) => { export const getOrganization = async (req: Request, res: Response) => {
let organization; const {
try { params: { organizationId }
organization = req.organization } = await validateRequest(reqValidator.GetOrgv1, req);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to find organization'
});
}
return res.status(200).send({ // ensure user has membership
organization 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 * @returns
*/ */
export const getOrganizationMembers = async (req: Request, res: Response) => { export const getOrganizationMembers = async (req: Request, res: Response) => {
let users; const {
try { params: { organizationId }
const { organizationId } = req.params; } = await validateRequest(reqValidator.GetOrgMembersv1, req);
users = await MembershipOrg.find({ const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
organization: organizationId ForbiddenError.from(permission).throwUnlessCan(
}).populate('user', '+publicKey'); OrgPermissionActions.Read,
} catch (err) { OrgPermissionSubjects.Member
Sentry.setUser({ email: req.user.email }); );
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization members'
});
}
return res.status(200).send({ const users = await MembershipOrg.find({
users 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 * @param res
* @returns * @returns
*/ */
export const getOrganizationWorkspaces = async ( export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId }
) => { } = await validateRequest(reqValidator.GetOrgWorkspacesv1, req);
let workspaces;
try {
const { organizationId } = req.params;
const workspacesSet = new Set( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
( ForbiddenError.from(permission).throwUnlessCan(
await Workspace.find( OrgPermissionActions.Read,
{ OrgPermissionSubjects.Workspace
organization: organizationId );
},
'_id'
)
).map((w) => w._id.toString())
);
workspaces = ( const workspacesSet = new Set(
await Membership.find({ (
user: req.user._id await Workspace.find(
}).populate('workspace') {
) organization: organizationId
.filter((m) => workspacesSet.has(m.workspace._id.toString())) },
.map((m) => m.workspace); "_id"
} catch (err) { )
Sentry.setUser({ email: req.user.email }); ).map((w) => w._id.toString())
Sentry.captureException(err); );
return res.status(400).send({
message: 'Failed to get my workspaces'
});
}
return res.status(200).send({ const workspaces = (
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 * @returns
*/ */
export const changeOrganizationName = async (req: Request, res: Response) => { export const changeOrganizationName = async (req: Request, res: Response) => {
let organization; const {
try { params: { organizationId },
const { organizationId } = req.params; body: { name }
const { name } = req.body; } = await validateRequest(reqValidator.ChangeOrgNamev1, req);
organization = await Organization.findOneAndUpdate( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
{ ForbiddenError.from(permission).throwUnlessCan(
_id: organizationId OrgPermissionActions.Edit,
}, OrgPermissionSubjects.Settings
{ );
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'
});
}
return res.status(200).send({ const organization = await Organization.findOneAndUpdate(
message: 'Successfully changed organization name', {
organization _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 * @param res
* @returns * @returns
*/ */
export const getOrganizationIncidentContacts = async ( export const getOrganizationIncidentContacts = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId }
) => { } = await validateRequest(reqValidator.GetOrgIncidentContactv1, req);
let incidentContactsOrg;
try {
const { organizationId } = req.params;
incidentContactsOrg = await IncidentContactOrg.find({ const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
organization: organizationId ForbiddenError.from(permission).throwUnlessCan(
}); OrgPermissionActions.Read,
} catch (err) { OrgPermissionSubjects.IncidentAccount
Sentry.setUser({ email: req.user.email }); );
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization incident contacts'
});
}
return res.status(200).send({ const incidentContactsOrg = await IncidentContactOrg.find({
incidentContactsOrg organization: organizationId
}); });
return res.status(200).send({
incidentContactsOrg
});
}; };
/** /**
@@ -245,31 +225,27 @@ export const getOrganizationIncidentContacts = async (
* @param res * @param res
* @returns * @returns
*/ */
export const addOrganizationIncidentContact = async ( export const addOrganizationIncidentContact = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId },
) => { body: { email }
let incidentContactOrg; } = await validateRequest(reqValidator.CreateOrgIncideContact, req);
try {
const { organizationId } = req.params;
const { email } = req.body;
incidentContactOrg = await IncidentContactOrg.findOneAndUpdate( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
{ email, organization: organizationId }, ForbiddenError.from(permission).throwUnlessCan(
{ email, organization: organizationId }, OrgPermissionActions.Create,
{ upsert: true, new: true } OrgPermissionSubjects.IncidentAccount
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to add incident contact for organization'
});
}
return res.status(200).send({ const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
incidentContactOrg { 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 * @param res
* @returns * @returns
*/ */
export const deleteOrganizationIncidentContact = async ( export const deleteOrganizationIncidentContact = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId },
) => { body: { email }
let incidentContactOrg; } = await validateRequest(reqValidator.DelOrgIncideContact, req);
try {
const { organizationId } = req.params;
const { email } = req.body;
incidentContactOrg = await IncidentContactOrg.findOneAndDelete({ const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
email, ForbiddenError.from(permission).throwUnlessCan(
organization: organizationId OrgPermissionActions.Delete,
}); OrgPermissionSubjects.IncidentAccount
} catch (err) { );
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization incident contact'
});
}
return res.status(200).send({ const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
message: 'Successfully deleted organization incident contact', email,
incidentContactOrg 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 * if there is a card on file
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const createOrganizationPortalSession = async ( export const createOrganizationPortalSession = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId }
) => { } = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
let session;
try {
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
// check if there is a payment method on file const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
const paymentMethods = await stripe.paymentMethods.list({ ForbiddenError.from(permission).throwUnlessCan(
customer: req.organization.customerId, OrgPermissionActions.Edit,
type: 'card' OrgPermissionSubjects.Billing
}); );
if (paymentMethods.data.length < 1) {
// case: no payment method on file
session = await stripe.checkout.sessions.create({
customer: req.organization.customerId,
mode: 'setup',
payment_method_types: ['card'],
success_url: (await getSiteURL()) + '/dashboard',
cancel_url: (await getSiteURL()) + '/dashboard'
});
} else {
session = await stripe.billingPortal.sessions.create({
customer: req.organization.customerId,
return_url: (await getSiteURL()) + '/dashboard'
});
}
return res.status(200).send({ url: session.url }); const organization = await Organization.findById(organizationId);
} catch (err) { if (!organization) {
Sentry.setUser({ email: req.user.email }); throw OrganizationNotFoundError({
Sentry.captureException(err); message: "Failed to find organization"
return res.status(400).send({ });
message: 'Failed to redirect to organization billing portal' }
});
} 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 * Given a org id, return the projects each member of the org belongs to
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationMembersAndTheirWorkspaces = async ( export const getOrganizationMembersAndTheirWorkspaces = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId }
) => { } = await validateRequest(reqValidator.GetOrgMembersv1, req);
const { organizationId } = req.params;
const workspacesSet = ( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
await Workspace.find( ForbiddenError.from(permission).throwUnlessCan(
{ OrgPermissionActions.Read,
organization: organizationId OrgPermissionSubjects.Member
}, );
'_id' ForbiddenError.from(permission).throwUnlessCan(
) OrgPermissionActions.Read,
).map((w) => w._id.toString()); OrgPermissionSubjects.Workspace
);
const memberships = ( const workspacesSet = (
await Membership.find({ await Workspace.find(
workspace: { $in: workspacesSet } {
}).populate('workspace') organization: organizationId
); },
const userToWorkspaceIds: any = {}; "_id"
)
).map((w) => w._id.toString());
memberships.forEach(membership => { const memberships = await Membership.find({
const user = membership.user.toString(); workspace: { $in: workspacesSet }
if (userToWorkspaceIds[user]) { }).populate("workspace");
userToWorkspaceIds[user].push(membership.workspace); const userToWorkspaceIds: any = {};
} else {
userToWorkspaceIds[user] = [membership.workspace];
}
});
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 { Request, Response } from "express";
import * as Sentry from '@sentry/node';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp'); const jsrp = require("jsrp");
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from "bigint-conversion";
import { User, BackupPrivateKey, LoginSRPDetail } from '../../models'; import { BackupPrivateKey, LoginSRPDetail, User } from "../../models";
import { createToken } from '../../helpers/auth'; import { clearTokens, createToken, sendMail } from "../../helpers";
import { sendMail } from '../../helpers/nodemailer'; import { TokenService } from "../../services";
import { TokenService } from '../../services'; import { TOKEN_EMAIL_PASSWORD_RESET } from "../../variables";
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables'; import { BadRequestError } from "../../utils/errors";
import { BadRequestError } from '../../utils/errors'; import {
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config'; 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. * for account recovery.
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const emailPasswordReset = async (req: Request, res: Response) => { export const emailPasswordReset = async (req: Request, res: Response) => {
let email: string; const {
try { body: { email }
email = req.body.email; } = await validateRequest(reqValidator.EmailPasswordResetV1, req);
const user = await User.findOne({ email }).select('+publicKey'); const user = await User.findOne({ email }).select("+publicKey");
if (!user || !user?.publicKey) { if (!user || !user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ return res.status(200).send({
error: 'Failed to send email verification for password reset' message: "If an account exists with this email, a password reset link has been sent"
}); });
} }
const token = await TokenService.createToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email
});
await sendMail({
template: 'passwordReset.handlebars',
subjectLine: 'Infisical password reset',
recipients: [email],
substitutions: {
email,
token,
callback_url: (await getSiteURL()) + '/password-reset'
}
});
} 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({ const token = await TokenService.createToken({
message: `Sent an email for account recovery to ${email}` 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] * Password reset step 2: Verify email verification link sent to email [email]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const emailPasswordResetVerify = async (req: Request, res: Response) => { export const emailPasswordResetVerify = async (req: Request, res: Response) => {
let user, token; const {
try { body: { email, code }
const { email, code } = req.body; } = await validateRequest(reqValidator.EmailPasswordResetVerifyV1, req);
user = await User.findOne({ email }).select('+publicKey'); const user = await User.findOne({ email }).select("+publicKey");
if (!user || !user?.publicKey) { if (!user || !user?.publicKey) {
// case: user doesn't exist with email [email] or // case: user doesn't exist with email [email] or
// hasn't even completed their account // hasn't even completed their account
return res.status(403).send({ return res.status(403).send({
error: 'Failed email verification for password reset' error: "Failed email verification for password reset"
}); });
} }
await TokenService.validateToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email,
token: code
});
// generate temporary password-reset token await TokenService.validateToken({
token = createToken({ type: TOKEN_EMAIL_PASSWORD_RESET,
payload: { email,
userId: user._id.toString() token: code
}, });
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'
});
}
return res.status(200).send({ // generate temporary password-reset token
message: 'Successfully verified email', const token = createToken({
user, payload: {
token 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 * 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 * @returns
*/ */
export const srp1 = async (req: Request, res: Response) => { export const srp1 = async (req: Request, res: Response) => {
// return salt, serverPublicKey as part of first step of SRP protocol // return salt, serverPublicKey as part of first step of SRP protocol
try { const {
const { clientPublicKey } = req.body; body: { clientPublicKey }
const user = await User.findOne({ } = await validateRequest(reqValidator.Srp1V1, req);
email: req.user.email
}).select('+salt +verifier');
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(); if (!user) throw new Error("Failed to find user");
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, { const server = new jsrp.server();
email: req.user.email, server.init(
clientPublicKey: clientPublicKey, {
serverBInt: bigintConversion.bigintToBuf(server.bInt), salt: user.salt,
}, { upsert: true, returnNewDocument: false }) verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
return res.status(200).send({ await LoginSRPDetail.findOneAndReplace(
serverPublicKey, { email: req.user.email },
salt: user.salt {
}); email: req.user.email,
} clientPublicKey: clientPublicKey,
); serverBInt: bigintConversion.bigintToBuf(server.bInt)
} catch (err) { },
Sentry.setUser({ email: req.user.email }); { upsert: true, returnNewDocument: false }
Sentry.captureException(err); );
return res.status(400).send({
error: 'Failed to start change password process' return res.status(200).send({
}); serverPublicKey,
} salt: user.salt
});
}
);
}; };
/** /**
@@ -165,80 +156,91 @@ export const srp1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changePassword = async (req: Request, res: Response) => { export const changePassword = async (req: Request, res: Response) => {
try { const {
const { body: {
clientProof, clientProof,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt, salt,
verifier verifier
} = req.body; }
} = await validateRequest(reqValidator.ChangePasswordV1, req);
const user = await User.findOne({ const user = await User.findOne({
email: req.user.email email: req.user.email
}).select('+salt +verifier'); }).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) { if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) 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(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier, verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt b: loginSRPDetailFromDB.serverBInt
}, },
async () => { async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys // compare server and client shared keys
if (server.checkClientProof(clientProof)) { if (server.checkClientProof(clientProof)) {
// change password // change password
await User.findByIdAndUpdate( await User.findByIdAndUpdate(
req.user._id.toString(), req.user._id.toString(),
{ {
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
encryptedPrivateKey, encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag, tag: encryptedPrivateKeyTag,
salt, salt,
verifier verifier
}, },
{ {
new: true new: true
} }
); );
return res.status(200).send({ if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
message: 'Successfully changed password' await clearTokens(req.authData.tokenVersionId);
}); }
}
return res.status(400).send({ // clear httpOnly cookie
error: 'Failed to change password. Try again?'
}); res.cookie("jid", "", {
} httpOnly: true,
); path: "/",
} catch (err) { sameSite: "strict",
Sentry.setUser({ email: req.user.email }); secure: (await getHttpsEnabled()) as boolean
Sentry.captureException(err); });
return res.status(400).send({
error: 'Failed to change password. Try again?' return res.status(200).send({
}); message: "Successfully changed password"
} });
}
return res.status(400).send({
error: "Failed to change password. Try again?"
});
}
);
}; };
/** /**
@@ -248,141 +250,120 @@ export const changePassword = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createBackupPrivateKey = async (req: Request, res: Response) => { export const createBackupPrivateKey = async (req: Request, res: Response) => {
// create/change backup private key // create/change backup private key
// requires verifying [clientProof] as part of second step of SRP protocol // requires verifying [clientProof] as part of second step of SRP protocol
// as initiated in /srp1 // 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 { if (!user) throw new Error("Failed to find user");
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'); 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) { const server = new jsrp.server();
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) server.init(
} {
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
const server = new jsrp.server(); // compare server and client shared keys
server.init( if (server.checkClientProof(clientProof)) {
{ // create new or replace backup private key
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(
loginSRPDetailFromDB.clientPublicKey
);
// compare server and client shared keys const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
if (server.checkClientProof(clientProof)) { { user: req.user._id },
// create new or replace backup private key {
user: req.user._id,
encryptedPrivateKey,
iv,
tag,
salt,
verifier
},
{ upsert: true, new: true }
).select("+user, encryptedPrivateKey");
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate( // issue tokens
{ user: req.user._id }, return res.status(200).send({
{ message: "Successfully updated backup private key",
user: req.user._id, backupPrivateKey
encryptedPrivateKey, });
iv, }
tag,
salt,
verifier
},
{ upsert: true, new: true }
).select('+user, encryptedPrivateKey');
// issue tokens return res.status(400).send({
return res.status(200).send({ message: "Failed to update backup private key"
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 backup private key for user * Return backup private key for user
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getBackupPrivateKey = async (req: Request, res: Response) => { export const getBackupPrivateKey = async (req: Request, res: Response) => {
let backupPrivateKey; const backupPrivateKey = await BackupPrivateKey.findOne({
try { user: req.user._id
backupPrivateKey = await BackupPrivateKey.findOne({ }).select("+encryptedPrivateKey +iv +tag");
user: req.user._id
}).select('+encryptedPrivateKey +iv +tag');
if (!backupPrivateKey) throw new Error('Failed to find backup private key'); 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'
});
}
return res.status(200).send({ return res.status(200).send({
backupPrivateKey backupPrivateKey
}); });
} };
export const resetPassword = async (req: Request, res: Response) => { export const resetPassword = async (req: Request, res: Response) => {
try { const {
const { body: {
protectedKey, encryptedPrivateKey,
protectedKeyIV, protectedKeyTag,
protectedKeyTag, protectedKey,
encryptedPrivateKey, protectedKeyIV,
encryptedPrivateKeyIV, salt,
encryptedPrivateKeyTag, verifier,
salt, encryptedPrivateKeyIV,
verifier, encryptedPrivateKeyTag
} = req.body; }
} = await validateRequest(reqValidator.ResetPasswordV1, req);
await User.findByIdAndUpdate( await User.findByIdAndUpdate(
req.user._id.toString(), req.user._id.toString(),
{ {
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
encryptedPrivateKey, encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag, tag: encryptedPrivateKeyTag,
salt, salt,
verifier verifier
}, },
{ {
new: true 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'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully reset password' message: "Successfully reset password"
}); });
} };

View File

@@ -1,31 +1,30 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import { Types } from "mongoose";
import { Types } from 'mongoose'; import { Key } from "../../models";
import { Key, Secret } from '../../models';
import { import {
v1PushSecrets as push, pullSecrets as pull,
pullSecrets as pull, v1PushSecrets as push,
reformatPullSecrets reformatPullSecrets
} from '../../helpers/secret'; } from "../../helpers/secret";
import { pushKeys } from '../../helpers/key'; import { pushKeys } from "../../helpers/key";
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from "../../events";
import { EventService } from '../../services'; import { EventService } from "../../services";
import { TelemetryService } from '../../services'; import { TelemetryService } from "../../services";
interface PushSecret { interface PushSecret {
ciphertextKey: string; ciphertextKey: string;
ivKey: string; ivKey: string;
tagKey: string; tagKey: string;
hashKey: string; hashKey: string;
ciphertextValue: string; ciphertextValue: string;
ivValue: string; ivValue: string;
tagValue: string; tagValue: string;
hashValue: string; hashValue: string;
ciphertextComment: string; ciphertextComment: string;
ivComment: string; ivComment: string;
tagComment: string; tagComment: string;
hashComment: string; hashComment: string;
type: 'shared' | 'personal'; type: "shared" | "personal";
} }
/** /**
@@ -36,71 +35,59 @@ interface PushSecret {
* @returns * @returns
*/ */
export const pushSecrets = async (req: Request, res: Response) => { 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 { // validate environment
const postHogClient = await TelemetryService.getPostHogClient(); const workspaceEnvs = req.membership.workspace.environments;
let { secrets }: { secrets: PushSecret[] } = req.body; if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
const { keys, environment, channel } = req.body; throw new Error("Failed to validate environment");
const { workspaceId } = req.params; }
// validate environment // sanitize secrets
const workspaceEnvs = req.membership.workspace.environments; secrets = secrets.filter((s: PushSecret) => s.ciphertextKey !== "" && s.ciphertextValue !== "");
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
// sanitize secrets await push({
secrets = secrets.filter( userId: req.user._id,
(s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== '' workspaceId,
); environment,
secrets
});
await push({ await pushKeys({
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
environment, keys
secrets });
});
await pushKeys({ if (postHogClient) {
userId: req.user._id, postHogClient.capture({
workspaceId, event: "secrets pushed",
keys distinctId: req.user.email,
}); properties: {
numberOfSecrets: secrets.length,
environment,
if (postHogClient) { workspaceId,
postHogClient.capture({ channel: channel ? channel : "cli"
event: 'secrets pushed', }
distinctId: req.user.email, });
properties: { }
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment environment,
}) secretPath: "/"
}); })
});
} catch (err) { return res.status(200).send({
Sentry.setUser({ email: req.user.email }); message: "Successfully uploaded workspace secrets"
Sentry.captureException(err); });
return res.status(400).send({
message: 'Failed to upload 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 * @returns
*/ */
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
let secrets; let secrets;
let key;
try {
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment const postHogClient = await TelemetryService.getPostHogClient();
const workspaceEnvs = req.membership.workspace.environments; const environment: string = req.query.environment as string;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { const channel: string = req.query.channel as string;
throw new Error('Failed to validate environment'); const { workspaceId } = req.params;
}
secrets = await pull({ // validate environment
userId: req.user._id.toString(), const workspaceEnvs = req.membership.workspace.environments;
workspaceId, if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
environment, throw new Error("Failed to validate environment");
channel: channel ? channel : 'cli', }
ipAddress: req.ip
});
key = await Key.findOne({ secrets = await pull({
workspace: workspaceId, userId: req.user._id.toString(),
receiver: req.user._id workspaceId,
}) environment,
.sort({ createdAt: -1 }) channel: channel ? channel : "cli",
.populate('sender', '+publicKey'); ipAddress: req.realIP
});
if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets });
}
if (postHogClient) { const key = await Key.findOne({
// capture secrets pushed event in production workspace: workspaceId,
postHogClient.capture({ receiver: req.user._id
distinctId: req.user.email, })
event: 'secrets pulled', .sort({ createdAt: -1 })
properties: { .populate("sender", "+publicKey");
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'
});
}
return res.status(200).send({ if (channel !== "cli") {
secrets, secrets = reformatPullSecrets({ secrets });
key }
});
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 * @returns
*/ */
export const pullSecretsServiceToken = async (req: Request, res: Response) => { export const pullSecretsServiceToken = async (req: Request, res: Response) => {
let secrets; const postHogClient = await TelemetryService.getPostHogClient();
let key; const environment: string = req.query.environment as string;
try { const channel: string = req.query.channel as string;
const postHogClient = await TelemetryService.getPostHogClient(); const { workspaceId } = req.params;
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment // validate environment
const workspaceEnvs = req.membership.workspace.environments; const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment'); throw new Error("Failed to validate environment");
} }
secrets = await pull({ const secrets = await pull({
userId: req.serviceToken.user._id.toString(), userId: req.serviceToken.user._id.toString(),
workspaceId, workspaceId,
environment, environment,
channel: 'cli', channel: "cli",
ipAddress: req.ip ipAddress: req.realIP
}); });
key = { const key = {
encryptedKey: req.serviceToken.encryptedKey, encryptedKey: req.serviceToken.encryptedKey,
nonce: req.serviceToken.nonce, nonce: req.serviceToken.nonce,
sender: { sender: {
publicKey: req.serviceToken.publicKey publicKey: req.serviceToken.publicKey
}, },
receiver: req.serviceToken.user, receiver: req.serviceToken.user,
workspace: req.serviceToken.workspace workspace: req.serviceToken.workspace
}; };
if (postHogClient) { if (postHogClient) {
// capture secrets pulled event in production // capture secrets pulled event in production
postHogClient.capture({ postHogClient.capture({
distinctId: req.serviceToken.user.email, distinctId: req.serviceToken.user.email,
event: 'secrets pulled', event: "secrets pulled",
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : 'cli' 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'
});
}
return res.status(200).send({ return res.status(200).send({
secrets: reformatPullSecrets({ secrets }), secrets: reformatPullSecrets({ secrets }),
key 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 { ForbiddenError, subject } from "@casl/ability";
import { Secret } from '../../models'; import { Request, Response } from "express";
import Folder from '../../models/folder'; import { Types } from "mongoose";
import { BadRequestError } from '../../utils/errors'; import { EventType, FolderVersion } from "../../ee/models";
import { ROOT_FOLDER_PATH, getFolderPath, getParentPath, normalizePath, validateFolderName } from '../../utils/folder'; import { EEAuditLogService, EESecretService } from "../../ee/services";
import { ADMIN, MEMBER } from '../../variables'; import { isValidScope } from "../../helpers/secrets";
import { validateMembership } from '../../helpers/membership'; 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) => { 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)) { 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 folders = await Folder.findOne({
const parentFolder = await Folder.find({ environment: environment, workspace: workspaceId, id: parentFolderId }); workspace: workspaceId,
if (!parentFolder) { environment
throw BadRequestError({ message: "The parent folder doesn't exist" }) }).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) await Folder.findByIdAndUpdate(folders._id, folders);
if (completePath == ROOT_FOLDER_PATH) {
completePath = ""
}
const currentFolderPath = completePath + "/" + folderName // construct new path with current folder to be created const folderVersion = new FolderVersion({
const normalizedCurrentPath = normalizePath(currentFolderPath)
const normalizedParentPath = getParentPath(normalizedCurrentPath)
const existingFolder = await Folder.findOne({
name: folderName,
workspace: workspaceId, workspace: workspaceId,
environment: environment, environment,
parent: parentFolderId, nodes: parentFolder
path: normalizedCurrentPath });
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolderId
}); });
if (existingFolder) { const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
return res.json(existingFolder)
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({ const folders = await Folder.findOne({ workspace: workspaceId, environment });
name: folderName, 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, workspace: workspaceId,
environment: environment, environment,
parent: parentFolderId, nodes: parentFolder
path: normalizedCurrentPath, });
parentPath: normalizedParentPath 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) => { 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); #swagger.parameters['folderId'] = {
if (!folder) { "description": "ID of the folder to be deleted",
throw BadRequestError({ message: "The folder doesn't exist" }) "required": true,
} "type": "string",
"in": "path"
// 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);
} }
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() const delOp = deleteFolderById(folders.nodes, folderId);
} if (!delOp) {
throw BadRequestError({ message: "The folder doesn't exist" });
// 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" })
} }
// check that user is a member of the workspace const { deletedNode: delFolder, parent: parentFolder } = delOp;
await validateMembership({ const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
userId: req.user._id.toString(),
workspaceId: folder.workspace as any, if (req.authData.authPayload instanceof ServiceTokenData) {
acceptedRoles: [ADMIN, MEMBER] 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 { Request, Response } from "express";
import { ServiceToken } from '../../models'; import { ServiceToken } from "../../models";
import { createToken } from '../../helpers/auth'; import { createToken } from "../../helpers/auth";
import { getJwtServiceSecret } from '../../config'; import { getJwtServiceSecret } from "../../config";
/** /**
* Return service token on request * Return service token on request
@@ -11,7 +11,7 @@ import { getJwtServiceSecret } from '../../config';
*/ */
export const getServiceToken = async (req: Request, res: Response) => { export const getServiceToken = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
serviceToken: req.serviceToken serviceToken: req.serviceToken,
}); });
}; };
@@ -31,13 +31,13 @@ export const createServiceToken = async (req: Request, res: Response) => {
expiresIn, expiresIn,
publicKey, publicKey,
encryptedKey, encryptedKey,
nonce nonce,
} = req.body; } = req.body;
// validate environment // validate environment
const workspaceEnvs = req.membership.workspace.environments; const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { 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 // compute access token expiration date
@@ -52,24 +52,24 @@ export const createServiceToken = async (req: Request, res: Response) => {
expiresAt, expiresAt,
publicKey, publicKey,
encryptedKey, encryptedKey,
nonce nonce,
}).save(); }).save();
token = createToken({ token = createToken({
payload: { payload: {
serviceTokenId: serviceToken._id.toString(), serviceTokenId: serviceToken._id.toString(),
workspaceId workspaceId,
}, },
expiresIn: expiresIn, expiresIn: expiresIn,
secret: await getJwtServiceSecret() secret: await getJwtServiceSecret(),
}); });
} catch (err) { } catch (err) {
return res.status(400).send({ return res.status(400).send({
message: 'Failed to create service token' message: "Failed to create service token",
}); });
} }
return res.status(200).send({ return res.status(200).send({
token token,
}); });
}; };

View File

@@ -1,13 +1,17 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import { AuthMethod, User } from "../../models";
import { User } from '../../models'; import { checkEmailVerification, sendEmailVerification } from "../../helpers/signup";
import { createToken } from "../../helpers/auth";
import { BadRequestError } from "../../utils/errors";
import { import {
sendEmailVerification, getInviteOnlySignup,
checkEmailVerification, getJwtSignupLifetime,
} from '../../helpers/signup'; getJwtSignupSecret,
import { createToken } from '../../helpers/auth'; getSmtpConfigured
import { BadRequestError } from '../../utils/errors'; } from "../../config";
import { 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 * Signup step 1: Initialize account for user under email [email] and send a verification code
@@ -17,32 +21,28 @@ import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpC
* @returns * @returns
*/ */
export const beginEmailSignup = async (req: Request, res: Response) => { export const beginEmailSignup = async (req: Request, res: Response) => {
let email: string; const {
try { body: { email }
email = req.body.email; } = await validateRequest(reqValidator.BeginEmailSignUpV1, req);
const user = await User.findOne({ email }).select('+publicKey'); // validate that email is not disposable
if (user && user?.publicKey) { validateUserEmail(email);
// case: user has already completed account
return res.status(403).send({ const user = await User.findOne({ email }).select("+publicKey");
error: 'Failed to send email verification code for complete account' if (user && user?.publicKey) {
}); // case: user has already completed account
}
// send send verification email return res.status(403).send({
await sendEmailVerification({ email }); error: "Failed to send email verification code for complete account"
} catch (err) { });
Sentry.setUser(null); }
Sentry.captureException(err);
return res.status(400).send({ // send send verification email
error: 'Failed to send email verification code' 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}`
});
}; };
/** /**
@@ -53,60 +53,57 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const verifyEmailSignup = async (req: Request, res: Response) => { export const verifyEmailSignup = async (req: Request, res: Response) => {
let user, token; let user;
try { const {
const { email, code } = req.body; body: { email, code }
} = await validateRequest(reqValidator.VerifyEmailSignUpV1, req);
// initialize user account // initialize user account
user = await User.findOne({ email }).select('+publicKey'); user = await User.findOne({ email }).select("+publicKey");
if (user && user?.publicKey) { if (user && user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: 'Failed email verification for complete user' error: "Failed email verification for complete user"
}); });
} }
if (await getInviteOnlySignup()) { 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 // 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({}) const userCount = await User.countDocuments({});
if (userCount != 0) { if (userCount != 0) {
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." }) throw BadRequestError({
} message: "New user sign ups are not allowed at this time. You must be invited to sign up."
} });
}
}
// verify email // verify email
if (await getSmtpConfigured()) { if (await getSmtpConfigured()) {
await checkEmailVerification({ await checkEmailVerification({
email, email,
code code
}); });
} }
if (!user) { if (!user) {
user = await new User({ user = await new User({
email email,
}).save(); authMethods: [AuthMethod.EMAIL]
} }).save();
}
// generate temporary signup token // generate temporary signup token
token = createToken({ const token = createToken({
payload: { payload: {
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: await getJwtSignupLifetime(), expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret() secret: await getJwtSignupSecret()
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed email verification'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfuly verified email', message: "Successfuly verified email",
user, user,
token 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 { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import { validateRequest } from "../../helpers/validation";
import { UserAction } from '../../models'; import { UserAction } from "../../models";
import * as reqValidator from "../../validation/action";
/** /**
* Add user action [action] * Add user action [action]
@@ -9,35 +10,27 @@ import { UserAction } from '../../models';
* @returns * @returns
*/ */
export const addUserAction = async (req: Request, res: Response) => { 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; const userAction = await UserAction.findOneAndUpdate(
try { {
const { action } = req.body; user: req.user._id,
action
},
{ user: req.user._id, action },
{
new: true,
upsert: true
}
);
userAction = await UserAction.findOneAndUpdate( return res.status(200).send({
{ message: "Successfully recorded user action",
user: req.user._id, userAction
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
});
}; };
/** /**
@@ -47,24 +40,17 @@ export const addUserAction = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getUserAction = async (req: Request, res: Response) => { export const getUserAction = async (req: Request, res: Response) => {
// get user action [action] for user with id [req.user._id] // get user action [action] for user with id [req.user._id]
let userAction; const {
try { query: { action }
const action: string = req.query.action as string; } = await validateRequest(reqValidator.GetUserActionV1, req);
userAction = await UserAction.findOne({ const userAction = await UserAction.findOne({
user: req.user._id, user: req.user._id,
action action
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get user action'
});
}
return res.status(200).send({ return res.status(200).send({
userAction userAction
}); });
}; };

View File

@@ -1,4 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
/** /**
* Return user on request * Return user on request
@@ -8,6 +8,6 @@ import { Request, Response } from 'express';
*/ */
export const getUser = async (req: Request, res: Response) => { export const getUser = async (req: Request, res: Response) => {
return res.status(200).send({ 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 { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import { import {
Workspace, IUser,
Membership,
MembershipOrg,
Integration, Integration,
IntegrationAuth, IntegrationAuth,
IUser, Membership,
Organization,
ServiceToken, ServiceToken,
ServiceTokenData, Workspace
} from "../../models"; } from "../../models";
import { import { createWorkspace as create, deleteWorkspace as deleteWork } from "../../helpers/workspace";
createWorkspace as create, import { EELicenseService } from "../../ee/services";
deleteWorkspace as deleteWork,
} from "../../helpers/workspace";
import { addMemberships } from "../../helpers/membership"; import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables"; 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] * Return public keys of members of workspace with id [workspaceId]
@@ -24,30 +35,29 @@ import { ADMIN } from "../../variables";
* @returns * @returns
*/ */
export const getWorkspacePublicKeys = async (req: Request, res: Response) => { export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
let publicKeys; const {
try { params: { workspaceId }
const { workspaceId } = req.params; } = await validateRequest(reqValidator.GetWorkspacePublicKeysV1, req);
publicKeys = ( const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
await Membership.find({ ForbiddenError.from(permission).throwUnlessCan(
workspace: workspaceId, ProjectPermissionActions.Read,
}).populate<{ user: IUser }>("user", "publicKey") ProjectPermissionSub.Member
).map((member) => { );
return {
publicKey: member.user.publicKey, const publicKeys = (
userId: member.user._id, await Membership.find({
}; workspace: workspaceId
}); }).populate<{ user: IUser }>("user", "publicKey")
} catch (err) { ).map((member) => {
Sentry.setUser({ email: req.user.email }); return {
Sentry.captureException(err); publicKey: member.user.publicKey,
return res.status(400).send({ userId: member.user._id
message: "Failed to get workspace member public keys", };
}); });
}
return res.status(200).send({ return res.status(200).send({
publicKeys, publicKeys
}); });
}; };
@@ -58,23 +68,22 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceMemberships = async (req: Request, res: Response) => { export const getWorkspaceMemberships = async (req: Request, res: Response) => {
let users; const {
try { params: { workspaceId }
const { workspaceId } = req.params; } = await validateRequest(reqValidator.GetWorkspaceMembershipsV1, req);
users = await Membership.find({ const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
workspace: workspaceId, ForbiddenError.from(permission).throwUnlessCan(
}).populate("user", "+publicKey"); ProjectPermissionActions.Read,
} catch (err) { ProjectPermissionSub.Member
Sentry.setUser({ email: req.user.email }); );
Sentry.captureException(err);
return res.status(400).send({ const users = await Membership.find({
message: "Failed to get workspace members", workspace: workspaceId
}); }).populate("user", "+publicKey");
}
return res.status(200).send({ return res.status(200).send({
users, users
}); });
}; };
@@ -85,23 +94,14 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaces = async (req: Request, res: Response) => { export const getWorkspaces = async (req: Request, res: Response) => {
let workspaces; const workspaces = (
try { await Membership.find({
workspaces = ( user: req.user._id
await Membership.find({ }).populate("workspace")
user: req.user._id, ).map((m) => m.workspace);
}).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",
});
}
return res.status(200).send({ return res.status(200).send({
workspaces, workspaces
}); });
}; };
@@ -112,23 +112,16 @@ export const getWorkspaces = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspace = async (req: Request, res: Response) => { export const getWorkspace = async (req: Request, res: Response) => {
let workspace; const {
try { params: { workspaceId }
const { workspaceId } = req.params; } = await validateRequest(reqValidator.GetWorkspaceV1, req);
workspace = await Workspace.findOne({ const workspace = await Workspace.findOne({
_id: workspaceId, _id: workspaceId
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace",
});
}
return res.status(200).send({ return res.status(200).send({
workspace, workspace
}); });
}; };
@@ -140,45 +133,54 @@ export const getWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createWorkspace = async (req: Request, res: Response) => { export const createWorkspace = async (req: Request, res: Response) => {
let workspace; const {
try { body: { organizationId, workspaceName }
const { workspaceName, organizationId } = req.body; } = await validateRequest(reqValidator.CreateWorkspaceV1, req);
// validate organization membership const organization = await Organization.findById(organizationId);
const membershipOrg = await MembershipOrg.findOne({ if (!organization) {
user: req.user._id, throw OrganizationNotFoundError({
organization: organizationId, message: "Failed to find organization"
});
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 { 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({ return res.status(200).send({
workspace, workspace
}); });
}; };
@@ -189,23 +191,23 @@ export const createWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteWorkspace = async (req: Request, res: Response) => { export const deleteWorkspace = async (req: Request, res: Response) => {
try { const {
const { workspaceId } = req.params; params: { workspaceId }
} = await validateRequest(reqValidator.DeleteWorkspaceV1, req);
// delete workspace const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
await deleteWork({ ForbiddenError.from(permission).throwUnlessCan(
id: workspaceId, ProjectPermissionActions.Delete,
}); ProjectPermissionSub.Workspace
} catch (err) { );
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); // delete workspace
return res.status(400).send({ await deleteWork({
message: "Failed to delete workspace", id: workspaceId
}); });
}
return res.status(200).send({ 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 * @returns
*/ */
export const changeWorkspaceName = async (req: Request, res: Response) => { export const changeWorkspaceName = async (req: Request, res: Response) => {
let workspace; const {
try { params: { workspaceId },
const { workspaceId } = req.params; body: { name }
const { name } = req.body; } = await validateRequest(reqValidator.ChangeWorkspaceNameV1, req);
workspace = await Workspace.findOneAndUpdate( const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
{ ForbiddenError.from(permission).throwUnlessCan(
_id: workspaceId, ProjectPermissionActions.Edit,
}, ProjectPermissionSub.Workspace
{ );
name,
}, const workspace = await Workspace.findOneAndUpdate(
{ {
new: true, _id: workspaceId
} },
); {
} catch (err) { name
Sentry.setUser({ email: req.user.email }); },
Sentry.captureException(err); {
return res.status(400).send({ new: true
message: "Failed to change workspace name", }
}); );
}
return res.status(200).send({ return res.status(200).send({
message: "Successfully changed workspace name", message: "Successfully changed workspace name",
workspace, workspace
}); });
}; };
@@ -253,23 +254,21 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceIntegrations = async (req: Request, res: Response) => { export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
let integrations; const {
try { params: { workspaceId }
const { workspaceId } = req.params; } = 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({ const integrations = await Integration.find({
workspace: workspaceId, workspace: workspaceId
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace integrations",
});
}
return res.status(200).send({ return res.status(200).send({
integrations, integrations
}); });
}; };
@@ -279,27 +278,23 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceIntegrationAuthorizations = async ( export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { workspaceId }
) => { } = await validateRequest(reqValidator.GetWorkspaceIntegrationAuthorizationsV1, req);
let authorizations;
try {
const { workspaceId } = req.params;
authorizations = await IntegrationAuth.find({ const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
workspace: workspaceId, ForbiddenError.from(permission).throwUnlessCan(
}); ProjectPermissionActions.Read,
} catch (err) { ProjectPermissionSub.Integrations
Sentry.setUser({ email: req.user.email }); );
Sentry.captureException(err);
return res.status(400).send({ const authorizations = await IntegrationAuth.find({
message: "Failed to get workspace integration authorizations", workspace: workspaceId
}); });
}
return res.status(200).send({ return res.status(200).send({
authorizations, authorizations
}); });
}; };
@@ -309,27 +304,24 @@ export const getWorkspaceIntegrationAuthorizations = async (
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceServiceTokens = async ( export const getWorkspaceServiceTokens = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { workspaceId }
) => { } = await validateRequest(reqValidator.GetWorkspaceServiceTokensV1, req);
let serviceTokens;
try { const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { workspaceId } = req.params; ForbiddenError.from(permission).throwUnlessCan(
// ?? FIX. ProjectPermissionActions.Read,
serviceTokens = await ServiceToken.find({ ProjectPermissionSub.ServiceTokens
user: req.user._id, );
workspace: workspaceId,
}); // ?? FIX.
} catch (err) { const serviceTokens = await ServiceToken.find({
Sentry.setUser({ email: req.user.email }); user: req.user._id,
Sentry.captureException(err); workspace: workspaceId
return res.status(400).send({ });
message: "Failed to get workspace service tokens",
});
}
return res.status(200).send({ 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 */ /* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from 'express'; import { Request, Response } from "express";
import jwt from 'jsonwebtoken'; import jwt from "jsonwebtoken";
import * as Sentry from '@sentry/node'; import * as bigintConversion from "bigint-conversion";
import * as bigintConversion from 'bigint-conversion'; const jsrp = require("jsrp");
const jsrp = require('jsrp'); import { LoginSRPDetail, User } from "../../models";
import { User, LoginSRPDetail } from '../../models'; import { createToken, issueAuthTokens } from "../../helpers/auth";
import { issueAuthTokens, createToken } from '../../helpers/auth'; import { checkUserDevice } from "../../helpers/user";
import { checkUserDevice } from '../../helpers/user'; import { sendMail } from "../../helpers/nodemailer";
import { sendMail } from '../../helpers/nodemailer'; import { TokenService } from "../../services";
import { TokenService } from '../../services'; import { EELogService } from "../../ee/services";
import { EELogService } from '../../ee/services'; import { BadRequestError, InternalServerError } from "../../utils/errors";
import { BadRequestError, InternalServerError } from '../../utils/errors'; import { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables";
import { import { getUserAgentType } from "../../utils/posthog"; // TODO: move this
TOKEN_EMAIL_MFA, import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config";
ACTION_LOGIN import { validateRequest } from "../../helpers/validation";
} from '../../variables'; import * as reqValidator from "../../validation/auth";
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getJwtMfaLifetime,
getJwtMfaSecret,
getHttpsEnabled
} from '../../config';
declare module 'jsonwebtoken' { declare module "jsonwebtoken" {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string; userId: string;
} }
@@ -35,47 +29,40 @@ declare module 'jsonwebtoken' {
* @returns * @returns
*/ */
export const login1 = async (req: Request, res: Response) => { 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({ const user = await User.findOne({
email email
}).select('+salt +verifier'); }).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(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier verifier: user.verifier
}, },
async () => { async () => {
// generate server-side public key // generate server-side public key
const serverPublicKey = server.getPublicKey(); const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: email }, { await LoginSRPDetail.findOneAndReplace(
{ email: email },
{
email: email, email: email,
clientPublicKey: clientPublicKey, clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt), serverBInt: bigintConversion.bigintToBuf(server.bInt)
}, { upsert: true, returnNewDocument: false }); },
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({ return res.status(200).send({
serverPublicKey, serverPublicKey,
salt: user.salt salt: user.salt
}); });
} }
); );
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
}; };
/** /**
@@ -86,195 +73,185 @@ export const login1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const login2 = async (req: Request, res: Response) => { export const login2 = async (req: Request, res: Response) => {
try { if (!req.headers["user-agent"])
throw InternalServerError({ message: "User-Agent header is required" });
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; if (!user) throw new Error("Failed to find user");
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'); 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) { const server = new jsrp.server();
return BadRequestError(Error("Failed to find login details for SRP")) server.init(
} {
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
const server = new jsrp.server(); // compare server and client shared keys
server.init( if (server.checkClientProof(clientProof)) {
{ if (user.isMfaEnabled) {
salt: user.salt, // case: user has MFA enabled
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys // generate temporary MFA token
if (server.checkClientProof(clientProof)) { const token = createToken({
payload: {
if (user.isMfaEnabled) { userId: user._id.toString()
// case: user has MFA enabled },
expiresIn: await getJwtMfaLifetime(),
// generate temporary MFA token secret: await getJwtMfaSecret()
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'] ?? ''
}); });
// issue tokens const code = await TokenService.createToken({
const tokens = await issueAuthTokens({ userId: user._id.toString() }); type: TOKEN_EMAIL_MFA,
email
// 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 // send MFA code [code] to [email]
// return (access) token in response await sendMail({
template: "emailMfa.handlebars",
interface ResponseData { subjectLine: "Infisical MFA code",
mfaEnabled: boolean; recipients: [email],
encryptionVersion: any; substitutions: {
protectedKey?: string; code
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({ return res.status(200).send({
userId: user._id, mfaEnabled: true,
actions: [loginAction], token
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
}); });
return res.status(200).send(response);
} }
return res.status(400).send({ await checkUserDevice({
message: 'Failed to authenticate. Try again?' 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) { return res.status(400).send({
Sentry.setUser(null); message: "Failed to authenticate. Try again?"
Sentry.captureException(err); });
return res.status(400).send({ }
message: 'Failed to authenticate. Try again?' );
});
}
}; };
/** /**
* Send MFA token to email [email] * Send MFA token to email [email]
* @param req * @param req
* @param res * @param res
*/ */
export const sendMfaToken = async (req: Request, res: Response) => { export const sendMfaToken = async (req: Request, res: Response) => {
try { const {
const { email } = req.body; body: { email }
} = await validateRequest(reqValidator.SendMfaTokenV2, req);
const code = await TokenService.createToken({ const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA, type: TOKEN_EMAIL_MFA,
email email
}); });
// send MFA code [code] to [email] // send MFA code [code] to [email]
await sendMail({ await sendMail({
template: 'emailMfa.handlebars', template: "emailMfa.handlebars",
subjectLine: 'Infisical MFA code', subjectLine: "Infisical MFA code",
recipients: [email], recipients: [email],
substitutions: { substitutions: {
code code
} }
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send MFA code'
});
}
return res.status(200).send({ 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 * Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
* MFA token [mfaToken] is valid * MFA token [mfaToken] is valid
* @param req * @param req
* @param res * @param res
*/ */
export const verifyMfaToken = async (req: Request, res: Response) => { 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({ await TokenService.validateToken({
type: TOKEN_EMAIL_MFA, type: TOKEN_EMAIL_MFA,
@@ -284,24 +261,32 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
const user = await User.findOne({ const user = await User.findOne({
email 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({ await checkUserDevice({
user, user,
ip: req.ip, ip: req.realIP,
userAgent: req.headers['user-agent'] ?? '' userAgent: req.headers["user-agent"] ?? ""
}); });
// issue tokens // 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 // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie("jid", tokens.refreshToken, {
httpOnly: true, httpOnly: true,
path: '/', path: "/",
sameSite: 'strict', sameSite: "strict",
secure: await getHttpsEnabled() secure: await getHttpsEnabled()
}); });
@@ -336,7 +321,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
encryptedPrivateKey: user.encryptedPrivateKey as string, encryptedPrivateKey: user.encryptedPrivateKey as string,
iv: user.iv as string, iv: user.iv as string,
tag: user.tag as string tag: user.tag as string
} };
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) { if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
resObj.protectedKey = user.protectedKey; resObj.protectedKey = user.protectedKey;
@@ -349,12 +334,13 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
userId: user._id userId: user._id
}); });
loginAction && await EELogService.createLog({ loginAction &&
userId: user._id, (await EELogService.createLog({
actions: [loginAction], userId: user._id,
channel: getChannelFromUserAgent(req.headers['user-agent']), actions: [loginAction],
ipAddress: req.ip channel: getUserAgentType(req.headers["user-agent"]),
}); ipAddress: req.realIP
}));
return res.status(200).send(resObj); return res.status(200).send(resObj);
} };

View File

@@ -1,61 +1,236 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import { Types } from "mongoose";
import { import {
Integration,
Membership,
Secret, Secret,
ServiceToken, ServiceToken,
Workspace,
Integration,
ServiceTokenData, ServiceTokenData,
Membership, Workspace
} from '../../models'; } from "../../models";
import { SecretVersion } from '../../ee/models'; import { EventType, SecretVersion } from "../../ee/models";
import { BadRequestError } from '../../utils/errors'; import { EEAuditLogService, EELicenseService } from "../../ee/services";
import _ from 'lodash'; import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables'; 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 req
* @param res * @param res
* @returns * @returns
*/ */
export const createWorkspaceEnvironment = async ( export const createWorkspaceEnvironment = async (req: Request, res: Response) => {
req: Request, /*
res: Response #swagger.summary = 'Create environment'
) => { #swagger.description = 'Create environment'
const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body; #swagger.security = [{
try { "apiKeyAuth": []
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');
}
workspace?.environments.push({ #swagger.parameters['workspaceId'] = {
name: environmentName, "description": "ID of project",
slug: environmentSlug.toLowerCase(), "required": true,
}); "type": "string"
await workspace.save(); }
} catch (err) {
Sentry.setUser({ email: req.user.email }); /*
Sentry.captureException(err); #swagger.summary = 'Create environment'
return res.status(400).send({ #swagger.description = 'Create environment'
message: 'Failed to create new workspace 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({ return res.status(200).send({
message: 'Successfully created new environment', message: "Successfully created new environment",
workspace: workspaceId, workspace: workspaceId,
environment: { environment: {
name: environmentName, 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 * @param res
* @returns * @returns
*/ */
export const renameWorkspaceEnvironment = async ( export const renameWorkspaceEnvironment = async (req: Request, res: Response) => {
req: Request, /*
res: Response #swagger.summary = 'Rename workspace environment'
) => { #swagger.description = 'Rename a specific environment within a workspace'
const { workspaceId } = req.params;
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
try {
// user should pass both new slug and env name
if (!environmentSlug || !environmentName) {
throw new Error('Invalid environment given.');
}
// atomic update the env to avoid conflict #swagger.parameters['workspaceId'] = {
const workspace = await Workspace.findById(workspaceId).exec(); "description": "ID of the workspace",
if (!workspace) { "required": true,
throw new Error('Failed to create workspace environment'); "type": "string",
} "in": "path"
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.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({ return res.status(200).send({
message: 'Successfully update environment', message: "Successfully update environment",
workspace: workspaceId, workspace: workspaceId,
environment: { environment: {
name: environmentName, name: environmentName,
slug: environmentSlug, slug: environmentSlug
}, }
}); });
}; };
@@ -157,106 +417,238 @@ export const renameWorkspaceEnvironment = async (
* @param res * @param res
* @returns * @returns
*/ */
export const deleteWorkspaceEnvironment = async ( export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => {
req: Request, /*
res: Response #swagger.summary = 'Delete workspace environment'
) => { #swagger.description = 'Delete a specific environment from a workspace'
const { workspaceId } = req.params;
const { environmentSlug } = req.body; #swagger.security = [{
try { "apiKeyAuth": []
// atomic update the env to avoid conflict }]
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) { #swagger.parameters['workspaceId'] = {
throw new Error('Failed to create workspace environment'); "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( #swagger.responses[200] = {
({ slug }) => slug === environmentSlug content: {
); "application/json": {
if (envIndex === -1) { "schema": {
throw new Error('Invalid environment given'); "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); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
await workspace.save(); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Environments
);
// clean up // atomic update the env to avoid conflict
await Secret.deleteMany({ const workspace = await Workspace.findById(workspaceId).exec();
workspace: workspaceId, if (!workspace) {
environment: environmentSlug, throw new Error("Failed to create workspace environment");
});
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',
});
} }
return res.status(200).send({ const envIndex = workspace?.environments.findIndex(({ slug }) => slug === environmentSlug);
message: 'Successfully deleted environment', 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, 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 ( #swagger.parameters['workspaceId'] = {
req: Request, "description": "ID of the workspace",
res: Response "required": true,
) => { "type": "string",
const { workspaceId } = req.params; "in": "path"
const workspacesUserIsMemberOf = await Membership.findOne({ }
workspace: workspaceId,
user: req.user
})
if (!workspacesUserIsMemberOf) { #swagger.responses[200] = {
throw BadRequestError() 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 { membership: workspacesUserIsMemberOf } = await getUserProjectPermissions(
const deniedPermission = workspacesUserIsMemberOf.deniedPermissions 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) { if (!relatedWorkspace) {
throw BadRequestError() throw BadRequestError();
} }
relatedWorkspace.environments.forEach(environment => { relatedWorkspace.environments.forEach((environment) => {
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS }) const isReadBlocked = _.some(deniedPermission, {
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS }) environmentSlug: environment.slug,
ability: PERMISSION_READ_SECRETS
});
const isWriteBlocked = _.some(deniedPermission, {
environmentSlug: environment.slug,
ability: PERMISSION_WRITE_SECRETS
});
if (isReadBlocked && isWriteBlocked) { if (isReadBlocked && isWriteBlocked) {
return return;
} else { } else {
accessibleEnvironments.push({ accessibleEnvironments.push({
name: environment.name, name: environment.name,
slug: environment.slug, slug: environment.slug,
isWriteDenied: isWriteBlocked, isWriteDenied: isWriteBlocked,
isReadDenied: isReadBlocked isReadDenied: isReadBlocked
}) });
} }
}) });
res.json({ accessibleEnvironments }) res.json({ accessibleEnvironments });
}; };

View File

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

View File

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

View File

@@ -1,25 +1,36 @@
import to from "await-to-js";
import { Request, Response } from "express"; import { Request, Response } from "express";
import mongoose, { Types } from "mongoose"; import mongoose, { Types } from "mongoose";
import Secret, { ISecret } from "../../models/secret"; import {
import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCreate, SanitizedSecretModify } from "../../types/secret"; CreateSecretRequestBody,
ModifySecretRequestBody,
SanitizedSecretForCreate,
SanitizedSecretModify
} from "../../types/secret";
const { ValidationError } = mongoose.Error; const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import {
import { AnyBulkWriteOperation } from 'mongodb'; ValidationError as RouteValidationError,
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, SECRET_PERSONAL, SECRET_SHARED } from "../../variables"; UnauthorizedRequestError
import { TelemetryService } from '../../services'; } from "../../utils/errors";
import { User } from "../../models"; import { AnyBulkWriteOperation } from "mongodb";
import { AccountNotFoundError } from '../../utils/errors'; 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] * Create secret for workspace with id [workspaceId] and environment [environment]
* @param req * @param req
* @param res * @param res
*/ */
export const createSecret = async (req: Request, res: Response) => { export const createSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const secretToCreate: CreateSecretRequestBody = req.body.secret; const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environment } = req.params const { workspaceId, environment } = req.params;
const sanitizedSecret: SanitizedSecretForCreate = { const sanitizedSecret: SanitizedSecretForCreate = {
secretKeyCiphertext: secretToCreate.secretKeyCiphertext, secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
secretKeyIV: secretToCreate.secretKeyIV, secretKeyIV: secretToCreate.secretKeyIV,
@@ -39,45 +50,41 @@ export const createSecret = async (req: Request, res: Response) => {
user: new Types.ObjectId(req.user._id), user: new Types.ObjectId(req.user._id),
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8
} };
const secret = await new Secret(sanitizedSecret).save();
const [error, secret] = await to(Secret.create(sanitizedSecret).then())
if (error instanceof ValidationError) {
throw RouteValidationError({ message: error.message, stack: error.stack })
}
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets added', event: "secrets added",
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId, workspaceId,
environment, environment,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.['user-agent'] userAgent: req.headers?.["user-agent"]
} }
}); });
} }
res.status(200).send({ res.status(200).send({
secret secret
}) });
} };
/** /**
* Create many secrets for workspace wiht id [workspaceId] and environment [environment] * Create many secrets for workspace with id [workspaceId] and environment [environment]
* @param req * @param req
* @param res * @param res
*/ */
export const createSecrets = async (req: Request, res: Response) => { export const createSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environment } = req.params const { workspaceId, environment } = req.params;
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [];
secretsToCreate.forEach(rawSecret => { secretsToCreate.forEach((rawSecret) => {
const safeUpdateFields: SanitizedSecretForCreate = { const safeUpdateFields: SanitizedSecretForCreate = {
secretKeyCiphertext: rawSecret.secretKeyCiphertext, secretKeyCiphertext: rawSecret.secretKeyCiphertext,
secretKeyIV: rawSecret.secretKeyIV, secretKeyIV: rawSecret.secretKeyIV,
@@ -97,140 +104,129 @@ export const createSecrets = async (req: Request, res: Response) => {
user: new Types.ObjectId(req.user._id), user: new Types.ObjectId(req.user._id),
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8
} };
sanitizedSecretesToCreate.push(safeUpdateFields) sanitizedSecretesToCreate.push(safeUpdateFields);
}) });
const [bulkCreateError, secrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then()) const secrets = await Secret.insertMany(sanitizedSecretesToCreate);
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 })
}
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets added', event: "secrets added",
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: (secretsToCreate ?? []).length, numberOfSecrets: (secretsToCreate ?? []).length,
workspaceId, workspaceId,
environment, environment,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.['user-agent'] userAgent: req.headers?.["user-agent"]
} }
}); });
} }
res.status(200).send({ res.status(200).send({
secrets secrets
}) });
} };
/** /**
* Delete secrets in workspace with id [workspaceId] and environment [environment] * Delete secrets in workspace with id [workspaceId] and environment [environment]
* @param req * @param req
* @param res * @param res
*/ */
export const deleteSecrets = async (req: Request, res: Response) => { export const deleteSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params;
const secretIdsToDelete: string[] = req.body.secretIds const secretIdsToDelete: string[] = req.body.secretIds;
const [secretIdsUserCanDeleteError, secretIdsUserCanDelete] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) const secretIdsUserCanDelete = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
if (secretIdsUserCanDeleteError) {
throw InternalServerError({ message: `Unable to fetch secrets you own: [error=${secretIdsUserCanDeleteError.message}]` })
}
const secretsUserCanDeleteSet: Set<string> = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString())); const secretsUserCanDeleteSet: Set<string> = new Set(
const deleteOperationsToPerform: AnyBulkWriteOperation<ISecret>[] = [] secretIdsUserCanDelete.map((objectId) => objectId._id.toString())
);
const deleteOperationsToPerform: AnyBulkWriteOperation<ISecret>[] = [];
let numSecretsDeleted = 0; let numSecretsDeleted = 0;
secretIdsToDelete.forEach(secretIdToDelete => { secretIdsToDelete.forEach((secretIdToDelete) => {
if (secretsUserCanDeleteSet.has(secretIdToDelete)) { if (secretsUserCanDeleteSet.has(secretIdToDelete)) {
const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } } const deleteOperation = {
deleteOperationsToPerform.push(deleteOperation) deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } }
};
deleteOperationsToPerform.push(deleteOperation);
numSecretsDeleted++; numSecretsDeleted++;
} else { } 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()) await Secret.bulkWrite(deleteOperationsToPerform);
if (bulkDeleteError) {
if (bulkDeleteError instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkDeleteError.stack })
}
throw InternalServerError()
}
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets deleted', event: "secrets deleted",
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: numSecretsDeleted, numberOfSecrets: numSecretsDeleted,
environment: environmentName, environment: environmentName,
workspaceId, workspaceId,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.['user-agent'] userAgent: req.headers?.["user-agent"]
} }
}); });
} }
res.status(200).send() res.status(200).send();
} };
/** /**
* Delete secret with id [secretId] * Delete secret with id [secretId]
* @param req * @param req
* @param res * @param res
*/ */
export const deleteSecret = async (req: Request, res: Response) => { export const deleteSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
await Secret.findByIdAndDelete(req._secret._id) await Secret.findByIdAndDelete(req._secret._id);
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets deleted', event: "secrets deleted",
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId: req._secret.workspace.toString(), workspaceId: req._secret.workspace.toString(),
environment: req._secret.environment, environment: req._secret.environment,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.['user-agent'] userAgent: req.headers?.["user-agent"]
} }
}); });
} }
res.status(200).send({ res.status(200).send({
secret: req._secret secret: req._secret
}) });
} };
/** /**
* Update secrets for workspace with id [workspaceId] and environment [environment] * Update secrets for workspace with id [workspaceId] and environment [environment]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateSecrets = async (req: Request, res: Response) => { export const updateSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params;
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) const secretIdsUserCanModify = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
if (secretIdsUserCanModifyError) {
throw InternalServerError({ message: "Unable to fetch secrets you own" })
}
const secretsUserCanModifySet: Set<string> = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString())); const secretsUserCanModifySet: Set<string> = new Set(
const updateOperationsToPerform: any = [] secretIdsUserCanModify.map((objectId) => objectId._id.toString())
);
const updateOperationsToPerform: any = [];
secretsModificationsRequested.forEach(userModifiedSecret => { secretsModificationsRequested.forEach((userModifiedSecret) => {
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) { if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
const sanitizedSecret: SanitizedSecretModify = { const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext, secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext,
@@ -244,57 +240,54 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext, secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext,
secretCommentIV: userModifiedSecret.secretCommentIV, secretCommentIV: userModifiedSecret.secretCommentIV,
secretCommentTag: userModifiedSecret.secretCommentTag, secretCommentTag: userModifiedSecret.secretCommentTag,
secretCommentHash: userModifiedSecret.secretCommentHash, secretCommentHash: userModifiedSecret.secretCommentHash
} };
const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: sanitizedSecret } } } const updateOperation = {
updateOperationsToPerform.push(updateOperation) updateOne: {
filter: { _id: userModifiedSecret._id, workspace: workspaceId },
update: { $inc: { version: 1 }, $set: sanitizedSecret }
}
};
updateOperationsToPerform.push(updateOperation);
} else { } 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()) await Secret.bulkWrite(updateOperationsToPerform);
if (bulkModificationInfoError) {
if (bulkModificationInfoError instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkModificationInfoError.stack })
}
throw InternalServerError()
}
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets modified', event: "secrets modified",
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: (secretsModificationsRequested ?? []).length, numberOfSecrets: (secretsModificationsRequested ?? []).length,
environment: environmentName, environment: environmentName,
workspaceId, workspaceId,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.['user-agent'] 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] * Update a secret within workspace with id [workspaceId] and environment [environment]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateSecret = async (req: Request, res: Response) => { export const updateSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params;
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret; const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
const [secretIdUserCanModifyError, secretIdUserCanModify] = await to(Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) await Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
if (secretIdUserCanModifyError && !secretIdUserCanModify) {
throw BadRequestError()
}
const sanitizedSecret: SanitizedSecretModify = { const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext, secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
@@ -308,45 +301,55 @@ export const updateSecret = async (req: Request, res: Response) => {
secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext, secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext,
secretCommentIV: secretModificationsRequested.secretCommentIV, secretCommentIV: secretModificationsRequested.secretCommentIV,
secretCommentTag: secretModificationsRequested.secretCommentTag, 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()) const singleModificationUpdate = await Secret.updateOne(
if (error instanceof ValidationError) { { _id: secretModificationsRequested._id, workspace: workspaceId },
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: error.stack }) { $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) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets modified', event: "secrets modified",
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
environment: environmentName, environment: environmentName,
workspaceId, workspaceId,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.['user-agent'] 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 * Return secrets for workspace with id [workspaceId], environment [environment] and user
* with id [req.user._id] * with id [req.user._id]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getSecrets = async (req: Request, res: Response) => { export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const { environment } = req.query; const { environment } = req.query;
const { workspaceId } = req.params; const { workspaceId } = req.params;
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user let userId: Types.ObjectId | undefined = undefined; // used for getting personal secrets for user
let userEmail: string | undefined = undefined // used for posthog let userEmail: string | undefined = undefined; // used for posthog
if (req.user) { if (req.user) {
userId = req.user._id; userId = req.user._id;
userEmail = req.user.email; userEmail = req.user.email;
@@ -354,47 +357,47 @@ export const getSecrets = async (req: Request, res: Response) => {
if (req.serviceTokenData) { if (req.serviceTokenData) {
userId = req.serviceTokenData.user; 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(); if (!user) throw AccountNotFoundError();
userEmail = user.email; userEmail = user.email;
} }
const [err, secrets] = await to(Secret.find( const secrets = await Secret.find({
{ workspace: workspaceId,
workspace: workspaceId, environment,
environment, $or: [{ user: userId }, { user: { $exists: false } }],
$or: [{ user: userId }, { user: { $exists: false } }], type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } })
} .catch((err) => {
).then()) throw RouteValidationError({
message: "Failed to get secrets, please try again",
if (err) { stack: err.stack
throw RouteValidationError({ message: "Failed to get secrets, please try again", stack: err.stack }) });
} })
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets pulled', event: "secrets pulled",
distinctId: userEmail, distinctId: userEmail,
properties: { properties: {
numberOfSecrets: (secrets ?? []).length, numberOfSecrets: (secrets ?? []).length,
environment, environment,
workspaceId, workspaceId,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.['user-agent'] userAgent: req.headers?.["user-agent"]
} }
}); });
} }
return res.json(secrets) return res.json(secrets);
} };
/** /**
* Return secret with id [secretId] * Return secret with id [secretId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getSecret = async (req: Request, res: Response) => { export const getSecret = async (req: Request, res: Response) => {
// if (postHogClient) { // if (postHogClient) {
@@ -414,4 +417,4 @@ export const getSecret = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
secret: req._secret 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 { Request, Response } from "express";
import { Types } from 'mongoose'; import { Types } from "mongoose";
import crypto from 'crypto'; import crypto from "crypto";
import bcrypt from 'bcrypt'; import bcrypt from "bcrypt";
import { import {
ServiceAccount, ServiceAccount,
ServiceAccountKey, ServiceAccountKey,
ServiceAccountOrganizationPermission, ServiceAccountOrganizationPermission,
ServiceAccountWorkspacePermission ServiceAccountWorkspacePermission,
} from '../../models'; } from "../../models";
import { import {
CreateServiceAccountDto CreateServiceAccountDto,
} from '../../interfaces/serviceAccounts/dto'; } from "../../interfaces/serviceAccounts/dto";
import { BadRequestError, ServiceAccountNotFoundError } from '../../utils/errors'; import { BadRequestError, ServiceAccountNotFoundError } from "../../utils/errors";
import { getSaltRounds } from '../../config'; import { getSaltRounds } from "../../config";
/** /**
* Return service account tied to the request (service account) client * 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) => { export const getCurrentServiceAccount = async (req: Request, res: Response) => {
const serviceAccount = await ServiceAccount.findById(req.serviceAccount._id); const serviceAccount = await ServiceAccount.findById(req.serviceAccount._id);
if (!serviceAccount) { if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' }); throw ServiceAccountNotFoundError({ message: "Failed to find service account" });
} }
return res.status(200).send({ 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) => { export const getServiceAccountById = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params; const { serviceAccountId } = req.params;
const serviceAccount = await ServiceAccount.findById(serviceAccountId); const serviceAccount = await ServiceAccount.findById(serviceAccountId);
if (!serviceAccount) { if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' }); throw ServiceAccountNotFoundError({ message: "Failed to find service account" });
} }
return res.status(200).send({ return res.status(200).send({
serviceAccount serviceAccount,
}); });
} }
@@ -71,9 +71,9 @@ export const createServiceAccount = async (req: Request, res: Response) => {
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); 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()); const secretHash = await bcrypt.hash(secret, await getSaltRounds());
// create service account // create service account
const serviceAccount = await new ServiceAccount({ const serviceAccount = await new ServiceAccount({
name, name,
@@ -82,23 +82,23 @@ export const createServiceAccount = async (req: Request, res: Response) => {
publicKey, publicKey,
lastUsed: new Date(), lastUsed: new Date(),
expiresAt, expiresAt,
secretHash secretHash,
}).save(); }).save()
const serviceAccountObj = serviceAccount.toObject(); const serviceAccountObj = serviceAccount.toObject();
delete serviceAccountObj.secretHash; delete (serviceAccountObj as any).secretHash;
// provision default org-level permission for service account // provision default org-level permission for service account
await new ServiceAccountOrganizationPermission({ await new ServiceAccountOrganizationPermission({
serviceAccount: serviceAccount._id serviceAccount: serviceAccount._id,
}).save(); }).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({ return res.status(200).send({
serviceAccountAccessKey: `sa.${secretId}.${secret}`, 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) => { export const changeServiceAccountName = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params; const { serviceAccountId } = req.params;
const { name } = req.body; const { name } = req.body;
const serviceAccount = await ServiceAccount.findOneAndUpdate( 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({ return res.status(200).send({
serviceAccount serviceAccount,
}); });
} }
@@ -140,15 +140,15 @@ export const addServiceAccountKey = async (req: Request, res: Response) => {
const { const {
workspaceId, workspaceId,
encryptedKey, encryptedKey,
nonce nonce,
} = req.body; } = req.body;
const serviceAccountKey = await new ServiceAccountKey({ const serviceAccountKey = await new ServiceAccountKey({
encryptedKey, encryptedKey,
nonce, nonce,
sender: req.user._id, sender: req.user._id,
serviceAccount: req.serviceAccount._d, serviceAccount: req.serviceAccount._d,
workspace: new Types.ObjectId(workspaceId) workspace: new Types.ObjectId(workspaceId),
}).save(); }).save();
return serviceAccountKey; return serviceAccountKey;
@@ -161,11 +161,11 @@ export const addServiceAccountKey = async (req: Request, res: Response) => {
*/ */
export const getServiceAccountWorkspacePermissions = async (req: Request, res: Response) => { export const getServiceAccountWorkspacePermissions = async (req: Request, res: Response) => {
const serviceAccountWorkspacePermissions = await ServiceAccountWorkspacePermission.find({ const serviceAccountWorkspacePermissions = await ServiceAccountWorkspacePermission.find({
serviceAccount: req.serviceAccount._id serviceAccount: req.serviceAccount._id,
}).populate('workspace'); }).populate("workspace");
return res.status(200).send({ return res.status(200).send({
serviceAccountWorkspacePermissions serviceAccountWorkspacePermissions,
}); });
} }
@@ -182,48 +182,48 @@ export const addServiceAccountWorkspacePermission = async (req: Request, res: Re
read = false, read = false,
write = false, write = false,
encryptedKey, encryptedKey,
nonce nonce,
} = req.body; } = req.body;
if (!req.membership.workspace.environments.some((e: { name: string; slug: string }) => e.slug === environment)) { if (!req.membership.workspace.environments.some((e: { name: string; slug: string }) => e.slug === environment)) {
return res.status(400).send({ return res.status(400).send({
message: 'Failed to validate workspace environment' message: "Failed to validate workspace environment",
}); });
} }
const existingPermission = await ServiceAccountWorkspacePermission.findOne({ const existingPermission = await ServiceAccountWorkspacePermission.findOne({
serviceAccount: new Types.ObjectId(serviceAccountId), serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId), 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({ const serviceAccountWorkspacePermission = await new ServiceAccountWorkspacePermission({
serviceAccount: new Types.ObjectId(serviceAccountId), serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
read, read,
write write,
}).save(); }).save();
const existingServiceAccountKey = await ServiceAccountKey.findOne({ const existingServiceAccountKey = await ServiceAccountKey.findOne({
serviceAccount: new Types.ObjectId(serviceAccountId), serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId) workspace: new Types.ObjectId(workspaceId),
}); });
if (!existingServiceAccountKey) { if (!existingServiceAccountKey) {
await new ServiceAccountKey({ await new ServiceAccountKey({
encryptedKey, encryptedKey,
nonce, nonce,
sender: req.user._id, sender: req.user._id,
serviceAccount: new Types.ObjectId(serviceAccountId), serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId) workspace: new Types.ObjectId(workspaceId),
}).save(); }).save();
} }
return res.status(200).send({ return res.status(200).send({
serviceAccountWorkspacePermission serviceAccountWorkspacePermission,
}); });
} }
@@ -240,19 +240,19 @@ export const deleteServiceAccountWorkspacePermission = async (req: Request, res:
const { serviceAccount, workspace } = serviceAccountWorkspacePermission; const { serviceAccount, workspace } = serviceAccountWorkspacePermission;
const count = await ServiceAccountWorkspacePermission.countDocuments({ const count = await ServiceAccountWorkspacePermission.countDocuments({
serviceAccount, serviceAccount,
workspace workspace,
}); });
if (count === 0) { if (count === 0) {
await ServiceAccountKey.findOneAndDelete({ await ServiceAccountKey.findOneAndDelete({
serviceAccount, serviceAccount,
workspace workspace,
}); });
} }
} }
return res.status(200).send({ return res.status(200).send({
serviceAccountWorkspacePermission serviceAccountWorkspacePermission,
}); });
} }
@@ -269,20 +269,20 @@ export const deleteServiceAccount = async (req: Request, res: Response) => {
if (serviceAccount) { if (serviceAccount) {
await ServiceAccountKey.deleteMany({ await ServiceAccountKey.deleteMany({
serviceAccount: serviceAccount._id serviceAccount: serviceAccount._id,
}); });
await ServiceAccountOrganizationPermission.deleteMany({ await ServiceAccountOrganizationPermission.deleteMany({
serviceAccount: new Types.ObjectId(serviceAccountId) serviceAccount: new Types.ObjectId(serviceAccountId),
}); });
await ServiceAccountWorkspacePermission.deleteMany({ await ServiceAccountWorkspacePermission.deleteMany({
serviceAccount: new Types.ObjectId(serviceAccountId) serviceAccount: new Types.ObjectId(serviceAccountId),
}); });
} }
return res.status(200).send({ 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) => { export const getServiceAccountKeys = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const serviceAccountKeys = await ServiceAccountKey.find({ const serviceAccountKeys = await ServiceAccountKey.find({
serviceAccount: req.serviceAccount._id, serviceAccount: req.serviceAccount._id,
...(workspaceId ? { workspace: new Types.ObjectId(workspaceId) } : {}) ...(workspaceId ? { workspace: new Types.ObjectId(workspaceId) } : {}),
}); });
return res.status(200).send({ 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 { Request, Response } from 'express'; import crypto from "crypto";
import crypto from 'crypto'; import bcrypt from "bcrypt";
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 { import {
User, ProjectPermissionActions,
ServiceAccount, ProjectPermissionSub,
ServiceTokenData getUserProjectPermissions
} from '../../models'; } from "../../ee/services/ProjectRoleService";
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; import { ForbiddenError } from "@casl/ability";
import { import { Types } from "mongoose";
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';
/** /**
* Return service token data associated with service token on request * Return service token data associated with service token on request
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getServiceTokenData = async (req: Request, res: Response) => { export const getServiceTokenData = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return Infisical Token data' #swagger.summary = 'Return Infisical Token data'
#swagger.description = 'Return Infisical Token data' #swagger.description = 'Return Infisical Token data'
@@ -37,111 +36,152 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"serviceTokenData": { "serviceTokenData": {
"type": "object", "type": "object",
$ref: "#/components/schemas/ServiceTokenData", $ref: "#/components/schemas/ServiceTokenData",
"description": "Details of service token" "description": "Details of service token"
} }
} }
} }
} }
} }
} }
*/ */
if (!(req.authData.authPayload instanceof ServiceTokenData)) throw BadRequestError({ if (!(req.authData.authPayload instanceof ServiceTokenData))
message: 'Failed accepted client validation for service token data' throw BadRequestError({
message: "Failed accepted client validation for service token data"
}); });
const serviceTokenData = await ServiceTokenData const serviceTokenData = await ServiceTokenData.findById(req.authData.authPayload._id)
.findById(req.authData.authPayload._id) .select("+encryptedKey +iv +tag")
.select('+encryptedKey +iv +tag') .populate("user")
.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 * Create new service token data for workspace with id [workspaceId] and
* environment [environment]. * environment [environment].
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const createServiceTokenData = async (req: Request, res: Response) => { 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, name,
workspaceId, scopes
environment, }
encryptedKey, },
iv, {
tag, workspaceId: new Types.ObjectId(workspaceId)
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);
} }
);
let user, serviceAccount; return res.status(200).send({
serviceToken,
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) { serviceTokenData
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
});
}
/** /**
* Delete service token data with id [serviceTokenDataId]. * Delete service token data with id [serviceTokenDataId].
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteServiceTokenData = async (req: Request, res: Response) => { 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({ 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,15 +1,14 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import { MembershipOrg, User } from "../../models";
import { User, MembershipOrg } from '../../models'; import { completeAccount } from "../../helpers/user";
import { completeAccount } from '../../helpers/user';
import { import {
initializeDefaultOrg initializeDefaultOrg,
} from '../../helpers/signup'; } from "../../helpers/signup";
import { issueAuthTokens } from '../../helpers/auth'; import { issueAuthTokens } from "../../helpers/auth";
import { INVITED, ACCEPTED } from '../../variables'; import { ACCEPTED, INVITED } from "../../variables";
import { standardRequest } from '../../config/request'; import { standardRequest } from "../../config/request";
import { getLoopsApiKey, getHttpsEnabled } from '../../config'; import { getHttpsEnabled, getLoopsApiKey } from "../../config";
import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
/** /**
* Complete setting up user by adding their personal and auth information as part of the * Complete setting up user by adding their personal and auth information as part of the
@@ -19,142 +18,136 @@ import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
* @returns * @returns
*/ */
export const completeAccountSignup = async (req: Request, res: Response) => { export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token, refreshToken; let user;
try { const {
const { email,
email, firstName,
firstName, lastName,
lastName, protectedKey,
protectedKey, protectedKeyIV,
protectedKeyIV, protectedKeyTag,
protectedKeyTag, publicKey,
publicKey, encryptedPrivateKey,
encryptedPrivateKey, encryptedPrivateKeyIV,
encryptedPrivateKeyIV, encryptedPrivateKeyTag,
encryptedPrivateKeyTag, salt,
salt, verifier,
verifier, organizationName,
organizationName }: {
}: { email: string;
email: string; firstName: string;
firstName: string; lastName: string;
lastName: string; protectedKey: string;
protectedKey: string; protectedKeyIV: string;
protectedKeyIV: string; protectedKeyTag: string;
protectedKeyTag: string; publicKey: string;
publicKey: string; encryptedPrivateKey: string;
encryptedPrivateKey: string; encryptedPrivateKeyIV: string;
encryptedPrivateKeyIV: string; encryptedPrivateKeyTag: string;
encryptedPrivateKeyTag: string; salt: string;
salt: string; verifier: string;
verifier: string; organizationName: string;
organizationName: string; } = req.body;
} = req.body;
// get user // get user
user = await User.findOne({ email }); user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) { if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist. // case 1: user doesn't exist.
// case 2: user has already completed account // case 2: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: 'Failed to complete account for complete user' error: "Failed to complete account for complete user",
}); });
} }
// complete setting up user's account // complete setting up user's account
user = await completeAccount({ user = await completeAccount({
userId: user._id.toString(), userId: user._id.toString(),
firstName, firstName,
lastName, lastName,
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt, salt,
verifier verifier,
}); });
if (!user) if (!user)
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
// initialize default organization and workspace // initialize default organization and workspace
await initializeDefaultOrg({ await initializeDefaultOrg({
organizationName, organizationName,
user user,
}); });
// update organization membership statuses that are // update organization membership statuses that are
// invited to completed with user attached // invited to completed with user attached
const membershipsToUpdate = await MembershipOrg.find({ const membershipsToUpdate = await MembershipOrg.find({
inviteEmail: email, inviteEmail: email,
status: INVITED status: INVITED,
}); });
membershipsToUpdate.forEach(async (membership) => { membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({ await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString() organizationId: membership.organization.toString(),
}); });
}); });
// update organization membership statuses that are // update organization membership statuses that are
// invited to completed with user attached // invited to completed with user attached
await MembershipOrg.updateMany( await MembershipOrg.updateMany(
{ {
inviteEmail: email, inviteEmail: email,
status: INVITED status: INVITED,
}, },
{ {
user, user,
status: ACCEPTED status: ACCEPTED,
} }
); );
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ const tokens = await issueAuthTokens({
userId: user._id.toString() userId: user._id,
}); ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
});
token = tokens.token; const token = tokens.token;
// sending a welcome email to new users // sending a welcome email to new users
if (await getLoopsApiKey()) { if (await getLoopsApiKey()) {
await standardRequest.post("https://app.loops.so/api/v1/events/send", { await standardRequest.post("https://app.loops.so/api/v1/events/send", {
"email": email, "email": email,
"eventName": "Sign Up", "eventName": "Sign Up",
"firstName": firstName, "firstName": firstName,
"lastName": lastName "lastName": lastName,
}, { }, {
headers: { headers: {
"Accept": "application/json", "Accept": "application/json",
"Authorization": "Bearer " + (await getLoopsApiKey()) "Authorization": "Bearer " + (await getLoopsApiKey()),
}, },
}); });
} }
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie("jid", tokens.refreshToken, {
httpOnly: true, httpOnly: true,
path: '/', path: "/",
sameSite: 'strict', sameSite: "strict",
secure: await getHttpsEnabled() secure: await getHttpsEnabled(),
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to complete account setup'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully set up account', message: "Successfully set up account",
user, user,
token token,
}); });
}; };
@@ -166,110 +159,104 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const completeAccountInvite = async (req: Request, res: Response) => { export const completeAccountInvite = async (req: Request, res: Response) => {
let user, token, refreshToken; let user;
try { const {
const { email,
email, firstName,
firstName, lastName,
lastName, protectedKey,
protectedKey, protectedKeyIV,
protectedKeyIV, protectedKeyTag,
protectedKeyTag, publicKey,
publicKey, encryptedPrivateKey,
encryptedPrivateKey, encryptedPrivateKeyIV,
encryptedPrivateKeyIV, encryptedPrivateKeyTag,
encryptedPrivateKeyTag, salt,
salt, verifier,
verifier } = req.body;
} = req.body;
// get user // get user
user = await User.findOne({ email }); user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) { if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist. // case 1: user doesn't exist.
// case 2: user has already completed account // case 2: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: 'Failed to complete account for complete user' error: "Failed to complete account for complete user",
}); });
} }
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email, inviteEmail: email,
status: INVITED 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 // complete setting up user's account
user = await completeAccount({ user = await completeAccount({
userId: user._id.toString(), userId: user._id.toString(),
firstName, firstName,
lastName, lastName,
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt, salt,
verifier verifier,
}); });
if (!user) if (!user)
throw new Error('Failed to complete account for non-existent user'); throw new Error("Failed to complete account for non-existent user");
// update organization membership statuses that are // update organization membership statuses that are
// invited to completed with user attached // invited to completed with user attached
const membershipsToUpdate = await MembershipOrg.find({ const membershipsToUpdate = await MembershipOrg.find({
inviteEmail: email, inviteEmail: email,
status: INVITED status: INVITED,
}); });
membershipsToUpdate.forEach(async (membership) => { membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({ await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString() organizationId: membership.organization.toString(),
}); });
}); });
await MembershipOrg.updateMany( await MembershipOrg.updateMany(
{ {
inviteEmail: email, inviteEmail: email,
status: INVITED status: INVITED,
}, },
{ {
user, user,
status: ACCEPTED status: ACCEPTED,
} }
); );
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ const tokens = await issueAuthTokens({
userId: user._id.toString() userId: user._id,
}); ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
});
token = tokens.token; const token = tokens.token;
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie("jid", tokens.refreshToken, {
httpOnly: true, httpOnly: true,
path: '/', path: "/",
sameSite: 'strict', sameSite: "strict",
secure: await getHttpsEnabled() secure: await getHttpsEnabled(),
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to complete account setup'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully set up account', message: "Successfully set up account",
user, user,
token token,
}); });
}; };

View File

@@ -1,72 +1,82 @@
import { Request, Response } from 'express'; import { ForbiddenError } from "@casl/ability";
import * as Sentry from '@sentry/node'; import { Request, Response } from "express";
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { Secret, Tag } from "../../models";
import { BadRequestError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation";
import { import {
Membership, Secret, ProjectPermissionActions,
} from '../../models'; ProjectPermissionSub,
import Tag, { ITag } from '../../models/tag'; getUserProjectPermissions
import { Builder } from "builder-pattern" } from "../../ee/services/ProjectRoleService";
import to from 'await-to-js'; import * as reqValidator from "../../validation/tags";
import { BadRequestError, UnauthorizedRequestError } from '../../utils/errors';
import { MongoError } from 'mongodb';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
export const createWorkspaceTag = async (req: Request, res: Response) => { export const createWorkspaceTag = async (req: Request, res: Response) => {
const { workspaceId } = req.params const {
const { name, slug } = req.body body: { name, slug },
const sanitizedTagToCreate = Builder<ITag>() params: { workspaceId }
.name(name) } = await validateRequest(reqValidator.CreateWorkspaceTagsV2, req);
.workspace(new Types.ObjectId(workspaceId))
.slug(slug)
.user(new Types.ObjectId(req.user._id))
.build();
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) { const tagToCreate = {
if ((err as MongoError).code === 11000) { name,
throw BadRequestError({ message: "Tags must be unique in a workspace" }) 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) => { 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) const tagFromDB = await Tag.findById(tagId);
if (!tagFromDB) { if (!tagFromDB) {
throw BadRequestError() throw BadRequestError();
} }
// can only delete if the request user is one that belongs to the same workspace as the tag const { permission } = await getUserProjectPermissions(
const membership = await Membership.findOne({ req.user._id,
user: req.user, tagFromDB.workspace.toString()
workspace: tagFromDB.workspace );
}); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Tags
);
if (!membership) { const result = await Tag.findByIdAndDelete(tagId);
UnauthorizedRequestError({ message: 'Failed to validate membership' });
}
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 res.json(result);
await Secret.updateMany( };
{ tags: { $in: [tagId] } },
{ $pull: { tags: tagId } }
);
res.json(result);
}
export const getWorkspaceTags = async (req: Request, res: Response) => { export const getWorkspaceTags = async (req: Request, res: Response) => {
const { workspaceId } = req.params const {
const workspaceTags = await Tag.find({ workspace: workspaceId }) params: { workspaceId }
return res.json({ } = await validateRequest(reqValidator.GetWorkspaceTagsV2, req);
workspaceTags 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 { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import { Types } from "mongoose";
import { import crypto from "crypto";
User, import bcrypt from "bcrypt";
MembershipOrg import { APIKeyData, AuthMethod, MembershipOrg, TokenVersion, User } from "../../models";
} from '../../models'; import { getSaltRounds } from "../../config";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation";
/** /**
* Return the current user. * Return the current user.
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getMe = async (req: Request, res: Response) => { export const getMe = async (req: Request, res: Response) => {
/* /*
#swagger.summary = "Retrieve the current user on the request" #swagger.summary = "Retrieve the current user on the request"
#swagger.description = "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; const user = await User.findById(req.user._id).select(
try { "+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"
user = await User );
.findById(req.user._id)
.select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag'); return res.status(200).send({
} catch (err) { user
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
});
}
/** /**
* Update the current user's MFA-enabled status [isMfaEnabled]. * Update the current user's MFA-enabled status [isMfaEnabled].
* Note: Infisical currently only supports email-based 2FA only; this will expand to * Note: Infisical currently only supports email-based 2FA only; this will expand to
* include SMS and authenticator app modes of authentication in the future. * include SMS and authenticator app modes of authentication in the future.
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateMyMfaEnabled = async (req: Request, res: Response) => { export const updateMyMfaEnabled = async (req: Request, res: Response) => {
let user; const {
try { body: { isMfaEnabled }
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body; } = await validateRequest(reqValidator.UpdateMyMfaEnabledV2, req);
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(); req.user.isMfaEnabled = isMfaEnabled;
user = req.user; if (isMfaEnabled) {
} catch (err) { // TODO: adapt this route/controller
Sentry.setUser({ email: req.user.email }); // to work for different forms of MFA
Sentry.captureException(err); req.user.mfaMethods = ["email"];
return res.status(400).send({ } else {
message: "Failed to update current user's MFA status" 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. * Return organizations that the current user is part of.
* @param req * @param req
* @param res * @param res
*/ */
export const getMyOrganizations = async (req: Request, res: Response) => { export const getMyOrganizations = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return organizations that current user is part of' #swagger.summary = 'Return organizations that current user is part of'
#swagger.description = '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; const organizations = (
try { await MembershipOrg.find({
organizations = ( user: req.user._id
await MembershipOrg.find({ }).populate("organization")
user: req.user._id ).map((m) => m.organization);
}).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"
});
}
return res.status(200).send({ return res.status(200).send({
organizations 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 { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import { Types } from "mongoose";
import { Types } from 'mongoose'; import { Key, Membership, ServiceTokenData, Workspace } from "../../models";
import { import {
Workspace, pullSecrets as pull,
Secret, v2PushSecrets as push,
Membership, reformatPullSecrets
MembershipOrg, } from "../../helpers/secret";
Integration, import { pushKeys } from "../../helpers/key";
IntegrationAuth, import { EventService, TelemetryService } from "../../services";
Key, import { eventPushSecrets } from "../../events";
IUser, import { EEAuditLogService } from "../../ee/services";
ServiceToken, import { EventType } from "../../ee/models";
ServiceTokenData import { validateRequest } from "../../helpers/validation";
} from '../../models'; import * as reqValidator from "../../validation";
import { import {
v2PushSecrets as push, ProjectPermissionActions,
pullSecrets as pull, ProjectPermissionSub,
reformatPullSecrets getUserProjectPermissions
} from '../../helpers/secret'; } from "../../ee/services/ProjectRoleService";
import { pushKeys } from '../../helpers/key'; import { ForbiddenError } from "@casl/ability";
import { TelemetryService, EventService } from '../../services';
import { eventPushSecrets } from '../../events';
interface V2PushSecret { interface V2PushSecret {
type: string; // personal or shared type: string; // personal or shared
secretKeyCiphertext: string; secretKeyCiphertext: string;
secretKeyIV: string; secretKeyIV: string;
secretKeyTag: string; secretKeyTag: string;
secretKeyHash: string; secretKeyHash: string;
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretValueHash: string; secretValueHash: string;
secretCommentCiphertext?: string; secretCommentCiphertext?: string;
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
secretCommentHash?: string; secretCommentHash?: string;
} }
/** /**
@@ -46,71 +44,63 @@ interface V2PushSecret {
* @returns * @returns
*/ */
export const pushWorkspaceSecrets = async (req: Request, res: Response) => { export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
try { const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = await TelemetryService.getPostHogClient(); let { secrets }: { secrets: V2PushSecret[] } = req.body;
let { secrets }: { secrets: V2PushSecret[] } = req.body; const { keys, environment, channel } = req.body;
const { keys, environment, channel } = req.body; const { workspaceId } = req.params;
const { workspaceId } = req.params;
// validate environment // validate environment
const workspaceEnvs = req.membership.workspace.environments; const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment'); throw new Error("Failed to validate environment");
} }
// sanitize secrets // sanitize secrets
secrets = secrets.filter( secrets = secrets.filter(
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== '' (s: V2PushSecret) => s.secretKeyCiphertext !== "" && s.secretValueCiphertext !== ""
); );
await push({ await push({
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
environment, environment,
secrets, secrets,
channel: channel ? channel : 'cli', channel: channel ? channel : "cli",
ipAddress: req.ip ipAddress: req.realIP
}); });
await pushKeys({ await pushKeys({
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
keys keys
}); });
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets pushed', event: "secrets pushed",
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : 'cli' channel: channel ? channel : "cli"
} }
}); });
} }
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment environment,
}) secretPath: "/"
}); })
});
} catch (err) { return res.status(200).send({
Sentry.setUser({ email: req.user.email }); message: "Successfully uploaded workspace secrets"
Sentry.captureException(err); });
return res.status(400).send({
message: 'Failed to upload 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 * @returns
*/ */
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
let secrets; let secrets;
try { const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = await TelemetryService.getPostHogClient(); const environment: string = req.query.environment as string;
const environment: string = req.query.environment as string; const channel: string = req.query.channel as string;
const channel: string = req.query.channel as string; const { workspaceId } = req.params;
const { workspaceId } = req.params;
let userId; let userId;
if (req.user) { if (req.user) {
userId = req.user._id.toString(); userId = req.user._id.toString();
} else if (req.serviceTokenData) { } else if (req.serviceTokenData) {
userId = req.serviceTokenData.user.toString(); userId = req.serviceTokenData.user.toString();
} }
// validate environment // validate environment
const workspaceEnvs = req.membership.workspace.environments; const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment'); throw new Error("Failed to validate environment");
} }
secrets = await pull({ secrets = await pull({
userId, userId,
workspaceId, workspaceId,
environment, environment,
channel: channel ? channel : 'cli', channel: channel ? channel : "cli",
ipAddress: req.ip ipAddress: req.realIP
}); });
if (channel !== 'cli') { if (channel !== "cli") {
secrets = reformatPullSecrets({ secrets }); secrets = reformatPullSecrets({ secrets });
} }
if (postHogClient) { if (postHogClient) {
// capture secrets pushed event in production // capture secrets pushed event in production
postHogClient.capture({ postHogClient.capture({
distinctId: req.user.email, distinctId: req.user.email,
event: 'secrets pulled', event: "secrets pulled",
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : 'cli' 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'
});
}
return res.status(200).send({ return res.status(200).send({
secrets secrets
}); });
}; };
export const getWorkspaceKey = async (req: Request, res: Response) => { export const getWorkspaceKey = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return encrypted project key' #swagger.summary = 'Return encrypted project key'
#swagger.description = 'Return encrypted project key' #swagger.description = 'Return encrypted project key'
@@ -207,61 +189,53 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
} }
} }
*/ */
let key; const {
try { params: { workspaceId }
const { workspaceId } = req.params; } = await validateRequest(reqValidator.GetWorkspaceKeyV2, req);
key = await Key.findOne({ const key = await Key.findOne({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}).populate('sender', '+publicKey'); }).populate("sender", "+publicKey");
if (!key) throw new Error('Failed to find workspace key'); 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'
});
}
return res.status(200).json(key); await EEAuditLogService.createAuditLog(
} req.authData,
export const getWorkspaceServiceTokenData = async ( {
req: Request, type: EventType.GET_WORKSPACE_KEY,
res: Response metadata: {
) => { keyId: key._id.toString()
let serviceTokenData; }
try { },
const { workspaceId } = req.params; {
workspaceId: new Types.ObjectId(workspaceId)
}
);
serviceTokenData = await ServiceTokenData return res.status(200).json(key);
.find({ };
workspace: workspaceId
})
.select('+encryptedKey +iv +tag');
} catch (err) { export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
Sentry.setUser({ email: req.user.email }); const { workspaceId } = req.params;
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace service token data'
});
}
return res.status(200).send({ const serviceTokenData = await ServiceTokenData.find({
serviceTokenData workspace: workspaceId
}); }).select("+encryptedKey +iv +tag");
}
return res.status(200).send({
serviceTokenData
});
};
/** /**
* Return memberships for workspace with id [workspaceId] * Return memberships for workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceMemberships = async (req: Request, res: Response) => { export const getWorkspaceMemberships = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return project memberships' #swagger.summary = 'Return project memberships'
#swagger.description = 'Return project memberships' #swagger.description = 'Return project memberships'
@@ -294,34 +268,33 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
} }
} }
*/ */
let memberships; const {
try { params: { workspaceId }
const { workspaceId } = req.params; } = await validateRequest(reqValidator.GetWorkspaceMembershipsV2, req);
memberships = await Membership.find({ const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
workspace: workspaceId ForbiddenError.from(permission).throwUnlessCan(
}).populate('user', '+publicKey'); ProjectPermissionActions.Read,
} catch (err) { ProjectPermissionSub.Member
Sentry.setUser({ email: req.user.email }); );
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace memberships'
});
}
return res.status(200).send({ const memberships = await Membership.find({
memberships workspace: workspaceId
}); }).populate("user", "+publicKey");
}
return res.status(200).send({
memberships
});
};
/** /**
* Update role of membership with id [membershipId] to role [role] * Update role of membership with id [membershipId] to role [role]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateWorkspaceMembership = async (req: Request, res: Response) => { export const updateWorkspaceMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Update project membership' #swagger.summary = 'Update project membership'
#swagger.description = 'Update project membership' #swagger.description = 'Update project membership'
@@ -374,42 +347,40 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
} }
} }
*/ */
let membership; const {
try { params: { workspaceId, membershipId },
const { body: { role }
membershipId } = await validateRequest(reqValidator.UpdateWorkspaceMembershipsV2, req);
} = req.params;
const { role } = req.body; const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
membership = await Membership.findByIdAndUpdate( ProjectPermissionActions.Edit,
membershipId, ProjectPermissionSub.Member
{ );
role
}, { const membership = await Membership.findByIdAndUpdate(
new: true membershipId,
} {
); role
} catch (err) { },
Sentry.setUser({ email: req.user.email }); {
Sentry.captureException(err); new: true
return res.status(400).send({ }
message: 'Failed to update workspace membership' );
});
} return res.status(200).send({
membership
return res.status(200).send({ });
membership };
});
}
/** /**
* Delete workspace membership with id [membershipId] * Delete workspace membership with id [membershipId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteWorkspaceMembership = async (req: Request, res: Response) => { export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Delete project membership' #swagger.summary = 'Delete project membership'
#swagger.description = 'Delete project membership' #swagger.description = 'Delete project membership'
@@ -445,32 +416,29 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
} }
} }
*/ */
let membership; const {
try { params: { workspaceId, membershipId }
const { } = await validateRequest(reqValidator.DeleteWorkspaceMembershipsV2, req);
membershipId
} = req.params; const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
membership = await Membership.findByIdAndDelete(membershipId); ProjectPermissionActions.Delete,
ProjectPermissionSub.Member
if (!membership) throw new Error('Failed to delete workspace membership'); );
await Key.deleteMany({ const membership = await Membership.findByIdAndDelete(membershipId);
receiver: membership.user,
workspace: membership.workspace if (!membership) throw new Error("Failed to delete workspace membership");
});
} catch (err) { await Key.deleteMany({
Sentry.setUser({ email: req.user.email }); receiver: membership.user,
Sentry.captureException(err); workspace: membership.workspace
return res.status(400).send({ });
message: 'Failed to delete workspace membership'
}); return res.status(200).send({
} membership
});
return res.status(200).send({ };
membership
});
}
/** /**
* Change autoCapitilzation Rule of workspace * Change autoCapitilzation Rule of workspace
@@ -479,32 +447,31 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
* @returns * @returns
*/ */
export const toggleAutoCapitalization = async (req: Request, res: Response) => { export const toggleAutoCapitalization = async (req: Request, res: Response) => {
let workspace; const {
try { params: { workspaceId },
const { workspaceId } = req.params; body: { autoCapitalization }
const { autoCapitalization } = req.body; } = await validateRequest(reqValidator.ToggleAutoCapitalizationV2, req);
workspace = await Workspace.findOneAndUpdate( const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
{ ForbiddenError.from(permission).throwUnlessCan(
_id: workspaceId ProjectPermissionActions.Edit,
}, ProjectPermissionSub.Settings
{ );
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'
});
}
return res.status(200).send({ const workspace = await Workspace.findOneAndUpdate(
message: 'Successfully changed autoCapitalization setting', {
workspace _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 secretsController from "./secretsController";
import * as workspacesController from './workspacesController'; import * as workspacesController from "./workspacesController";
import * as authController from "./authController";
import * as signupController from "./signupController";
export { export {
authController,
secretsController, secretsController,
workspacesController signupController,
} workspacesController,
}

View File

@@ -1,183 +1,704 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import { EventService, SecretService } from "../../services";
SecretService, import { eventPushSecrets } from "../../events";
TelemetryService, import { BotService } from "../../services";
EventService import { containsGlobPatterns, isValidScope, repackageSecretToRaw } from "../../helpers/secrets";
} from '../../services'; import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { eventPushSecrets } from '../../events'; import { getAllImportedSecrets } from "../../services/SecretImportService";
import { getAuthDataPayloadIdObj } from '../../utils/auth'; import { Folder, IServiceTokenData } from "../../models";
import { BadRequestError } from '../../utils/errors'; 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 * Return secrets for workspace with id [workspaceId] and environment
* [environment] * [environment] in plaintext
* @param req * @param req
* @param res * @param res
*/ */
export const getSecrets = async (req: Request, res: Response) => { export const getSecretsRaw = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string; const validatedData = await validateRequest(reqValidator.GetSecretsRawV3, req);
const environment = req.query.environment as string; let {
query: { secretPath, environment, workspaceId }
} = validatedData;
const {
query: { folderId, include_imports: includeImports }
} = validatedData;
const secrets = await SecretService.getSecrets({ // if the service token has single scope, it will get all secrets for that scope by default
workspaceId: new Types.ObjectId(workspaceId), const serviceTokenDetails: IServiceTokenData = req?.serviceTokenData;
environment, if (
authData: req.authData 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({ if (folderId && folderId !== "root") {
secrets const folder = await Folder.findOne({ workspace: workspaceId, environment });
}); if (!folder) throw BadRequestError({ message: "Folder not found" });
}
/** secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
* 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;
const secret = await SecretService.getSecret({ if (!environment || !workspaceId)
secretName, throw BadRequestError({ message: "Missing environment or workspace id" });
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData
});
return res.status(200).send({
secret
});
}
/** let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
* Create secret with name [secretName] if (req.user?._id) {
* @param req const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
* @param res ForbiddenError.from(permission).throwUnlessCan(
*/ ProjectPermissionActions.Read,
export const createSecret = async (req: Request, res: Response) => { subject(ProjectPermissionSub.Secrets, { environment, secretPath })
const { secretName } = req.params; );
const { permissionCheckFn = (env: string, secPath: string) =>
workspaceId, permission.can(
environment, ProjectPermissionActions.Read,
type, subject(ProjectPermissionSub.Secrets, {
secretKeyCiphertext, environment: env,
secretKeyIV, secretPath: secPath
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
}) })
);
} 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(); const secrets = await SecretService.getSecrets({
delete secretWithoutBlindIndex.secretBlindIndex; 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({ 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] * Update secret with name [secretName]
* @param req * @param req
* @param res * @param res
*/ */
export const updateSecretByName = async (req: Request, res: Response) => { export const updateSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params; const {
const { params: { secretName },
workspaceId, body: { secretValue, environment, secretPath, type, workspaceId }
environment, } = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req);
type,
secretValueCiphertext,
secretValueIV,
secretValueTag
} = req.body;
const secret = await SecretService.updateSecret({ if (req.user?._id) {
secretName, const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
workspaceId, ForbiddenError.from(permission).throwUnlessCan(
environment, ProjectPermissionActions.Edit,
type, subject(ProjectPermissionSub.Secrets, { environment, secretPath })
authData: req.authData, );
secretValueCiphertext, } else {
secretValueIV, await validateServiceTokenDataClientForWorkspace({
secretValueTag serviceTokenData: req.authData.authPayload as IServiceTokenData,
}); workspaceId: new Types.ObjectId(workspaceId),
environment,
await EventService.handleEvent({ secretPath,
event: eventPushSecrets({ requiredPermissions: [PERMISSION_WRITE_SECRETS]
workspaceId: new Types.ObjectId(workspaceId),
environment
})
}); });
}
return res.status(200).send({ const key = await BotService.getWorkspaceKeyWithBot({
secret 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] * Delete secret with name [secretName]
* @param req * @param req
* @param res * @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) => { export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params; const {
const { body: { type, environment, secretPath, workspaceId },
workspaceId, params: { secretName }
environment, } = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
type
} = req.body;
const { secret, secrets } = await SecretService.deleteSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData
});
await EventService.handleEvent({ if (req.user?._id) {
event: eventPushSecrets({ const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
workspaceId: new Types.ObjectId(workspaceId), ForbiddenError.from(permission).throwUnlessCan(
environment 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({ const { secret } = await SecretService.deleteSecret({
secret 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 { Request, Response } from "express";
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { Secret } from '../../models'; import { validateRequest } from "../../helpers/validation";
import { SecretService } from'../../services'; 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] * Return whether or not all secrets in workspace with id [workspaceId]
* are blind-indexed * are blind-indexed
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => { 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({ const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
workspace: new Types.ObjectId(workspaceId), if (membership.role !== "admin")
secretBlindIndex: { throw UnauthorizedRequestError({ message: "User must be an admin" });
$exists: false
}
});
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] * Get all secrets for workspace with id [workspaceId]
*/ */
export const getWorkspaceSecrets = async (req: Request, res: Response) => { 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({ const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
workspace: new Types.ObjectId (workspaceId) if (membership.role !== "admin")
}); throw UnauthorizedRequestError({ message: "User must be an admin" });
return res.status(200).send({ const secrets = await Secret.find({
secrets workspace: new Types.ObjectId(workspaceId)
}); });
}
return res.status(200).send({
secrets
});
};
/** /**
* Update blind indices for secrets in workspace with id [workspaceId] * Update blind indices for secrets in workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
*/ */
export const nameWorkspaceSecrets = async (req: Request, res: Response) => { export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
interface SecretToUpdate { const {
secretName: string; params: { workspaceId },
_id: string; body: { secretsToUpdate }
} } = await validateRequest(reqValidator.NameWorkspaceSecretsV3, req);
const { workspaceId } = req.params; const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
const { if (membership.role !== "admin")
secretsToUpdate throw UnauthorizedRequestError({ message: "User must be an admin" });
}: {
secretsToUpdate: SecretToUpdate[];
} = req.body;
// get secret blind index salt // get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({ const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
}); });
// update secret blind indices // update secret blind indices
const operations = await Promise.all( const operations = await Promise.all(
secretsToUpdate.map(async (secretToUpdate: SecretToUpdate) => { secretsToUpdate.map(async (secretToUpdate) => {
const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName: secretToUpdate.secretName, secretName: secretToUpdate.secretName,
salt salt
}); });
return ({
updateOne: {
filter: {
_id: new Types.ObjectId(secretToUpdate._id)
},
update: {
secretBlindIndex
}
}
});
})
);
await Secret.bulkWrite(operations); return {
updateOne: {
filter: {
_id: new Types.ObjectId(secretToUpdate._id)
},
update: {
secretBlindIndex
}
}
};
})
);
return res.status(200).send({ await Secret.bulkWrite(operations);
message: 'Successfully named workspace secrets'
}); 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 { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import { Action } from "../../models";
import { Action, SecretVersion } from '../../models'; import { ActionNotFoundError } from "../../../utils/errors";
import { ActionNotFoundError } from '../../../utils/errors'; import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/action";
export const getAction = async (req: Request, res: Response) => { export const getAction = async (req: Request, res: Response) => {
let action; let action;
try { try {
const { actionId } = req.params; const {
params: { actionId }
action = await Action } = await validateRequest(reqValidator.GetActionV1, req);
.findById(actionId)
.populate([
'payload.secretVersions.oldSecretVersion',
'payload.secretVersions.newSecretVersion'
]);
if (!action) throw ActionNotFoundError({
message: 'Failed to find action'
});
} catch (err) { action = await Action.findById(actionId).populate([
throw ActionNotFoundError({ "payload.secretVersions.oldSecretVersion",
message: 'Failed to find action' "payload.secretVersions.newSecretVersion"
}); ]);
}
if (!action)
return res.status(200).send({ throw ActionNotFoundError({
action message: "Failed to find action"
});
} catch (err) {
throw ActionNotFoundError({
message: "Failed to find action"
}); });
} }
return res.status(200).send({
action
});
};

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
import { Request, Response } from "express"; 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 { IMembershipPermission } from "../../../models/membership";
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
import { ADMIN, MEMBER } from "../../../variables/organization"; import { ADMIN, MEMBER } from "../../../variables/organization";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../../variables'; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../../variables";
import { Builder } from "builder-pattern"
import _ from "lodash"; import _ from "lodash";
import { EEAuditLogService } from "../../services";
export const denyMembershipPermissions = async (req: Request, res: Response) => { export const denyMembershipPermissions = async (req: Request, res: Response) => {
const { membershipId } = req.params; 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" }) throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" })
} }
return Builder<IMembershipPermission>() return {
.environmentSlug(permission.environmentSlug) environmentSlug: permission.environmentSlug,
.ability(permission.ability) ability: permission.ability
.build(); }
}) })
const sanitizedMembershipPermissionsUnique = _.uniqWith(sanitizedMembershipPermissions, _.isEqual) 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" }) 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 => { sanitizedMembershipPermissionsUnique.forEach(permission => {
if (!uniqueEnvironmentSlugs.has(permission.environmentSlug)) { if (!uniqueEnvironmentSlugs.has(permission.environmentSlug)) {
@@ -52,13 +53,34 @@ export const denyMembershipPermissions = async (req: Request, res: Response) =>
{ _id: membershipToModify._id }, { _id: membershipToModify._id },
{ $set: { deniedPermissions: sanitizedMembershipPermissionsUnique } }, { $set: { deniedPermissions: sanitizedMembershipPermissionsUnique } },
{ new: true } { new: true }
) ).populate<{ user: IUser }>("user");
if (!updatedMembershipWithPermissions) { if (!updatedMembershipWithPermissions) {
throw BadRequestError({ message: "The resource has been removed before it can be modified" }) 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({ res.send({
permissionsDenied: updatedMembershipWithPermissions.deniedPermissions permissionsDenied: updatedMembershipWithPermissions.deniedPermissions,
}) })
} }

View File

@@ -1,83 +1,505 @@
import { Request, Response } from 'express'; import { Types } from "mongoose";
import { getLicenseServerUrl } from '../../../config'; import { Request, Response } from "express";
import { licenseServerKeyRequest } from '../../../config/request'; import { getLicenseServerUrl } from "../../../config";
import { EELicenseService } from '../../services'; 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's current plan and allowed feature set * Return the organization current plan's feature set
*/ */
export const getOrganizationPlan = async (req: Request, res: Response) => { export const getOrganizationPlan = async (req: Request, res: Response) => {
const { organizationId } = req.params; const {
query: { workspaceId },
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanv1, req);
const plan = await EELicenseService.getOrganizationPlan(organizationId); const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
return res.status(200).send({ const plan = await EELicenseService.getPlan(
plan, new Types.ObjectId(organizationId),
}); new Types.ObjectId(workspaceId)
} );
return res.status(200).send({
plan
});
};
/** /**
* Update the organization plan to product with id [productId] * Return checkout url for pro trial
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateOrganizationPlan = async (req: Request, res: Response) => { export const startOrganizationTrial = async (req: Request, res: Response) => {
const { const {
productId params: { organizationId },
} = req.body; body: { success_url }
} = await validateRequest(reqValidator.StartOrgTrailv1, req);
const { data } = await licenseServerKeyRequest.patch( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan`, ForbiddenError.from(permission).throwUnlessCan(
{ OrgPermissionActions.Create,
productId OrgPermissionSubjects.Billing
} );
); ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
return res.status(200).send(data); 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 * Return the organization's payment methods on file
*/ */
export const getOrganizationPmtMethods = async (req: Request, res: Response) => { export const getOrganizationPmtMethods = async (req: Request, res: Response) => {
const { data: { pmtMethods } } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods` params: { organizationId }
); } = await validateRequest(reqValidator.GetOrgPmtMethodsv1, req);
return res.status(200).send({ const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
pmtMethods 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 a Stripe session URL to add payment method for organization * Return URL to add payment method for organization
*/ */
export const addOrganizationPmtMethod = async (req: Request, res: Response) => { export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
const { const {
success_url, params: { organizationId },
cancel_url body: { success_url, cancel_url }
} = req.body; } = await validateRequest(reqValidator.CreateOrgPmtMethodv1, req);
const { data: { url } } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`,
{
success_url,
cancel_url
}
);
return res.status(200).send({
url
});
}
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) => { export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => {
const { pmtMethodId } = req.params; const {
params: { organizationId, pmtMethodId }
} = await validateRequest(reqValidator.DelOrgPmtMethodv1, req);
const { data } = await licenseServerKeyRequest.delete( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods/${pmtMethodId}`, ForbiddenError.from(permission).throwUnlessCan(
); OrgPermissionActions.Delete,
OrgPermissionSubjects.Billing
return res.status(200).send(data); );
}
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 { ForbiddenError, subject } from "@casl/ability";
import * as Sentry from '@sentry/node'; import { Request, Response } from "express";
import { Secret } from '../../../models'; import { validateRequest } from "../../../helpers/validation";
import { SecretVersion } from '../../models'; import { Folder, Secret } from "../../../models";
import { EESecretService } from '../../services'; 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] * Return secret versions for secret with id [secretId]
* @param req * @param req
* @param res * @param res
*/ */
export const getSecretVersions = async (req: Request, res: Response) => { export const getSecretVersions = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return secret versions' #swagger.summary = 'Return secret versions'
#swagger.description = 'Return secret versions' #swagger.description = 'Return secret versions'
@@ -55,41 +64,42 @@ import { EESecretService } from '../../services';
} }
} }
*/ */
let secretVersions; const {
try { params: { secretId },
const { secretId } = req.params; query: { offset, limit }
} = await validateRequest(reqValidator.GetSecretVersionsV1, req);
const offset: number = parseInt(req.query.offset as string); const secret = await Secret.findById(secretId);
const limit: number = parseInt(req.query.limit as string); if (!secret) {
throw BadRequestError({ message: "Failed to find secret" });
secretVersions = await SecretVersion.find({ }
secret: secretId
})
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
} catch (err) { const { permission } = await getUserProjectPermissions(req.user._id, secret.workspace.toString());
Sentry.setUser({ email: req.user.email }); ForbiddenError.from(permission).throwUnlessCan(
Sentry.captureException(err); ProjectPermissionActions.Read,
return res.status(400).send({ ProjectPermissionSub.SecretRollback
message: 'Failed to get secret versions' );
});
} const secretVersions = await SecretVersion.find({
secret: secretId
return res.status(200).send({ })
secretVersions .sort({ createdAt: -1 })
}); .skip(offset)
} .limit(limit);
return res.status(200).send({
secretVersions
});
};
/** /**
* Roll back secret with id [secretId] to version [version] * Roll back secret with id [secretId] to version [version]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const rollbackSecretVersion = async (req: Request, res: Response) => { export const rollbackSecretVersion = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Roll back secret to a version.' #swagger.summary = 'Roll back secret to a version.'
#swagger.description = 'Roll back secret to a version.' #swagger.description = 'Roll back secret to a version.'
@@ -137,97 +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,
algorithm,
keyEncoding
} = oldSecretVersion;
// update secret
secret = await Secret.findByIdAndUpdate(
secretId,
{
$inc: {
version: 1
},
workspace,
type,
user,
environment,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
keyEncoding
},
{
new: true
}
);
if (!secret) throw new Error('Failed to find and update secret');
// add new secret version const {
await new SecretVersion({ params: { secretId },
secret: secretId, body: { version }
version: secret.version, } = await validateRequest(reqValidator.RollbackSecretVersionV1, req);
workspace,
type, const toBeUpdatedSec = await Secret.findById(secretId);
user, if (!toBeUpdatedSec) {
environment, throw BadRequestError({ message: "Failed to find secret" });
isDeleted: false, }
...(secretBlindIndex ? { secretBlindIndex } : {}), const { permission } = await getUserProjectPermissions(
secretKeyCiphertext, req.user._id,
secretKeyIV, toBeUpdatedSec.workspace.toString()
secretKeyTag, );
secretValueCiphertext, ForbiddenError.from(permission).throwUnlessCan(
secretValueIV, ProjectPermissionActions.Create,
secretValueTag, ProjectPermissionSub.SecretRollback
algorithm, );
keyEncoding
}).save(); // validate secret version
const oldSecretVersion = await SecretVersion.findOne({
// take secret snapshot secret: secretId,
await EESecretService.takeSecretSnapshot({ version
workspaceId: secret.workspace }).select("+secretBlindIndex");
});
if (!oldSecretVersion) throw new Error("Failed to find secret version");
} catch (err) {
Sentry.setUser({ email: req.user.email }); const {
Sentry.captureException(err); workspace,
return res.status(400).send({ type,
message: 'Failed to roll back secret version' user,
}); environment,
} secretBlindIndex,
secretKeyCiphertext,
return res.status(200).send({ secretKeyIV,
secret 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 { ForbiddenError } from "@casl/ability";
import * as Sentry from '@sentry/node'; import { Request, Response } from "express";
import { SecretSnapshot } from '../../models'; 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] * Return secret snapshot with id [secretSnapshotId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getSecretSnapshot = async (req: Request, res: Response) => { export const getSecretSnapshot = async (req: Request, res: Response) => {
let secretSnapshot; const {
try { params: { secretSnapshotId }
const { secretSnapshotId } = req.params; } = await validateRequest(reqValidator.GetSecretSnapshotV1, req);
secretSnapshot = await SecretSnapshot const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
.findById(secretSnapshotId) .lean()
.populate('secretVersions'); .populate<{ secretVersions: ISecretVersion[] }>({
path: "secretVersions",
if (!secretSnapshot) throw new Error('Failed to find secret snapshot'); populate: {
path: "tags",
model: "Tag"
}
})
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
} catch (err) { if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); const { permission } = await getUserProjectPermissions(
return res.status(400).send({ req.user._id,
message: 'Failed to get secret snapshot' secretSnapshot.workspace.toString()
}); );
} ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
return res.status(200).send({ ProjectPermissionSub.SecretRollback
secretSnapshot );
});
} 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 { Types } from "mongoose";
import { Action } from '../models'; import { Action } from "../models";
import { import {
getLatestNSecretSecretVersionIds,
getLatestSecretVersionIds, getLatestSecretVersionIds,
getLatestNSecretSecretVersionIds } from "../helpers/secretVersion";
} from '../helpers/secretVersion';
import { import {
ACTION_ADD_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_LOGIN, ACTION_LOGIN,
ACTION_LOGOUT, ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_READ_SECRETS, ACTION_READ_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
} from '../../variables'; } from "../../variables";
/** /**
* Create an (audit) action for updating secrets * Create an (audit) action for updating secrets
@@ -26,7 +26,7 @@ const createActionUpdateSecret = async ({
serviceAccountId, serviceAccountId,
serviceTokenDataId, serviceTokenDataId,
workspaceId, workspaceId,
secretIds secretIds,
}: { }: {
name: string; name: string;
userId?: Types.ObjectId; userId?: Types.ObjectId;
@@ -37,11 +37,11 @@ const createActionUpdateSecret = async ({
}) => { }) => {
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({ const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
secretIds, secretIds,
n: 2 n: 2,
})) }))
.map((s) => ({ .map((s) => ({
oldSecretVersion: s.versions[0]._id, oldSecretVersion: s.versions[0]._id,
newSecretVersion: s.versions[1]._id newSecretVersion: s.versions[1]._id,
})); }));
const action = await new Action({ const action = await new Action({
@@ -51,8 +51,8 @@ const createActionUpdateSecret = async ({
serviceTokenData: serviceTokenDataId, serviceTokenData: serviceTokenDataId,
workspace: workspaceId, workspace: workspaceId,
payload: { payload: {
secretVersions: latestSecretVersions secretVersions: latestSecretVersions,
} },
}).save(); }).save();
return action; return action;
@@ -72,7 +72,7 @@ const createActionSecret = async ({
serviceAccountId, serviceAccountId,
serviceTokenDataId, serviceTokenDataId,
workspaceId, workspaceId,
secretIds secretIds,
}: { }: {
name: string; name: string;
userId?: Types.ObjectId; userId?: Types.ObjectId;
@@ -84,10 +84,10 @@ const createActionSecret = async ({
// case: action is adding, deleting, or reading secrets // case: action is adding, deleting, or reading secrets
// -> add new secret versions // -> add new secret versions
const latestSecretVersions = (await getLatestSecretVersionIds({ const latestSecretVersions = (await getLatestSecretVersionIds({
secretIds secretIds,
})) }))
.map((s) => ({ .map((s) => ({
newSecretVersion: s.versionId newSecretVersion: s.versionId,
})); }));
const action = await new Action({ const action = await new Action({
@@ -97,8 +97,8 @@ const createActionSecret = async ({
serviceTokenData: serviceTokenDataId, serviceTokenData: serviceTokenDataId,
workspace: workspaceId, workspace: workspaceId,
payload: { payload: {
secretVersions: latestSecretVersions secretVersions: latestSecretVersions,
} },
}).save(); }).save();
return action; return action;
@@ -116,7 +116,7 @@ const createActionClient = ({
name, name,
userId, userId,
serviceAccountId, serviceAccountId,
serviceTokenDataId serviceTokenDataId,
}: { }: {
name: string; name: string;
userId?: Types.ObjectId; userId?: Types.ObjectId;
@@ -127,7 +127,7 @@ const createActionClient = ({
name, name,
user: userId, user: userId,
serviceAccount: serviceAccountId, serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId serviceTokenData: serviceTokenDataId,
}).save(); }).save();
return action; return action;
@@ -162,27 +162,27 @@ const createActionHelper = async ({
case ACTION_LOGOUT: case ACTION_LOGOUT:
action = await createActionClient({ action = await createActionClient({
name, name,
userId userId,
}); });
break; break;
case ACTION_ADD_SECRETS: case ACTION_ADD_SECRETS:
case ACTION_READ_SECRETS: case ACTION_READ_SECRETS:
case ACTION_DELETE_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({ action = await createActionSecret({
name, name,
userId, userId,
workspaceId, workspaceId,
secretIds secretIds,
}); });
break; break;
case ACTION_UPDATE_SECRETS: 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({ action = await createActionUpdateSecret({
name, name,
userId, userId,
workspaceId, workspaceId,
secretIds secretIds,
}); });
break; break;
} }
@@ -191,5 +191,5 @@ const createActionHelper = async ({
} }
export { export {
createActionHelper createActionHelper,
}; };

View File

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

View File

@@ -1,8 +1,8 @@
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import {
IAction,
Log, Log,
IAction } from "../models";
} from '../models';
/** /**
* Create an (audit) log * Create an (audit) log
@@ -21,7 +21,7 @@ const createLogHelper = async ({
workspaceId, workspaceId,
actions, actions,
channel, channel,
ipAddress ipAddress,
}: { }: {
userId?: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId; serviceAccountId?: Types.ObjectId;
@@ -39,12 +39,12 @@ const createLogHelper = async ({
actionNames: actions.map((a) => a.name), actionNames: actions.map((a) => a.name),
actions, actions,
channel, channel,
ipAddress ipAddress,
}).save(); }).save();
return log; return log;
} }
export { 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 { Types } from "mongoose";
import { Secret, ISecret } from "../../models"; import { Secret } from "../../models";
import { SecretSnapshot, SecretVersion, ISecretVersion } 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 * 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 ({ const takeSecretSnapshotHelper = async ({
workspaceId, workspaceId,
environment,
folderId = "root",
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string;
folderId?: string;
}) => { }) => {
// get all folder ids
const secretIds = ( const secretIds = (
await Secret.find( await Secret.find(
{ {
workspace: workspaceId, workspace: workspaceId,
environment,
folder: folderId,
}, },
"_id" "_id"
) ).lean()
).map((s) => s._id); ).map((s) => s._id);
const latestSecretVersions = ( const latestSecretVersions = (
await SecretVersion.aggregate([ await SecretVersion.aggregate([
{ {
$match: { $match: {
environment,
workspace: new Types.ObjectId(workspaceId),
secret: { secret: {
$in: secretIds, $in: secretIds,
}, },
@@ -45,6 +59,11 @@ const takeSecretSnapshotHelper = async ({
}, },
]).exec() ]).exec()
).map((s) => s.versionId); ).map((s) => s.versionId);
const latestFolderVersion = await FolderVersion.findOne({
environment,
workspace: workspaceId,
"nodes.id": folderId,
}).sort({ "nodes.version": -1 });
const latestSecretSnapshot = await SecretSnapshot.findOne({ const latestSecretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId, workspace: workspaceId,
@@ -52,8 +71,11 @@ const takeSecretSnapshotHelper = async ({
const secretSnapshot = await new SecretSnapshot({ const secretSnapshot = await new SecretSnapshot({
workspace: workspaceId, workspace: workspaceId,
environment,
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1, version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
secretVersions: latestSecretVersions, secretVersions: latestSecretVersions,
folderId,
folderVersion: latestFolderVersion,
}).save(); }).save();
return secretSnapshot; return secretSnapshot;
@@ -96,5 +118,5 @@ const markDeletedSecretVersionsHelper = async ({
export { export {
takeSecretSnapshotHelper, takeSecretSnapshotHelper,
addSecretVersionsHelper, addSecretVersionsHelper,
markDeletedSecretVersionsHelper markDeletedSecretVersionsHelper,
}; };

View File

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

View File

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

View File

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

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;

View File

@@ -0,0 +1,31 @@
import { Schema, model } from "mongoose";
type Installation = {
installationId: string
organizationId: string
user: Schema.Types.ObjectId
};
const gitAppOrganizationInstallation = new Schema<Installation>({
installationId: {
type: String,
required: true,
unique: true
},
organizationId: {
type: String,
required: true,
unique: true
},
user: {
type: Schema.Types.ObjectId,
ref: "User",
required: true,
}
});
const GitAppOrganizationInstallation = model<Installation>("git_app_organization_installation", gitAppOrganizationInstallation);
export default GitAppOrganizationInstallation;

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