Compare commits

..

218 Commits

Author SHA1 Message Date
a1f08b064e add tags support in secret imports 2023-08-24 17:21:14 -04:00
50977cf788 reduce k8 events 2023-08-24 15:41:29 -04:00
8ee6710e9b Merge pull request #889 from EBEN4REAL/custom-tag-colors
Custom tag colors
2023-08-23 21:03:46 -07:00
9fa28f5b5e Fix: added empty string as default for tag color and added regex to resolve issue with multiple spacing in tag names. 2023-08-24 03:59:49 +01:00
ae375916e8 Fix: added nullable check for adding tag color in project settings 2023-08-24 03:39:46 +01:00
21f1648998 Merge pull request #887 from Infisical/signup-secret-tagging
Update signup secret distinction/tagging for better telemetry
2023-08-23 19:23:44 -07:00
88695a2f8c Merge pull request #884 from monto7926/sortable-secrets-overview
feat: make secrets overview sortable
2023-08-23 17:47:34 -07:00
77114e02cf fixed the import linting issues 2023-08-23 17:42:29 -07:00
3ac1795a5b Update kubernetes-helm.mdx 2023-08-23 17:42:07 -04:00
8d6f59b253 up infisical chart version 2023-08-23 17:15:30 -04:00
7fd77b14ff print default connection string in helm 2023-08-23 17:14:09 -04:00
8d3d7d98e3 chore: updated style for tag color label 2023-08-23 18:50:24 +01:00
6cac879ed0 chore: removed console log 2023-08-23 16:46:06 +01:00
ac66834daa chore: fixed error with typings 2023-08-23 16:36:48 +01:00
0616f24923 Merge pull request #866 from Killian-Smith/email-case-sensitive
fix: normalize email when inviting memebers and logging in.
2023-08-23 18:08:28 +07:00
4e1abc6eba Add login email lowercasing to backend 2023-08-23 18:02:18 +07:00
8f57377130 Merge remote-tracking branch 'origin' into email-case-sensitive 2023-08-23 17:50:46 +07:00
2d7c7f075e Remove metadata from SecretVersion schema 2023-08-23 17:47:25 +07:00
c342b22d49 Fix telemetry issue for signup secrets 2023-08-23 17:37:01 +07:00
b8120f7512 Merge pull request #886 from Infisical/audit-log-paywall
Add paywall to Audit Logs V2
2023-08-23 17:00:27 +07:00
ca18883bd3 Add paywall for audit logs v2 2023-08-23 16:55:07 +07:00
8b381b2b80 Checkpoint add metadata to secret and secret version data structure 2023-08-23 16:30:42 +07:00
6bcf5cb54c override secrets before expand 2023-08-22 23:37:32 -04:00
51b425dceb swap out v2 login 2023-08-22 23:37:32 -04:00
84840bddb5 Merge branch 'main' of https://github.com/Infisical/infisical 2023-08-22 15:10:30 -07:00
93640c9d69 added tooltips to the sercret overview 2023-08-22 15:10:18 -07:00
ec856f0bcc remove return from integration loop 2023-08-22 21:18:18 +00:00
3e46bec6f7 add simple api to trigger integration sync 2023-08-22 14:55:08 -04:00
954806d950 chore: code cleanup 2023-08-22 17:59:11 +02:00
d6d3302659 feat: make secrets overview sortable 2023-08-22 17:21:21 +02:00
81743d55ab fix infisical radar app name 2023-08-22 09:35:31 -04:00
9a1b453c86 Feat: added tag color widgt and changed tag popover design 2023-08-22 05:12:23 +01:00
5b342409e3 Merge pull request #815 from Infisical/snyk-fix-477e109149f5e5a943a435c5bf8814b7
[Snyk] Security upgrade winston-loki from 6.0.6 to 6.0.7
2023-08-21 16:02:02 -04:00
a9f54009b8 Merge pull request #848 from narindraditantyo/fix/rootless-frontend-image
fix: frontend image displaying some errors due to sed write permission
2023-08-21 15:54:29 -04:00
82947e183c Merge pull request #851 from sreehari2003/main
fix: form not submitting on keyboard enter
2023-08-21 15:53:15 -04:00
eb7ef2196a Merge pull request #872 from iamunnip/blogs
added blog link for setting up infisical in developement cluster
2023-08-21 14:09:18 -04:00
ad3801ce36 Merge pull request #882 from akhilmhdh/feat/integration-var-not-found
fix(integration): instead of throwing error console and return empty string on interpolation
2023-08-21 13:51:16 -04:00
b7aac1a465 fix(integration): instead of throwing error console and return empty string on interpolation 2023-08-21 20:06:24 +05:30
e28ced8eed Provide default path for logging dashboard secrets event 2023-08-21 18:27:18 +07:00
4a95f936ea Correct enable blind-indexing web ui rendering condition 2023-08-21 17:27:32 +07:00
85a39c60bb Fix query condition on delete secret v3 2023-08-21 16:51:31 +07:00
66ea3ba172 feat: added custom design for tags 2023-08-20 10:02:40 +01:00
01d91c0dc7 update helm version 2023-08-19 17:19:42 -04:00
dedd27a781 remove unsed redis template 2023-08-19 17:19:07 -04:00
57a6d1fff6 fix syntax error in helm chart 2023-08-19 14:47:46 -04:00
554f0c79a4 update redis doc 2023-08-19 14:31:28 -04:00
2af88d4c99 Merge pull request #843 from Infisical/add-bull-queue
add bull queue
2023-08-19 14:13:34 -04:00
fc8b567352 fix syntax error in /api/status 2023-08-19 14:03:02 -04:00
ec234e198a Merge branch 'main' into add-bull-queue 2023-08-19 13:46:26 -04:00
6e1cc12e3a update redis banner text 2023-08-19 13:43:01 -04:00
1b4b7a967b fix docs typos 2023-08-19 13:42:33 -04:00
e47d6b7f2f added blog link for setting up infisical in developement cluster 2023-08-19 08:59:58 +05:30
45a13d06b5 add redis why docs & update redis notice 2023-08-18 21:20:20 -04:00
4a48c088df Merge pull request #868 from daninge98/custom-environment-sorting
Adds user customizable environment ordering
2023-08-18 17:05:37 -07:00
2b65f65063 Rename things and fix bug in error checking 2023-08-18 17:33:59 -04:00
065e150847 update status api 2023-08-18 09:42:33 -04:00
ab72eb1178 added scrollbar to modal 2023-08-17 14:03:56 -07:00
816099a8b4 Merge pull request #869 from Infisical/bring-back-file-vault
Bring back file vault
2023-08-17 15:28:37 -04:00
b5f672cc61 update vault docs 2023-08-17 15:20:17 -04:00
ddc7be18eb link to forked keyring and bring back vault command 2023-08-17 15:07:12 -04:00
c0ce92cf3d Formattting fix 2023-08-16 17:42:39 -04:00
0073fe459e Fix typo 2023-08-16 17:37:41 -04:00
a7f52a9298 Small formatting fixes 2023-08-16 17:36:07 -04:00
29c0d8ab57 Enable users to change the ordering of environments 2023-08-16 17:30:50 -04:00
d7b26cbf04 Fix Select placeholder in audit logs v2 2023-08-17 01:51:44 +07:00
767abe51ef Fix lint errors 2023-08-17 01:34:24 +07:00
5ac1816392 Correct SSO linking case and uncomment audit logs v2 2023-08-17 01:24:41 +07:00
c5b1e7298e Merge pull request #867 from daninge98/improve-tag-colors-and-sorting
Improve tag colors and sorting on dashboard page
2023-08-15 22:01:43 -07:00
3436e6be0e Small formatting changes 2023-08-15 18:46:17 -04:00
b000a78f74 Change tag color assignments and sorting 2023-08-15 18:39:15 -04:00
cb42db3de4 Normalize email when inviting memebers and logging in. 2023-08-15 15:57:27 +01:00
11bb0d648f fixed capitalization 2023-08-14 18:36:44 -07:00
90517258a2 added redis note 2023-08-14 18:30:40 -07:00
d78b37c632 add redis docs 2023-08-14 16:25:16 -04:00
4a6fc9e84f remove console.log and add redis to /status api 2023-08-14 16:24:43 -04:00
8030104c02 update helm read me with redis config details 2023-08-14 15:02:22 -04:00
3825269cbb Merge pull request #857 from Infisical/signup-secrets-fix
added a check for signup events
2023-08-14 11:22:16 +07:00
baa907dbb6 Update source to metadata.source 2023-08-14 11:09:22 +07:00
83465dff2d added a check for signup events 2023-08-13 18:26:41 -07:00
67a8211cb0 Merge pull request #853 from Infisical/linking-sso
Add linking for existing users without SSO enabled logging in via SSO
2023-08-13 22:57:13 +07:00
bc108a82b6 Add SSO linking feature for existing users 2023-08-13 22:47:29 +07:00
05be5910d0 Update changelog 2023-08-13 17:18:18 +07:00
2341ec0e11 Merge pull request #829 from sheensantoscapadngan/feature/enable-users-to-select-multi-auth-methods
Feature: enable users to select multi auth methods (backward compatible)
2023-08-13 17:15:39 +07:00
9652d534b6 fix: moved handler to form submission 2023-08-13 14:00:30 +05:30
dd8f55804c finalized sso controller 2023-08-13 16:18:11 +08:00
95d25b114e Fix incorrect field in validateProviderAuthToken 2023-08-13 14:08:26 +07:00
c0f3aecad3 Fix lint issues 2023-08-13 11:12:07 +07:00
f650cd3925 fix: form not submitting on keyboard enter 2023-08-13 00:54:22 +05:30
8a514e329f fix: frontend image displaying some errors due to sed write permission 2023-08-12 21:53:12 +07:00
dbd55441f2 Update login with multiple auth methods to toggle button and logic 2023-08-12 13:53:55 +07:00
01e613301a console.log queue errors 2023-08-11 19:47:15 -04:00
de7bd27b4b Update README.md 2023-08-11 14:54:34 -07:00
a4cdd14014 Merge pull request #846 from narindraditantyo/fix/rootless-backend-image
fix: backend image failed to start due to npm cache permission
2023-08-11 16:31:11 -04:00
632c78f401 fix: backend image failed to start due to npm cache permission 2023-08-11 23:34:41 +07:00
051f4501e8 Merge pull request #837 from akhilmhdh/fix/supbase-del-res-secret
fix: supabase failed integration due to res secret deletion
2023-08-11 10:38:15 -04:00
69605a1a54 Fast forward 2023-08-11 12:46:30 +07:00
e47912edd7 Merge pull request #838 from Infisical/deprecation
Cleaning & deprecating parts of code
2023-08-11 12:10:19 +07:00
a4edf6bd0c Remove remaining SecurityClient auth calls in favor of hooks, keep RouteGuard 2023-08-11 11:27:33 +07:00
b11cd29943 close all queues 2023-08-10 19:13:09 -04:00
395b51c265 Merge pull request #844 from hahnbeelee/main
cursor-pointer for Explore button
2023-08-10 17:54:17 -04:00
27f56be466 cursor-pointer fir Explore button 2023-08-10 14:33:37 -07:00
dfe95ac773 add bull queue 2023-08-10 17:22:20 -04:00
2dba7847b6 Convert all SecurityClient API calls to hooks except auth 2023-08-10 17:19:23 +07:00
78802409bd Move all integration queries/mutations to hooks 2023-08-10 14:15:24 +07:00
9963724a6a Continue removing unused frontend components/logic, improve querying in select pages 2023-08-10 12:18:17 +07:00
b49ef9efc9 minor frontend UX fixes 2023-08-09 12:03:00 -07:00
a31ffe9617 Update README.md 2023-08-09 10:12:10 -07:00
18beed7540 Merge pull request #839 from Infisical/permissions-audit-log
Add audit logs for workspace user role and read/write permission changes
2023-08-09 22:02:48 +07:00
0a538ac1a7 Add GitHub SSO to changelog 2023-08-09 22:02:19 +07:00
a75ad5ef26 Add logs for workspace user role and read/write permission changes 2023-08-09 21:54:56 +07:00
b47f61f1ad Delete more deprecated frontend calls 2023-08-09 17:55:57 +07:00
2a1665a2c3 Begin marking endpoints for deprecation, clean unused frontend code 2023-08-09 16:45:52 +07:00
e993bd048e fix: supabase failed integration due to res secret deletion 2023-08-09 14:41:42 +05:30
11833ccf0f Note endpoints to deprecate 2023-08-09 11:32:57 +07:00
37d52432d0 Revert run as node backend due to slow build times 2023-08-08 14:11:06 -04:00
04b7e04d98 run full backend docker image as node user 2023-08-08 14:02:39 -04:00
57a3384f32 Merge pull request #834 from akhilmhdh/fix/copy-sec-not-populating
fix: secret path / with a new env causes secret list to be empty
2023-08-08 13:18:41 -04:00
c813c91aec Merge pull request #825 from akhilmhdh/feat/service-token-folder-api
feat: implemented service token support for folder and secret import api
2023-08-08 13:17:54 -04:00
91947df5f6 feat: refactored getPathfromId to folder service 2023-08-08 22:33:59 +05:30
8330890087 feat: implemented service token support for folder and secret import api 2023-08-08 22:20:36 +05:30
221e601173 Correct user agent function name in test 2023-08-08 22:57:38 +07:00
28c24fc8c1 Merge pull request #828 from Infisical/audit-log-revamp
Audit Log V2
2023-08-08 22:45:54 +07:00
a9389643b8 Log GET secrets v2 2023-08-08 22:42:13 +07:00
58854e6b81 Fix merge conflicts 2023-08-08 22:07:22 +07:00
7ae859e9ae Update secret imports audit log v2 2023-08-08 22:04:23 +07:00
ff6e07bdcf minor style changes 2023-08-08 11:03:28 -04:00
fc9393b77f run backend docker commands as node instead of root 2023-08-08 09:48:27 -04:00
0cad823267 Add dashboard-specific secret and secret import audit logs, re-touch audit logs v2 UI 2023-08-08 19:03:24 +07:00
97a0728f02 fix: secret path / with a new env causes secret list to be empty 2023-08-08 12:18:21 +05:30
6cb8cf53f8 Add date filter and pagination component to audit log v2 2023-08-08 12:52:34 +07:00
1ac607b42e Merge remote-tracking branch 'origin' into audit-log-revamp 2023-08-08 10:00:47 +07:00
ec21e35f8c Merge pull request #831 from akhilmhdh/feat/pagination-datepicker
Feat/pagination datepicker
2023-08-08 09:57:41 +07:00
2591161272 Add more audit log events 2023-08-08 09:50:49 +07:00
be86e4176c feat(ui): added datepicker component 2023-08-07 16:22:15 +05:30
2067c021ed feat(ui): added pagination component 2023-08-07 16:21:51 +05:30
648968c453 Run linter 2023-08-07 11:25:06 +07:00
dc3f2c78c1 resolved lint issue 2023-08-06 22:38:24 +08:00
b4dbdbabac used const 2023-08-06 22:33:53 +08:00
681255187f modified initialize org to check for auth providers 2023-08-06 22:30:02 +08:00
bde30049bc ensured backwards compatibility 2023-08-06 22:29:31 +08:00
0a140f5333 updated implementation of user update after sso change 2023-08-06 22:04:39 +08:00
3a9bf5409b finalization of create token logic 2023-08-06 21:46:20 +08:00
04fdccc45d modified backend controllers to support new auth providers 2023-08-06 19:45:27 +08:00
5604232aea added user controller and modified auth method page 2023-08-06 19:09:38 +08:00
373dfff8e0 Remove print statement 2023-08-05 17:06:49 +07:00
b9ce448bed Fix merge conflicts 2023-08-05 16:57:21 +07:00
142fcf0a01 Finish preliminary v2 audit logs 2023-08-05 16:55:06 +07:00
49bcd8839f move github scanning service to ee 2023-08-04 18:15:07 -04:00
d5f6e20c78 Merge pull request #822
fix: added dirty flag to set fn in dropzone paste secret
2023-08-04 17:49:19 -04:00
00030f2231 move secret scanning to ee 2023-08-04 17:42:29 -04:00
24d23e89d0 add exit code to run command 2023-08-04 12:10:46 -04:00
3fe592686a add clarity to CLI docs 2023-08-04 12:01:42 -04:00
9cba0970be Merge pull request #827 from Infisical/maidul98-patch-2
Update usage.mdx
2023-08-04 11:11:48 -04:00
8b50150ec8 Update usage.mdx 2023-08-04 11:11:17 -04:00
5af1eb508c disable trust IP 2023-08-04 10:49:05 -04:00
9d57b1db87 fix: added dirty flag to set fn in dropzone paste secret 2023-08-04 11:46:23 +05:30
9a5329300c Merge pull request #817 from akhilmhdh/feat/import-sec-dashboard
feat: added copy secret feature in dashboard
2023-08-03 18:09:56 -04:00
b03c346985 nit: text update 2023-08-03 18:03:35 -04:00
84efc3de46 pushed out soem changes 2023-08-03 13:30:22 -04:00
2ff3818ecb feat: made changes as discussed with team on dropzone 2023-08-03 22:23:14 +05:30
6fbcbc4807 Add signup with GitHub option 2023-08-03 16:05:14 +07:00
9048988e2f Merge pull request #818 from JunedKhan101/feature-github-signin
initial-setup for github signin
2023-08-03 15:44:15 +07:00
98cfd72928 Merge remote-tracking branch 'origin' into feature-github-signin 2023-08-03 15:39:45 +07:00
2293abfc80 Revise and finish login with GitHub 2023-08-03 15:34:02 +07:00
817a783ec2 feat: updated text and added select all in copy secrets for dashboard 2023-08-03 13:15:28 +05:30
9006212ab5 Merge pull request #819 from Infisical/view-licenses
Add tab to view enterprise license keys in usage and billing section
2023-08-03 11:59:44 +07:00
1627674c2a Merge remote-tracking branch 'origin' into view-licenses 2023-08-03 11:51:12 +07:00
bc65bf1238 Add section for users to view purchased enterprise license keys in organization usage and billing section 2023-08-03 11:48:10 +07:00
3990b6dc49 fixed the autocapitalization ability 2023-08-02 20:01:06 -07:00
a3b8de2e84 Update mint.json 2023-08-02 18:37:59 -07:00
b5bffdbcac Merge pull request #813 from akhilmhdh/feat/sec-exp-ingtegration
Secret expansion and import in integrations
2023-08-02 19:16:03 -04:00
23e40e523a highlight infisical version in k8 docs 2023-08-02 17:41:45 -04:00
d1749deff0 enable checkIPAllowlist 2023-08-02 12:41:04 -04:00
960aceed29 initial-setup for github signin 2023-08-02 21:28:35 +05:30
bd8397bda7 add status code and url to CallGetAccessibleEnvironments 2023-08-02 11:23:22 -04:00
9dac06744b delay cli update notification 2023-08-02 10:57:04 -04:00
bd80c2ccc3 swap out keyring package 2023-08-02 10:11:23 -04:00
466dadc611 feat: added pull secret feature in dashboard with env json parsing and multiline parsing 2023-08-02 16:15:04 +05:30
cc5ca30057 feat: updated multi line format on integrations sync secrets 2023-08-02 16:12:58 +05:30
62fa59619b Add community call link in README 2023-08-02 12:42:32 +07:00
7accaeffcf Revert "keyring swap, better error messages/warnings, delay upgrade notif"
This reverts commit 7f69a3b23f873cd094f40a2a58f6c3fd7ca18832.
2023-08-01 22:55:19 -04:00
7f69a3b23f keyring swap, better error messages/warnings, delay upgrade notif 2023-08-01 21:12:13 -04:00
285a6d633a return instance of ServiceTokenData instead of object 2023-08-01 14:40:08 -04:00
12b71bcf67 Merge pull request #816 from jamesoyanna/fix-blog-404-issue
fix: Blog Section in Doc API Reference Redirects to 404
2023-08-01 13:33:33 -04:00
6c0be52ffa upgrade major version of mongoose from v6 to v7 2023-08-01 13:27:01 -04:00
9df51424a2 upgrade major version of mongoose from v6 to v7 2023-08-01 13:24:38 -04:00
bb466dbe1c fix: backend/package.json & backend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-PROTOBUFJS-5756498
2023-08-01 15:50:51 +00:00
531938a3f1 fix: Blog Section in Doc API Reference Redirects to 404 2023-08-01 16:28:32 +01:00
941a8699b5 Merge pull request #806 from atimapreandrew/teamcity-integration
Teamcity integration
2023-08-01 19:04:20 +07:00
6e42da9063 Optimize TeamCity integration 2023-08-01 18:59:27 +07:00
b1981df8f0 chore: resolved merge conflict 2023-08-01 15:29:34 +05:30
086652a89f fix: resolved infinite recursion cases 2023-08-01 15:24:18 +05:30
6574b6489f fix: added support for secret import and expansion in integrations 2023-08-01 15:24:18 +05:30
69903c0d5c Merge pull request #799 from afrieirham/fix/digital-ocean-sync
fix: digital ocean reset settings on sync
2023-08-01 15:24:57 +07:00
8ff33a4e63 Fix sentence in security mechanics 2023-08-01 14:46:50 +07:00
1d71864092 hide sign up with inviteOnlySignup=true 2023-07-31 23:54:35 -04:00
4b1a27b301 Update changelog 2023-08-01 10:11:22 +07:00
b78150e78d Updated membership logic for SAML auth 2023-07-31 17:36:42 +07:00
a0f08c73af Added images for TeamCity docs 2023-07-30 13:13:52 +01:00
59ebe0c22e Merge pull request #805 from Infisical/jumpcloud-saml
Add JumpCloud SAML Support
2023-07-30 14:36:10 +07:00
6729caeb75 Add JumpCloud SAML 2023-07-30 14:29:47 +07:00
3543a15c09 TeamCity integration 2023-07-30 01:57:39 +01:00
33e0f13eea Added TeamCity integration docs 2023-07-30 00:26:06 +01:00
e9cff4fe69 TeamCity integration 2023-07-30 00:00:24 +01:00
26867f7328 Update overview.mdx 2023-07-29 15:16:11 -07:00
233459d063 Merge pull request #804 from Infisical/jumpcloud-saml
Optimize SAML SSO configuration flow and add documentation for Azure AD SAML
2023-07-29 15:01:41 +07:00
ba6355e4d2 Fix lint errors 2023-07-29 14:58:13 +07:00
e961a30937 Optimize SAML SSO configuration flow, add docs for Azure AD SAML 2023-07-29 14:39:06 +07:00
53ff420304 Merge pull request #802 from akhilmhdh/feat/org-overview-loading
feat: added loading state for org overview page
2023-07-28 15:13:51 -04:00
196a613f16 Merge pull request #790 from subh-cs/subh-cs/better-logs-k8-operator
Better log for k8-operator
2023-07-28 15:11:40 -04:00
cc4b749ce8 Revise SAML flow, update Okta SAML docs 2023-07-29 01:57:07 +07:00
8cc5f2ef43 typo in cli usage.mdx 2023-07-28 13:26:35 -04:00
06bc02c392 feat: added loading state and show empty state only when loading for org overview page 2023-07-28 21:33:31 +05:30
5ce67bf750 fix: send current app settings with env sync update 2023-07-28 18:00:46 +08:00
ed2cf68935 Merge remote-tracking branch 'origin' into jumpcloud-saml 2023-07-28 14:52:28 +07:00
386bc09d49 Update Okta SSO image convention 2023-07-28 14:52:07 +07:00
6502d232c9 Start Azure AD SAML docs 2023-07-27 23:48:53 +07:00
e8a2575f7e logging workspaceId, tokenName from k8-operator 2023-07-27 09:10:08 +05:30
537 changed files with 16356 additions and 11225 deletions

View File

@ -25,6 +25,9 @@ JWT_PROVIDER_AUTH_LIFETIME=
# Required
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
# Redis
REDIS_URL=redis://redis:6379
# Optional credentials for MongoDB container instance and Mongo-Express
MONGO_USERNAME=root
MONGO_PASSWORD=example

View File

@ -1,3 +0,0 @@
{
"workbench.editor.wrapTabs": true
}

View File

@ -34,7 +34,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a>
<a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-395.8k-orange" alt="Cloudsmith downloads" />
<img src="https://img.shields.io/badge/Downloads-821.8k-orange" alt="Cloudsmith downloads" />
</a>
<a href="https://infisical.com/slack">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
@ -116,7 +116,7 @@ Lean about Infisical's code scanning feature [here](https://infisical.com/docs/c
This repo available under the [MIT expat license](https://github.com/Infisical/infisical/blob/main/LICENSE), with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license.
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our webiste](https://infisical.com/) or [book a meeting with us](https://cal.com/vmatsiiako/infisical-demo):
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://cal.com/vmatsiiako/infisical-demo):
<a href="https://cal.com/vmatsiiako/infisical-demo"><img alt="Schedule a meeting" src="https://cal.com/book-with-cal-dark.svg" /></a>
@ -136,6 +136,7 @@ Not sure where to get started? You can:
- [Book a free, non-pressure pairing session / code walkthrough with one of our teammates](https://cal.com/tony-infisical/30-min-meeting-contributing)!
- Join our <a href="https://infisical.com/slack">Slack</a>, and ask us any questions there.
- Join our [community calls](https://us06web.zoom.us/j/82623506356) every Wednesday at 11am EST to ask any questions, provide feedback, hangout and more.
## Resources

View File

@ -14,6 +14,8 @@ FROM node:16-alpine
WORKDIR /app
ENV npm_config_cache /home/node/.npm
COPY package*.json ./
RUN npm ci --only-production
@ -28,4 +30,4 @@ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
EXPOSE 4000
CMD ["npm", "run", "start"]
CMD ["npm", "run", "start"]

5123
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -30,11 +30,13 @@
"jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.10.5",
"mongodb": "^5.7.0",
"mongoose": "^7.4.1",
"nanoid": "^3.3.6",
"node-cache": "^5.1.2",
"nodemailer": "^6.8.0",
"passport": "^0.6.0",
"passport-github": "^1.1.0",
"passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0",
"probot": "^12.3.1",
@ -48,7 +50,7 @@
"typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
"winston-loki": "^6.0.7"
},
"name": "infisical-api",
"version": "1.0.0",
@ -82,6 +84,7 @@
"@posthog/plugin-scaffold": "^1.3.4",
"@types/bcrypt": "^5.0.0",
"@types/bcryptjs": "^2.4.2",
"@types/bull": "^4.10.0",
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",

View File

@ -3203,6 +3203,9 @@
"name": {
"example": "any"
},
"tagColor": {
"example": "any"
},
"slug": {
"example": "any"
}

View File

@ -36,7 +36,6 @@ export const getClientIdVercel = async () => (await client.getSecret("CLIENT_ID_
export const getClientIdNetlify = async () => (await client.getSecret("CLIENT_ID_NETLIFY")).secretValue;
export const getClientIdGitHub = async () => (await client.getSecret("CLIENT_ID_GITHUB")).secretValue;
export const getClientIdGitLab = async () => (await client.getSecret("CLIENT_ID_GITLAB")).secretValue;
export const getClientIdGoogle = async () => (await client.getSecret("CLIENT_ID_GOOGLE")).secretValue;
export const getClientIdBitBucket = async () => (await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
export const getClientSecretAzure = async () => (await client.getSecret("CLIENT_SECRET_AZURE")).secretValue;
export const getClientSecretHeroku = async () => (await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue;
@ -44,9 +43,14 @@ export const getClientSecretVercel = async () => (await client.getSecret("CLIENT
export const getClientSecretNetlify = async () => (await client.getSecret("CLIENT_SECRET_NETLIFY")).secretValue;
export const getClientSecretGitHub = async () => (await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue;
export const getClientSecretGitLab = async () => (await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue;
export const getClientSecretGoogle = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE")).secretValue;
export const getClientSecretBitBucket = async () => (await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIENT_ID_GOOGLE_LOGIN")).secretValue;
export const getClientSecretGoogleLogin = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
export const getClientIdGitHubLogin = async () => (await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
export const getClientSecretGitHubLogin = async () => (await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue;
export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";
export const getSentryDSN = async () => (await client.getSecret("SENTRY_DSN")).secretValue;
@ -64,6 +68,8 @@ export const getSecretScanningWebhookSecret = async () => (await client.getSecre
export const getSecretScanningGitAppId = async () => (await client.getSecret("SECRET_SCANNING_GIT_APP_ID")).secretValue;
export const getSecretScanningPrivateKey = async () => (await client.getSecret("SECRET_SCANNING_PRIVATE_KEY")).secretValue;
export const getRedisUrl = async () => (await client.getSecret("REDIS_URL")).secretValue;
export const getLicenseKey = async () => {
const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;

View File

@ -15,20 +15,20 @@ import { checkUserDevice } from "../../helpers/user";
import {
ACTION_LOGIN,
ACTION_LOGOUT,
AUTH_MODE_JWT,
} from "../../variables";
import {
BadRequestError,
UnauthorizedRequestError,
} from "../../utils/errors";
import { EELogService } from "../../ee/services";
import { getChannelFromUserAgent } from "../../utils/posthog";
import { getUserAgentType } from "../../utils/posthog";
import {
getHttpsEnabled,
getJwtAuthLifetime,
getJwtAuthSecret,
getJwtRefreshSecret,
} from "../../config";
import { ActorType } from "../../ee/models";
declare module "jsonwebtoken" {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -142,7 +142,7 @@ export const login2 = async (req: Request, res: Response) => {
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers["user-agent"]),
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP,
});
@ -170,7 +170,7 @@ export const login2 = async (req: Request, res: Response) => {
* @returns
*/
export const logout = async (req: Request, res: Response) => {
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId)
}
@ -190,7 +190,7 @@ export const logout = async (req: Request, res: Response) => {
logoutAction && await EELogService.createLog({
userId: req.user._id,
actions: [logoutAction],
channel: getChannelFromUserAgent(req.headers["user-agent"]),
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP,
});

View File

@ -3,7 +3,9 @@ import { Types } from "mongoose";
import { standardRequest } from "../../config/request";
import { getApps, getTeams, revokeAccess } from "../../integrations";
import { Bot, IntegrationAuth } from "../../models";
import { EventType } from "../../ee/models";
import { IntegrationService } from "../../services";
import { EEAuditLogService } from "../../ee/services";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
@ -62,6 +64,19 @@ export const oAuthExchange = async (req: Request, res: Response) => {
environment: environments[0].slug
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.AUTHORIZE_INTEGRATION,
metadata: {
integration: integrationAuth.integration
}
},
{
workspaceId: integrationAuth.workspace
}
);
return res.status(200).send({
integrationAuth
});
@ -129,6 +144,19 @@ export const saveIntegrationAccessToken = async (req: Request, res: Response) =>
});
if (!integrationAuth) throw new Error("Failed to save integration access token");
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.AUTHORIZE_INTEGRATION,
metadata: {
integration: integrationAuth.integration
}
},
{
workspaceId: integrationAuth.workspace
}
);
return res.status(200).send({
integrationAuth
@ -530,6 +558,23 @@ export const deleteIntegrationAuth = async (req: Request, res: Response) => {
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
});
if (!integrationAuth) return res.status(400).send({
message: "Failed to find integration authorization"
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UNAUTHORIZE_INTEGRATION,
metadata: {
integration: integrationAuth.integration
}
},
{
workspaceId: integrationAuth.workspace
}
);
return res.status(200).send({
integrationAuth

View File

@ -6,6 +6,9 @@ import { eventStartIntegration } from "../../events";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/syncSecretsToThirdPartyServices";
/**
* Create/initialize an (empty) integration for integration authorization
@ -75,6 +78,31 @@ export const createIntegration = async (req: Request, res: Response) => {
});
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_INTEGRATION,
metadata: {
integrationId: integration._id.toString(),
integration: integration.integration,
environment: integration.environment,
secretPath,
url: integration.url,
app: integration.app,
appId: integration.appId,
targetEnvironment: integration.targetEnvironment,
targetEnvironmentId: integration.targetEnvironmentId,
targetService: integration.targetService,
targetServiceId: integration.targetServiceId,
path: integration.path,
region: integration.region
}
},
{
workspaceId: integration.workspace
}
);
return res.status(200).send({
integration
});
@ -148,8 +176,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
};
/**
* Delete integration with id [integrationId] and deactivate bot if there are
* no integrations left
* Delete integration with id [integrationId]
* @param req
* @param res
* @returns
@ -163,7 +190,44 @@ export const deleteIntegration = async (req: Request, res: Response) => {
if (!integration) throw new Error("Failed to find integration");
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_INTEGRATION,
metadata: {
integrationId: integration._id.toString(),
integration: integration.integration,
environment: integration.environment,
secretPath: integration.secretPath,
url: integration.url,
app: integration.app,
appId: integration.appId,
targetEnvironment: integration.targetEnvironment,
targetEnvironmentId: integration.targetEnvironmentId,
targetService: integration.targetService,
targetServiceId: integration.targetServiceId,
path: integration.path,
region: integration.region
}
},
{
workspaceId: integration.workspace
}
);
return res.status(200).send({
integration
});
};
// Will trigger sync for all integrations within the given env and workspace id
export const manualSync = async (req: Request, res: Response) => {
const { workspaceId, environment } = req.body;
syncSecretsToActiveIntegrationsQueue({
workspaceId,
environment
})
res.status(200).send()
};

View File

@ -1,6 +1,9 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import { Key } from "../../models";
import { findMembership } from "../../helpers/membership";
import { EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services";
/**
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
@ -44,7 +47,7 @@ export const uploadKey = async (req: Request, res: Response) => {
*/
export const getLatestKey = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
// get latest key
const latestKey = await Key.find({
workspace: workspaceId,
@ -58,6 +61,18 @@ export const getLatestKey = async (req: Request, res: Response) => {
if (latestKey.length > 0) {
resObj["latestKey"] = latestKey[0];
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.GET_WORKSPACE_KEY,
metadata: {
keyId: latestKey[0]._id.toString()
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
}
return res.status(200).send(resObj);

View File

@ -1,9 +1,12 @@
import { Request, Response } from "express";
import { Key, Membership, MembershipOrg, User } from "../../models";
import { Types } from "mongoose";
import { IUser, Key, Membership, MembershipOrg, User } from "../../models";
import { EventType } from "../../ee/models";
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
import { sendMail } from "../../helpers/nodemailer";
import { ACCEPTED, ADMIN, MEMBER } from "../../variables";
import { getSiteURL } from "../../config";
import { EEAuditLogService } from "../../ee/services";
/**
* Check that user is a member of workspace with id [workspaceId]
@ -36,11 +39,11 @@ export const validateMembership = async (req: Request, res: Response) => {
*/
export const deleteMembership = async (req: Request, res: Response) => {
const { membershipId } = req.params;
// check if membership to delete exists
const membershipToDelete = await Membership.findOne({
_id: membershipId
}).populate("user");
}).populate<{ user: IUser }>("user");
if (!membershipToDelete) {
throw new Error("Failed to delete workspace membership that doesn't exist");
@ -66,6 +69,20 @@ export const deleteMembership = async (req: Request, res: Response) => {
const deletedMembership = await deleteMember({
membershipId: membershipToDelete._id.toString()
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.REMOVE_WORKSPACE_MEMBER,
metadata: {
userId: membershipToDelete.user._id.toString(),
email: membershipToDelete.user.email
}
},
{
workspaceId: membership.workspace
}
);
return res.status(200).send({
deletedMembership
@ -87,9 +104,9 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
}
// validate target membership
const membershipToChangeRole = await findMembership({
_id: membershipId
});
const membershipToChangeRole = await Membership
.findById(membershipId)
.populate<{ user: IUser }>("user");
if (!membershipToChangeRole) {
throw new Error("Failed to find membership to change role");
@ -110,9 +127,27 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
// user is not an admin member of the workspace
throw new Error("Insufficient role for changing member roles");
}
const oldRole = membershipToChangeRole.role;
membershipToChangeRole.role = role;
await membershipToChangeRole.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_USER_WORKSPACE_ROLE,
metadata: {
userId: membershipToChangeRole.user._id.toString(),
email: membershipToChangeRole.user.email,
oldRole,
newRole: membershipToChangeRole.role
}
},
{
workspaceId: membershipToChangeRole.workspace
}
);
return res.status(200).send({
membership: membershipToChangeRole
@ -140,7 +175,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
const inviteeMembership = await Membership.findOne({
user: invitee._id,
workspace: workspaceId
});
}).populate<{ user: IUser }>("user");
if (inviteeMembership) throw new Error("Failed to add existing member of workspace");
@ -181,6 +216,20 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
}
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.ADD_WORKSPACE_MEMBER,
metadata: {
userId: invitee._id.toString(),
email: invitee.email
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
invitee,
latestKey

View File

@ -103,14 +103,14 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
// validate membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: organizationId
organization: new Types.ObjectId(organizationId)
});
if (!membershipOrg) {
throw new Error("Failed to validate organization membership");
}
const plan = await EELicenseService.getPlan(organizationId);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
const ssoConfig = await SSOConfig.findOne({
organization: new Types.ObjectId(organizationId)

View File

@ -260,22 +260,6 @@ export const createOrganizationPortalSession = async (
}
};
/**
* Return organization subscriptions
* @param req
* @param res
* @returns
*/
export const getOrganizationSubscriptions = async (
req: Request,
res: Response
) => {
return res.status(200).send({
subscriptions: []
});
};
/**
* Given a org id, return the projects each member of the org belongs to
* @param req

View File

@ -5,7 +5,7 @@ import * as bigintConversion from "bigint-conversion";
import { BackupPrivateKey, LoginSRPDetail, User } from "../../models";
import { clearTokens, createToken, sendMail } from "../../helpers";
import { TokenService } from "../../services";
import { AUTH_MODE_JWT, TOKEN_EMAIL_PASSWORD_RESET } from "../../variables";
import { TOKEN_EMAIL_PASSWORD_RESET } from "../../variables";
import { BadRequestError } from "../../utils/errors";
import {
getHttpsEnabled,
@ -13,6 +13,7 @@ import {
getJwtSignupSecret,
getSiteURL
} from "../../config";
import { ActorType } from "../../ee/models";
/**
* Password reset step 1: Send email verification link to email [email]
@ -208,8 +209,7 @@ export const changePassword = async (req: Request, res: Response) => {
);
if (
req.authData.authMode === AUTH_MODE_JWT &&
req.authData.authPayload instanceof User &&
req.authData.actor.type === ActorType.USER &&
req.authData.tokenVersionId
) {
await clearTokens(req.authData.tokenVersionId);

View File

@ -1,17 +1,49 @@
import { Request, Response } from "express";
import { validateMembership } from "../../helpers";
import { isValidScope, validateMembership } from "../../helpers";
import { ServiceTokenData } from "../../models";
import Folder from "../../models/folder";
import SecretImport from "../../models/secretImports";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { BadRequestError } from "../../utils/errors";
import { getFolderWithPathFromId } from "../../services/FolderService";
import { BadRequestError, ResourceNotFoundError,UnauthorizedRequestError } from "../../utils/errors";
import { ADMIN, MEMBER } from "../../variables";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
export const createSecretImport = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId, secretImport } = req.body;
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
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
const importToSecretPath = folders?getFolderWithPathFromId(folders.nodes, folderId).folderPath:"/";
if (!importSecDoc) {
const doc = new SecretImport({
@ -20,7 +52,25 @@ export const createSecretImport = async (req: Request, res: Response) => {
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" });
}
@ -30,11 +80,30 @@ export const createSecretImport = async (req: Request, res: Response) => {
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" });
};
@ -48,14 +117,68 @@ export const updateSecretImport = async (req: Request, res: Response) => {
throw BadRequestError({ message: "Import not found" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: importSecDoc.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
if (!(req.authData.authPayload instanceof ServiceTokenData)) {
await validateMembership({
userId: req.user._id.toString(),
workspaceId: importSecDoc.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
} else {
// 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;
}
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const orderBefore = importSecDoc.imports;
importSecDoc.imports = secretImports;
await importSecDoc.save();
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment,
}).lean();
if (!folders) throw ResourceNotFoundError({
message: "Failed to find folder"
});
const importToSecretPath = folders?getFolderWithPathFromId(folders.nodes, importSecDoc.folderId).folderPath:"/";
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_SECRET_IMPORT,
metadata: {
importToEnvironment: importSecDoc.environment,
importToSecretPath,
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
orderBefore,
orderAfter: secretImports
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully updated secret import" });
};
@ -67,16 +190,69 @@ export const deleteSecretImport = async (req: Request, res: Response) => {
throw BadRequestError({ message: "Import not found" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: importSecDoc.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
if (!(req.authData.authPayload instanceof ServiceTokenData)) {
await validateMembership({
userId: req.user._id.toString(),
workspaceId: importSecDoc.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
} else {
// 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;
}
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
importSecDoc.imports = importSecDoc.imports.filter(
({ environment, secretPath }) =>
!(environment === secretImportEnv && secretPath === secretImportPath)
);
await importSecDoc.save();
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment,
}).lean();
if (!folders) throw ResourceNotFoundError({
message: "Failed to find folder"
});
const importToSecretPath = folders?getFolderWithPathFromId(folders.nodes, importSecDoc.folderId).folderPath:"/";
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
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully delete secret import" });
};
@ -92,6 +268,29 @@ export const getSecretImports = async (req: Request, res: Response) => {
return res.status(200).json({ secretImport: {} });
}
if (req.authData.authPayload instanceof ServiceTokenData) {
// 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;
}
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
return res.status(200).json({ secretImport: importSecDoc });
};
@ -111,6 +310,45 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => {
return res.status(200).json({ secrets: [] });
}
if (req.authData.authPayload instanceof ServiceTokenData) {
// 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;
}
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
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);
return res.status(200).json({ secrets });
};

View File

@ -1,11 +1,11 @@
import { Request, Response } from "express";
import GitAppInstallationSession from "../../models/gitAppInstallationSession";
import GitAppInstallationSession from "../../ee/models/gitAppInstallationSession";
import crypto from "crypto";
import { Types } from "mongoose";
import { UnauthorizedRequestError } from "../../utils/errors";
import GitAppOrganizationInstallation from "../../models/gitAppOrganizationInstallation";
import GitAppOrganizationInstallation from "../../ee/models/gitAppOrganizationInstallation";
import { MembershipOrg } from "../../models";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE, STATUS_RESOLVED_NOT_REVOKED, STATUS_RESOLVED_REVOKED } from "../../models/gitRisks";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE, STATUS_RESOLVED_NOT_REVOKED, STATUS_RESOLVED_REVOKED } from "../../ee/models/gitRisks";
export const createInstallationSession = async (req: Request, res: Response) => {
const sessionId = crypto.randomBytes(16).toString("hex");

View File

@ -1,39 +1,49 @@
import { Request, Response } from "express";
import { Secret } from "../../models";
import { Types } from "mongoose";
import { EventType, FolderVersion } from "../../ee/models";
import { EEAuditLogService, EESecretService } from "../../ee/services";
import { validateMembership } from "../../helpers/membership";
import { isValidScope } from "../../helpers/secrets";
import { Secret, ServiceTokenData } from "../../models";
import Folder from "../../models/folder";
import { BadRequestError } from "../../utils/errors";
import {
appendFolder,
deleteFolderById,
generateFolderId,
getAllFolderIds,
getFolderByPath,
getFolderWithPathFromId,
getParentFromFolderId,
searchByFolderId,
searchByFolderIdWithDir,
validateFolderName,
validateFolderName
} from "../../services/FolderService";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { ADMIN, MEMBER } from "../../variables";
import { validateMembership } from "../../helpers/membership";
import { FolderVersion } from "../../ee/models";
import { EESecretService } from "../../ee/services";
// TODO
// verify workspace id/environment
export const createFolder = async (req: Request, res: Response) => {
const { workspaceId, environment, folderName, parentFolderId } = req.body;
if (!validateFolderName(folderName)) {
throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes",
message: "Folder name cannot contain spaces. Only underscore and dashes"
});
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
environment
}).lean();
// 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,
@ -42,39 +52,93 @@ export const createFolder = async (req: Request, res: Response) => {
id: "root",
name: "root",
version: 1,
children: [{ id, name: folderName, children: [], version: 1 }],
},
children: [{ id, name: folderName, children: [], version: 1 }]
}
});
await folder.save();
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: folder.nodes,
nodes: folder.nodes
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
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" });
}
}
await Folder.findByIdAndUpdate(folders._id, folders);
const parentFolder = searchByFolderId(folders.nodes, parentFolderId);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder,
nodes: parentFolder
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId: parentFolderId,
folderId: parentFolderId
});
const {folderPath} = getFolderWithPathFromId(folders.nodes, folder.id);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: folder.id,
folderName,
folderPath
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.json({ folder });
};
@ -82,28 +146,46 @@ export const createFolder = async (req: Request, res: Response) => {
export const updateFolderById = async (req: Request, res: Response) => {
const { folderId } = req.params;
const { name, workspaceId, environment } = req.body;
if (!validateFolderName(name)) {
throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes"
});
}
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles: [ADMIN, MEMBER],
});
if (!(req.authData.authPayload instanceof ServiceTokenData)) {
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles: [ADMIN, MEMBER]
});
}
const parentFolder = getParentFromFolderId(folders.nodes, folderId);
if (!parentFolder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const folder = parentFolder.children.find(({ id }) => id === folderId);
if (!folder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
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;
@ -111,19 +193,38 @@ export const updateFolderById = async (req: Request, res: Response) => {
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder,
nodes: parentFolder
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId: parentFolder.id,
folderId: parentFolder.id
});
const {folderPath} = getFolderWithPathFromId(folders.nodes, folder.id);
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 },
folder: { name: folder.name, id: folder.id }
});
};
@ -136,12 +237,16 @@ export const deleteFolder = async (req: Request, res: Response) => {
throw BadRequestError({ message: "The folder doesn't exist" });
}
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles: [ADMIN, MEMBER],
});
if (!(req.authData.authPayload instanceof ServiceTokenData)) {
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles: [ADMIN, MEMBER]
});
}
const {folderPath} = getFolderWithPathFromId(folders.nodes, folderId);
const delOp = deleteFolderById(folders.nodes, folderId);
if (!delOp) {
@ -149,6 +254,14 @@ export const deleteFolder = async (req: Request, res: Response) => {
}
const { deletedNode: delFolder, parent: parentFolder } = delOp;
if (req.authData.authPayload instanceof ServiceTokenData) {
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
parentFolder.version += 1;
const delFolderIds = getAllFolderIds(delFolder);
@ -156,35 +269,50 @@ export const deleteFolder = async (req: Request, res: Response) => {
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder,
nodes: parentFolder
});
await folderVersion.save();
if (delFolderIds.length) {
await Secret.deleteMany({
folder: { $in: delFolderIds.map(({ id }) => id) },
workspace: workspaceId,
environment,
environment
});
}
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId: parentFolder.id,
folderId: parentFolder.id
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_FOLDER ,
metadata: {
environment,
folderId,
folderName: delFolder.name,
folderPath
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
res.send({ message: "successfully deleted folders", folders: delFolderIds });
};
// TODO: validate workspace
export const getFolders = async (req: Request, res: Response) => {
const { workspaceId, environment, parentFolderId, parentFolderPath } =
req.query as {
workspaceId: string;
environment: string;
parentFolderId?: string;
parentFolderPath?: string;
};
const { workspaceId, environment, parentFolderId, parentFolderPath } = req.query as {
workspaceId: string;
environment: string;
parentFolderId?: string;
parentFolderPath?: string;
};
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
@ -192,16 +320,29 @@ export const getFolders = async (req: Request, res: Response) => {
return;
}
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles: [ADMIN, MEMBER],
});
if (!(req.authData.authPayload instanceof ServiceTokenData)) {
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles: [ADMIN, MEMBER]
});
}
// 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;
@ -209,27 +350,36 @@ export const getFolders = async (req: Request, res: Response) => {
// 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 }],
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,
name
}));
res.send({ folders: rootFolders });
return;
}
const folderBySearch = searchByFolderIdWithDir(folders.nodes, parentFolderId);
if (!folderBySearch) {
throw BadRequestError({ message: "The folder doesn't exist" });
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" });
}
}
const { folder, dir } = folderBySearch;
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir,
dir
});
};

View File

@ -1,5 +1,5 @@
import { Request, Response } from "express";
import { User } from "../../models";
import { AuthMethod, User } from "../../models";
import { checkEmailVerification, sendEmailVerification } from "../../helpers/signup";
import { createToken } from "../../helpers/auth";
import { BadRequestError } from "../../utils/errors";
@ -81,7 +81,8 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
if (!user) {
user = await new User({
email
email,
authMethods: [AuthMethod.EMAIL]
}).save();
}

View File

@ -4,7 +4,9 @@ import { client, getRootEncryptionKey } from "../../config";
import { validateMembership } from "../../helpers";
import Webhook from "../../models/webhooks";
import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService";
import { BadRequestError } from "../../utils/errors";
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { ADMIN, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, MEMBER } from "../../variables";
export const createWebhook = async (req: Request, res: Response) => {
@ -27,6 +29,23 @@ export const createWebhook = async (req: Request, res: Response) => {
}
await webhook.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_WEBHOOK,
metadata: {
webhookId: webhook._id.toString(),
environment,
secretPath,
webhookUrl,
isDisabled: false
}
},
{
workspaceId
}
);
return res.status(200).send({
webhook,
@ -54,6 +73,23 @@ export const updateWebhook = async (req: Request, res: Response) => {
}
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"
@ -62,9 +98,10 @@ export const updateWebhook = async (req: Request, res: Response) => {
export const deleteWebhook = async (req: Request, res: Response) => {
const { webhookId } = req.params;
const webhook = await Webhook.findById(webhookId);
let webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
throw ResourceNotFoundError({ message: "Webhook not found!!" });
}
await validateMembership({
@ -72,7 +109,29 @@ export const deleteWebhook = async (req: Request, res: Response) => {
workspaceId: webhook.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
await webhook.remove();
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"

View File

@ -1,3 +1,4 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import {
IUser,
@ -108,14 +109,14 @@ export const createWorkspace = async (req: Request, res: Response) => {
// validate organization membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: organizationId,
organization: new Types.ObjectId(organizationId),
});
if (!membershipOrg) {
throw new Error("Failed to validate organization membership");
}
const plan = await EELicenseService.getPlan(organizationId);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (plan.workspaceLimit !== null) {
// case: limit imposed on number of workspaces allowed
@ -134,7 +135,7 @@ export const createWorkspace = async (req: Request, res: Response) => {
// create workspace and add user as member
const workspace = await create({
name: workspaceName,
organizationId,
organizationId: new Types.ObjectId(organizationId),
});
await addMemberships({

View File

@ -14,7 +14,7 @@ import {
ACTION_LOGIN,
TOKEN_EMAIL_MFA,
} from "../../variables";
import { getChannelFromUserAgent } from "../../utils/posthog"; // TODO: move this
import { getUserAgentType } from "../../utils/posthog"; // TODO: move this
import {
getHttpsEnabled,
getJwtMfaLifetime,
@ -203,7 +203,7 @@ export const login2 = async (req: Request, res: Response) => {
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers["user-agent"]),
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.ip,
});
@ -336,7 +336,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers["user-agent"]),
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP,
});

View File

@ -1,4 +1,5 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
Integration,
Membership,
@ -7,8 +8,8 @@ import {
ServiceTokenData,
Workspace,
} from "../../models";
import { SecretVersion } from "../../ee/models";
import { EELicenseService } from "../../ee/services";
import { EventType, SecretVersion } from "../../ee/models";
import { EEAuditLogService, EELicenseService } from "../../ee/services";
import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors";
import _ from "lodash";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
@ -30,7 +31,7 @@ export const createWorkspaceEnvironment = async (
if (!workspace) throw WorkspaceNotFoundError();
const plan = await EELicenseService.getPlan(workspace.organization.toString());
const plan = await EELicenseService.getPlan(workspace.organization);
if (plan.environmentLimit !== null) {
// case: limit imposed on number of environments allowed
@ -58,7 +59,21 @@ export const createWorkspaceEnvironment = async (
});
await workspace.save();
await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId);
await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId));
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_ENVIRONMENT,
metadata: {
name: environmentName,
slug: environmentSlug
}
},
{
workspaceId: workspace._id
}
);
return res.status(200).send({
message: "Successfully created new environment",
@ -70,6 +85,43 @@ export const createWorkspaceEnvironment = async (
});
};
/**
* 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 { workspaceId } = req.params;
const { environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName } = req.body;
// 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,
});
};
/**
* Rename workspace environment with new name and slug of a workspace with [workspaceId]
* Old slug [oldEnvironmentSlug] must be provided
@ -110,6 +162,8 @@ export const renameWorkspaceEnvironment = async (
throw new Error("Invalid environment given");
}
const oldEnvironment = workspace.environments[envIndex];
workspace.environments[envIndex].name = environmentName;
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
@ -141,8 +195,23 @@ export const renameWorkspaceEnvironment = async (
},
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
)
);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_ENVIRONMENT,
metadata: {
oldName: oldEnvironment.name,
newName: environmentName,
oldSlug: oldEnvironment.slug,
newSlug: environmentSlug.toLowerCase()
}
},
{
workspaceId: workspace._id
}
);
return res.status(200).send({
message: "Successfully update environment",
@ -179,6 +248,8 @@ export const deleteWorkspaceEnvironment = async (
throw new Error("Invalid environment given");
}
const oldEnvironment = workspace.environments[envIndex];
workspace.environments.splice(envIndex, 1);
await workspace.save();
@ -215,7 +286,21 @@ export const deleteWorkspaceEnvironment = async (
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
);
await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId);
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",

View File

@ -309,16 +309,16 @@ export const updateSecret = async (req: Request, res: Response) => {
{ _id: secretModificationsRequested._id, workspace: workspaceId },
{ $inc: { version: 1 }, $set: sanitizedSecret }
)
.catch((error) => {
if (error instanceof ValidationError) {
throw RouteValidationError({
message: "Unable to apply modifications, please try again",
stack: error.stack
});
}
.catch((error) => {
if (error instanceof ValidationError) {
throw RouteValidationError({
message: "Unable to apply modifications, please try again",
stack: error.stack
});
}
throw error;
});
throw error;
});
if (postHogClient) {
postHogClient.capture({
@ -370,12 +370,12 @@ export const getSecrets = async (req: Request, res: Response) => {
$or: [{ user: userId }, { user: { $exists: false } }],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
})
.catch((err) => {
throw RouteValidationError({
message: "Failed to get secrets, please try again",
stack: err.stack
});
})
.catch((err) => {
throw RouteValidationError({
message: "Failed to get secrets, please try again",
stack: err.stack
});
})
if (postHogClient) {
postHogClient.capture({

View File

@ -1,7 +1,7 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import { ISecret, Secret, ServiceTokenData } from "../../models";
import { IAction, SecretVersion } from "../../ee/models";
import { AuditLog, EventType, IAction, SecretVersion } from "../../ee/models";
import {
ACTION_ADD_SECRETS,
ACTION_DELETE_SECRETS,
@ -9,14 +9,15 @@ import {
ACTION_UPDATE_SECRETS,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
K8_USER_AGENT_NAME,
SECRET_PERSONAL
} from "../../variables";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { EventService } from "../../services";
import { eventPushSecrets } from "../../events";
import { EELogService, EESecretService } from "../../ee/services";
import { EEAuditLogService, EELogService, EESecretService } from "../../ee/services";
import { SecretService, TelemetryService } from "../../services";
import { getChannelFromUserAgent } from "../../utils/posthog";
import { getUserAgentType } from "../../utils/posthog";
import { PERMISSION_WRITE_SECRETS } from "../../variables";
import {
userHasNoAbility,
@ -44,7 +45,7 @@ import { getAllImportedSecrets } from "../../services/SecretImportService";
* @param res
*/
export const batchSecrets = async (req: Request, res: Response) => {
const channel = getChannelFromUserAgent(req.headers["user-agent"]);
const channel = getUserAgentType(req.headers["user-agent"]);
const postHogClient = await TelemetryService.getPostHogClient();
const {
@ -56,12 +57,13 @@ export const batchSecrets = async (req: Request, res: Response) => {
environment: string;
requests: BatchSecretRequest[];
} = req.body;
let secretPath = req.body.secretPath as string;
let folderId = req.body.folderId as string;
const createSecrets: BatchSecret[] = [];
const updateSecrets: BatchSecret[] = [];
const deleteSecrets: Types.ObjectId[] = [];
const deleteSecrets: { _id: Types.ObjectId, secretName: string; }[] = [];
const actions: IAction[] = [];
// get secret blind index salt
@ -133,7 +135,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
});
break;
case "DELETE":
deleteSecrets.push(new Types.ObjectId(request.secret._id));
deleteSecrets.push({ _id: new Types.ObjectId(request.secret._id), secretName: request.secret.secretName });
break;
}
}
@ -154,6 +156,30 @@ export const batchSecrets = async (req: Request, res: Response) => {
})
});
const auditLogs = await Promise.all(
createdSecrets.map((secret, index) => {
return EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SECRET,
metadata: {
environment: secret.environment,
secretPath: secretPath ?? "/",
secretId: secret._id.toString(),
secretKey: createSecrets[index].secretName,
secretVersion: secret.version
}
},
{
workspaceId: secret.workspace
},
false
);
})
);
await AuditLog.insertMany(auditLogs);
const addAction = (await EELogService.createAction({
name: ACTION_ADD_SECRETS,
userId: req.user?._id,
@ -209,6 +235,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
$inc: {
version: 1
},
$unset: {
'metadata.source': true as true
},
...u,
_id: new Types.ObjectId(u._id)
}
@ -253,6 +282,30 @@ export const batchSecrets = async (req: Request, res: Response) => {
}
});
const auditLogs = await Promise.all(
updateSecrets.map((secret) => {
return EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_SECRET,
metadata: {
environment,
secretPath: secretPath ?? "/",
secretId: secret._id.toString(),
secretKey: secret.secretName,
secretVersion: listedSecretsObj[secret._id.toString()].version
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
},
false
);
})
);
await AuditLog.insertMany(auditLogs);
const updateAction = (await EELogService.createAction({
name: ACTION_UPDATE_SECRETS,
userId: req.user._id,
@ -279,21 +332,60 @@ export const batchSecrets = async (req: Request, res: Response) => {
// handle delete secrets
if (deleteSecrets.length > 0) {
const deleteSecretIds: Types.ObjectId[] = deleteSecrets.map((s) => s._id);
const deletedSecretsObj = (await Secret.find({
_id: {
$in: deleteSecretIds
}
}))
.reduce(
(obj: any, secret: ISecret) => ({
...obj,
[secret._id.toString()]: secret
}),
{}
);
await Secret.deleteMany({
_id: {
$in: deleteSecrets
$in: deleteSecretIds
}
});
await EESecretService.markDeletedSecretVersions({
secretIds: deleteSecrets
secretIds: deleteSecretIds
});
const auditLogs = await Promise.all(
deleteSecrets.map((secret) => {
return EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_SECRET,
metadata: {
environment,
secretPath: secretPath ?? "/",
secretId: secret._id.toString(),
secretKey: secret.secretName,
secretVersion: deletedSecretsObj[secret._id.toString()].version
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
},
false
);
})
);
await AuditLog.insertMany(auditLogs);
const deleteAction = (await EELogService.createAction({
name: ACTION_DELETE_SECRETS,
userId: req.user._id,
workspaceId: new Types.ObjectId(workspaceId),
secretIds: deleteSecrets
secretIds: deleteSecretIds
})) as IAction;
actions.push(deleteAction);
@ -351,7 +443,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
}
if (deleteSecrets.length > 0) {
resObj["deletedSecrets"] = deleteSecrets.map((d) => d.toString());
resObj["deletedSecrets"] = deleteSecrets.map((d) => d._id.toString());
}
return res.status(200).send(resObj);
@ -416,7 +508,7 @@ export const createSecrets = async (req: Request, res: Response) => {
}
*/
const channel = getChannelFromUserAgent(req.headers["user-agent"]);
const channel = getUserAgentType(req.headers["user-agent"]);
const {
workspaceId,
environment,
@ -697,10 +789,16 @@ export const getSecrets = async (req: Request, res: Response) => {
const environment = req.query.environment as string;
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if ((!folders && folderId && folderId !== "root") || (!folders && secretPath)) {
if (
// if no folders and asking for a non root folder id or non root secret path
(!folders && folderId && folderId !== "root") ||
(!folders && secretPath && secretPath !== "/")
) {
res.send({ secrets: [] });
return;
}
if (folders && folderId !== "root") {
const folder = searchByFolderId(folders.nodes, folderId as string);
if (!folder) {
@ -834,7 +932,7 @@ export const getSecrets = async (req: Request, res: Response) => {
importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string);
}
const channel = getChannelFromUserAgent(req.headers["user-agent"]);
const channel = getUserAgentType(req.headers["user-agent"]);
const readAction = await EELogService.createAction({
name: ACTION_READ_SECRETS,
@ -856,22 +954,52 @@ export const getSecrets = async (req: Request, res: Response) => {
ipAddress: req.realIP
}));
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "secrets pulled",
distinctId: await TelemetryService.getDistinctId({
authData: req.authData
}),
properties: {
numberOfSecrets: secrets.length,
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.GET_SECRETS,
metadata: {
environment,
workspaceId,
channel,
folderId,
userAgent: req.headers?.["user-agent"]
secretPath: (secretPath as string) ?? "/",
numberOfSecrets: secrets.length
}
});
},
{
workspaceId: new Types.ObjectId(workspaceId as string)
}
);
const postHogClient = await TelemetryService.getPostHogClient();
// reduce the number of events captured
let shouldRecordK8Event = false
if (req.authData instanceof ServiceTokenData && req.authData.userAgent == K8_USER_AGENT_NAME) {
const randomNumber = Math.random();
if (randomNumber > 0.9) {
shouldRecordK8Event = true
}
}
if (postHogClient) {
const shouldCapture = req.authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event;
const approximateForNoneCapturedEvents = secrets.length * 10
if (shouldCapture) {
postHogClient.capture({
event: "secrets pulled",
distinctId: await TelemetryService.getDistinctId({
authData: req.authData
}),
properties: {
numberOfSecrets: shouldRecordK8Event ? approximateForNoneCapturedEvents : secrets.length,
environment,
workspaceId,
folderId,
channel: req.authData.userAgentType,
userAgent: req.authData.userAgent
}
});
}
}
return res.status(200).send({
@ -978,10 +1106,10 @@ export const updateSecrets = async (req: Request, res: Response) => {
tags,
...(secretCommentCiphertext !== undefined && secretCommentIV && secretCommentTag
? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
}
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
}
: {})
}
}
@ -1170,7 +1298,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
}
*/
const channel = getChannelFromUserAgent(req.headers["user-agent"]);
const channel = getUserAgentType(req.headers["user-agent"]);
const toDelete = req.secrets.map((s: any) => s._id);
await Secret.deleteMany({

View File

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

View File

@ -1,10 +1,11 @@
import { Request, Response } from "express";
import crypto from "crypto";
import bcrypt from "bcrypt";
import { ServiceAccount, ServiceTokenData, User } from "../../models";
import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT } from "../../variables";
import { ServiceTokenData } from "../../models";
import { getSaltRounds } from "../../config";
import { BadRequestError } from "../../utils/errors";
import { ActorType, EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services";
/**
* Return service token data associated with service token on request
@ -73,24 +74,16 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
let user, serviceAccount;
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
let user;
if (req.authData.actor.type === ActorType.USER) {
user = req.authData.authPayload._id;
}
if (
req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
req.authData.authPayload instanceof ServiceAccount
) {
serviceAccount = req.authData.authPayload._id;
}
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
user,
serviceAccount,
scopes,
lastUsed: new Date(),
expiresAt,
@ -107,6 +100,20 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
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,
scopes
}
},
{
workspaceId
}
);
return res.status(200).send({
serviceToken,
@ -124,6 +131,24 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
const { serviceTokenDataId } = req.params;
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
if (!serviceTokenData) return res.status(200).send({
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

@ -6,10 +6,11 @@ import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
export const createWorkspaceTag = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const { name, slug } = req.body;
const { name, slug, tagColor } = req.body;
const tagToCreate = {
name,
tagColor,
workspace: new Types.ObjectId(workspaceId),
slug,
user: new Types.ObjectId(req.user._id),

View File

@ -4,7 +4,7 @@ import crypto from "crypto";
import bcrypt from "bcrypt";
import {
APIKeyData,
AuthProvider,
AuthMethod,
MembershipOrg,
TokenVersion,
User
@ -113,24 +113,35 @@ export const updateName = async (req: Request, res: Response) => {
}
/**
* Update auth provider of the current user to [authProvider]
* Update auth method of the current user to [authMethods]
* @param req
* @param res
* @returns
*/
export const updateAuthProvider = async (req: Request, res: Response) => {
export const updateAuthMethods = async (req: Request, res: Response) => {
const {
authProvider
authMethods
} = req.body;
if (req.user?.authProvider === AuthProvider.OKTA_SAML) return res.status(400).send({
message: "Failed to update user authentication method because SAML SSO is enforced"
});
const 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(),
{
authProvider
authMethods
},
{
new: true
@ -142,6 +153,7 @@ export const updateAuthProvider = async (req: Request, res: Response) => {
});
}
/**
* Return organizations that the current user is part of.
* @param req

View File

@ -9,6 +9,8 @@ import {
import { pushKeys } from "../../helpers/key";
import { EventService, TelemetryService } from "../../services";
import { eventPushSecrets } from "../../events";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
interface V2PushSecret {
type: string; // personal or shared
@ -180,16 +182,30 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
}
*/
const { workspaceId } = req.params;
const key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
}).populate("sender", "+publicKey");
if (!key) throw new Error("Failed to find workspace key");
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.GET_WORKSPACE_KEY,
metadata: {
keyId: key._id.toString()
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).json(key);
};
export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
const { workspaceId } = req.params;

View File

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

View File

@ -362,7 +362,8 @@ export const createSecret = async (req: Request, res: Response) => {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretPath = "/"
secretPath = "/",
metadata
} = req.body;
const secret = await SecretService.createSecret({
@ -380,7 +381,8 @@ export const createSecret = async (req: Request, res: Response) => {
secretPath,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
secretCommentTag,
metadata
});
await EventService.handleEvent({

View File

@ -12,7 +12,7 @@ import { standardRequest } from "../../config/request";
import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config";
import { BadRequestError } from "../../utils/errors";
import { TelemetryService } from "../../services";
import { AuthProvider } from "../../models";
import { AuthMethod } from "../../models";
/**
* Complete setting up user by adding their personal and auth information as part of the
@ -71,8 +71,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
if (providerAuthToken) {
await validateProviderAuthToken({
email,
providerAuthToken,
user,
providerAuthToken
});
} else {
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers["authorization"]?.split(" ", 2) ?? [null, null]
@ -117,7 +116,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
if (!user)
throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
if (user.authProvider !== AuthProvider.OKTA_SAML) {
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,

View File

@ -1,10 +1,12 @@
import { Request, Response } from "express";
import { Membership, Workspace } from "../../../models";
import { IUser, Membership, Workspace } from "../../../models";
import { EventType } from "../../../ee/models";
import { IMembershipPermission } from "../../../models/membership";
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
import { ADMIN, MEMBER } from "../../../variables/organization";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../../variables";
import _ from "lodash";
import { EEAuditLogService } from "../../services";
export const denyMembershipPermissions = async (req: Request, res: Response) => {
const { membershipId } = req.params;
@ -51,12 +53,33 @@ export const denyMembershipPermissions = async (req: Request, res: Response) =>
{ _id: membershipToModify._id },
{ $set: { deniedPermissions: sanitizedMembershipPermissionsUnique } },
{ new: true }
)
).populate<{ user: IUser }>("user");
if (!updatedMembershipWithPermissions) {
throw BadRequestError({ message: "The resource has been removed before it can be modified" })
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS,
metadata: {
userId: updatedMembershipWithPermissions.user._id.toString(),
email: updatedMembershipWithPermissions.user.email,
deniedPermissions: updatedMembershipWithPermissions.deniedPermissions.map(({
environmentSlug,
ability
}) => ({
environmentSlug,
ability
}))
}
},
{
workspaceId: updatedMembershipWithPermissions.workspace
}
);
res.send({
permissionsDenied: updatedMembershipWithPermissions.deniedPermissions,
})

View File

@ -1,3 +1,4 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import { getLicenseServerUrl } from "../../../config";
import { licenseServerKeyRequest } from "../../../config/request";
@ -20,7 +21,7 @@ export const getOrganizationPlan = async (req: Request, res: Response) => {
const { organizationId } = req.params;
const workspaceId = req.query.workspaceId as string;
const plan = await EELicenseService.getPlan(organizationId, workspaceId);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId), new Types.ObjectId(workspaceId));
return res.status(200).send({
plan,
@ -44,7 +45,7 @@ export const startOrganizationTrial = async (req: Request, res: Response) => {
}
);
EELicenseService.delPlan(organizationId);
EELicenseService.delPlan(new Types.ObjectId(organizationId));
return res.status(200).send({
url
@ -137,6 +138,12 @@ export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
});
}
/**
* Delete payment method with id [pmtMethodId] for organization
* @param req
* @param res
* @returns
*/
export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => {
const { pmtMethodId } = req.params;
@ -206,4 +213,18 @@ export const getOrganizationInvoices = async (req: Request, res: Response) => {
);
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 { data: { licenses } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/licenses`
);
return res.status(200).send(licenses);
}

View File

@ -3,6 +3,7 @@ import { Types } from "mongoose";
import { BotOrgService } from "../../../services";
import { SSOConfig } from "../../models";
import {
AuthMethod,
MembershipOrg,
User
} from "../../../models";
@ -21,7 +22,7 @@ import { EELicenseService } from "../../services";
*/
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()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`);
}
return res.redirect(`${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`);
@ -57,10 +58,9 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
entryPoint,
issuer,
cert,
audience
} = req.body;
const plan = await EELicenseService.getPlan(organizationId);
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."
@ -78,9 +78,6 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
encryptedCert?: string;
certIV?: string;
certTag?: string;
encryptedAudience?: string;
audienceIV?: string;
audienceTag?: string;
}
const update: PatchUpdate = {};
@ -132,18 +129,6 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
update.certIV = certIV;
update.certTag = certTag;
}
if (audience) {
const {
ciphertext: encryptedAudience,
iv: audienceIV,
tag: audienceTag
} = client.encryptSymmetric(audience, key);
update.encryptedAudience = encryptedAudience;
update.audienceIV = audienceIV;
update.audienceTag = audienceTag;
}
const ssoConfig = await SSOConfig.findOneAndUpdate(
{
@ -172,7 +157,7 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
}
},
{
authProvider: ssoConfig.authProvider
authMethods: [ssoConfig.authProvider],
}
);
} else {
@ -183,9 +168,7 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
}
},
{
$unset: {
authProvider: 1
}
authMethods: [AuthMethod.EMAIL],
}
);
}
@ -207,11 +190,10 @@ export const createSSOConfig = async (req: Request, res: Response) => {
isActive,
entryPoint,
issuer,
cert,
audience
cert
} = req.body;
const plan = await EELicenseService.getPlan(organizationId);
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."
@ -238,12 +220,6 @@ export const createSSOConfig = async (req: Request, res: Response) => {
iv: certIV,
tag: certTag
} = client.encryptSymmetric(cert, key);
const {
ciphertext: encryptedAudience,
iv: audienceIV,
tag: audienceTag
} = client.encryptSymmetric(audience, key);
const ssoConfig = await new SSOConfig({
organization: new Types.ObjectId(organizationId),
@ -257,10 +233,7 @@ export const createSSOConfig = async (req: Request, res: Response) => {
issuerTag,
encryptedCert,
certIV,
certTag,
encryptedAudience,
audienceIV,
audienceTag
certTag
}).save();
return res.status(200).send(ssoConfig);

View File

@ -8,6 +8,6 @@ import { Request, Response } from "express";
*/
export const getMyIp = (req: Request, res: Response) => {
return res.status(200).send({
ip: req.authData.authIP
ip: req.authData.ipAddress
});
}

View File

@ -1,21 +1,26 @@
import { Request, Response } from "express";
import { PipelineStage, Types } from "mongoose";
import { Secret } from "../../../models";
import { Membership, Secret, ServiceTokenData, User } from "../../../models";
import {
ActorType,
AuditLog,
EventType,
FolderVersion,
IPType,
ISecretVersion,
Log,
SecretSnapshot,
SecretVersion,
ServiceActor,
TFolderRootVersionSchema,
TrustedIP
TrustedIP,
UserActor
} from "../../models";
import { EESecretService } from "../../services";
import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
import Folder, { TFolderSchema } from "../../../models/folder";
import { searchByFolderId } from "../../../services/FolderService";
import { EELicenseService } from "../../services";
import { EEAuditLogService, EELicenseService } from "../../services";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
/**
@ -593,6 +598,101 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
});
};
/**
* Return audit logs for workspace with id [workspaceId]
* @param req
* @param res
*/
export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const eventType = req.query.eventType;
const userAgentType = req.query.userAgentType;
const actor = req.query.actor as string | undefined;
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
const startDate = req.query.startDate as string;
const endDate = req.query.endDate as string;
const query = {
workspace: new Types.ObjectId(workspaceId),
...(eventType ? {
"event.type": eventType
} : {}),
...(userAgentType ? {
userAgentType
} : {}),
...(actor ? {
"actor.type": actor.split("-", 2)[0],
...(actor.split("-", 2)[0] === ActorType.USER ? {
"actor.metadata.userId": actor.split("-", 2)[1]
} : {
"actor.metadata.serviceId": actor.split("-", 2)[1]
})
} : {}),
...(startDate || endDate ? {
createdAt: {
...(startDate && { $gte: new Date(startDate) }),
...(endDate && { $lte: new Date(endDate) })
}
} : {})
}
const auditLogs = await AuditLog.find(query)
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
const totalCount = await AuditLog.countDocuments(query);
return res.status(200).send({
auditLogs,
totalCount
});
}
/**
* Return audit log actor filter options for workspace with id [workspaceId]
* @param req
* @param res
*/
export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const userIds = await Membership.distinct("user", {
workspace: new Types.ObjectId(workspaceId)
});
const userActors: UserActor[] = (await User.find({
_id: {
$in: userIds
}
})
.select("email"))
.map((user) => ({
type: ActorType.USER,
metadata: {
userId: user._id.toString(),
email: user.email
}
}));
const serviceActors: ServiceActor[] = (await ServiceTokenData.find({
workspace: new Types.ObjectId(workspaceId)
})
.select("name"))
.map((serviceTokenData) => ({
type: ActorType.SERVICE,
metadata: {
serviceId: serviceTokenData._id.toString(),
name: serviceTokenData.name
}
}));
return res.status(200).send({
actors: [...userActors, ...serviceActors]
});
}
/**
* Return trusted ips for workspace with id [workspaceId]
* @param req
@ -623,7 +723,7 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => {
isActive
} = req.body;
const plan = await EELicenseService.getPlan(req.workspace.organization.toString());
const plan = await EELicenseService.getPlan(req.workspace.organization);
if (!plan.ipAllowlisting) return res.status(400).send({
message: "Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range."
@ -645,6 +745,21 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => {
isActive,
comment,
}).save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.ADD_TRUSTED_IP,
metadata: {
trustedIpId: trustedIp._id.toString(),
ipAddress: trustedIp.ipAddress,
prefix: trustedIp.prefix
}
},
{
workspaceId: trustedIp.workspace
}
);
return res.status(200).send({
trustedIp
@ -663,7 +778,7 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => {
comment
} = req.body;
const plan = await EELicenseService.getPlan(req.workspace.organization.toString());
const plan = await EELicenseService.getPlan(req.workspace.organization);
if (!plan.ipAllowlisting) return res.status(400).send({
message: "Failed to update IP access range due to plan restriction. Upgrade plan to update IP access range."
@ -708,6 +823,25 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => {
}
);
if (!trustedIp) return res.status(400).send({
message: "Failed to update trusted IP"
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_TRUSTED_IP,
metadata: {
trustedIpId: trustedIp._id.toString(),
ipAddress: trustedIp.ipAddress,
prefix: trustedIp.prefix
}
},
{
workspaceId: trustedIp.workspace
}
);
return res.status(200).send({
trustedIp
});
@ -721,7 +855,7 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => {
export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => {
const { workspaceId, trustedIpId } = req.params;
const plan = await EELicenseService.getPlan(req.workspace.organization.toString());
const plan = await EELicenseService.getPlan(req.workspace.organization);
if (!plan.ipAllowlisting) return res.status(400).send({
message: "Failed to delete IP access range due to plan restriction. Upgrade plan to delete IP access range."
@ -731,6 +865,25 @@ export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => {
_id: new Types.ObjectId(trustedIpId),
workspace: new Types.ObjectId(workspaceId)
});
if (!trustedIp) return res.status(400).send({
message: "Failed to delete trusted IP"
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_TRUSTED_IP,
metadata: {
trustedIpId: trustedIp._id.toString(),
ipAddress: trustedIp.ipAddress,
prefix: trustedIp.prefix
}
},
{
workspaceId: trustedIp.workspace
}
);
return res.status(200).send({
trustedIp

View File

@ -51,13 +51,6 @@ export const getSSOConfigHelper = async ({
ssoConfig.certIV,
ssoConfig.certTag
);
const audience = client.decryptSymmetric(
ssoConfig.encryptedAudience,
key,
ssoConfig.audienceIV,
ssoConfig.audienceTag
);
return ({
_id: ssoConfig._id,
@ -66,7 +59,6 @@ export const getSSOConfigHelper = async ({
isActive: ssoConfig.isActive,
entryPoint,
issuer,
cert,
audience
cert
});
}

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

@ -4,4 +4,8 @@ export * from "./folderVersion";
export * from "./log";
export * from "./action";
export * from "./ssoConfig";
export * from "./trustedIp";
export * from "./trustedIp";
export * from "./auditLog";
export * from "./gitRisks";
export * from "./gitAppOrganizationInstallation";
export * from "./gitAppInstallationSession";

View File

@ -117,7 +117,7 @@ const secretVersionSchema = new Schema<ISecretVersion>(
ref: "Tag",
type: [Schema.Types.ObjectId],
default: [],
},
}
},
{
timestamps: true,

View File

@ -1,8 +1,14 @@
import { Schema, Types, model } from "mongoose";
export enum AuthProvider {
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
}
export interface ISSOConfig {
organization: Types.ObjectId;
authProvider: "okta-saml"
authProvider: AuthProvider;
isActive: boolean;
encryptedEntryPoint: string;
entryPointIV: string;
@ -13,9 +19,6 @@ export interface ISSOConfig {
encryptedCert: string;
certIV: string;
certTag: string;
encryptedAudience: string;
audienceIV: string;
audienceTag: string;
}
const ssoConfigSchema = new Schema<ISSOConfig>(
@ -26,9 +29,7 @@ const ssoConfigSchema = new Schema<ISSOConfig>(
},
authProvider: {
type: String,
enum: [
"okta-saml"
],
enum: AuthProvider,
required: true
},
isActive: {
@ -61,15 +62,6 @@ const ssoConfigSchema = new Schema<ISSOConfig>(
},
certTag: {
type: String
},
encryptedAudience: {
type: String
},
audienceIV: {
type: String
},
audienceTag: {
type: String
}
},
{

View File

@ -6,11 +6,12 @@ import {
} from "../../../middleware";
import { query } from "express-validator";
import { cloudProductsController } from "../../controllers/v1";
import { AuthMode } from "../../../variables";
router.get(
"/",
requireAuth({
acceptedAuthModes: ["jwt", "apiKey"],
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
query("billing-cycle").exists().isIn(["monthly", "yearly"]),
validateRequest,

View File

@ -6,6 +6,7 @@ import users from "./users";
import workspace from "./workspace";
import action from "./action";
import cloudProducts from "./cloudProducts";
import secretScanning from "./secretScanning";
export {
secret,
@ -16,4 +17,5 @@ export {
workspace,
action,
cloudProducts,
secretScanning
}

View File

@ -8,13 +8,13 @@ import {
import { body, param, query } from "express-validator";
import { organizationsController } from "../../controllers/v1";
import {
ACCEPTED, ADMIN, MEMBER, OWNER,
ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER
} from "../../../variables";
router.get(
"/:organizationId/plans/table",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -29,7 +29,7 @@ router.get(
router.get(
"/:organizationId/plan",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -44,7 +44,7 @@ router.get(
router.post(
"/:organizationId/session/trial",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -59,7 +59,7 @@ router.post(
router.get(
"/:organizationId/plan/billing",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -74,7 +74,7 @@ router.get(
router.get(
"/:organizationId/plan/table",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -89,7 +89,7 @@ router.get(
router.get(
"/:organizationId/billing-details",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -103,7 +103,7 @@ router.get(
router.patch(
"/:organizationId/billing-details",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -119,7 +119,7 @@ router.patch(
router.get(
"/:organizationId/billing-details/payment-methods",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -133,7 +133,7 @@ router.get(
router.post(
"/:organizationId/billing-details/payment-methods",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -149,7 +149,7 @@ router.post(
router.delete(
"/:organizationId/billing-details/payment-methods/:pmtMethodId",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -164,7 +164,7 @@ router.delete(
router.get(
"/:organizationId/billing-details/tax-ids",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -178,7 +178,7 @@ router.get(
router.post(
"/:organizationId/billing-details/tax-ids",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -194,7 +194,7 @@ router.post(
router.delete(
"/:organizationId/billing-details/tax-ids/:taxId",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -209,7 +209,7 @@ router.delete(
router.get(
"/:organizationId/invoices",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -220,4 +220,18 @@ router.get(
organizationsController.getOrganizationInvoices
);
router.get(
"/:organizationId/licenses",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationLicenses
);
export default router;

View File

@ -9,15 +9,16 @@ import { body, param, query } from "express-validator";
import { secretController } from "../../controllers/v1";
import {
ADMIN,
AuthMode,
MEMBER,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
PERMISSION_WRITE_SECRETS
} from "../../../variables";
router.get(
"/:secretId/secret-versions",
requireAuth({
acceptedAuthModes: ["jwt", "apiKey"],
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -33,7 +34,7 @@ router.get(
router.post(
"/:secretId/secret-versions/rollback",
requireAuth({
acceptedAuthModes: ["jwt", "apiKey"],
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER],

View File

@ -4,15 +4,15 @@ import {
requireAuth,
requireOrganizationAuth,
validateRequest,
} from "../../middleware";
} from "../../../middleware";
import { body, param } from "express-validator";
import { createInstallationSession, getCurrentOrganizationInstallationStatus, getRisksForOrganization, linkInstallationToOrganization, updateRisksStatus } from "../../controllers/v1/secretScanningController";
import { ACCEPTED, ADMIN, MEMBER, OWNER } from "../../variables";
import { createInstallationSession, getCurrentOrganizationInstallationStatus, getRisksForOrganization, linkInstallationToOrganization, updateRisksStatus } from "../../../controllers/v1/secretScanningController";
import { ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER } from "../../../variables";
router.post(
"/create-installation-session/organization/:organizationId",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
param("organizationId").exists().trim(),
requireOrganizationAuth({
@ -26,7 +26,7 @@ router.post(
router.post(
"/link-installation",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
body("installationId").exists().trim(),
body("sessionId").exists().trim(),
@ -37,7 +37,7 @@ router.post(
router.get(
"/installation-status/organization/:organizationId",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
param("organizationId").exists().trim(),
requireOrganizationAuth({
@ -51,7 +51,7 @@ router.get(
router.get(
"/organization/:organizationId/risks",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
param("organizationId").exists().trim(),
requireOrganizationAuth({
@ -65,7 +65,7 @@ router.get(
router.post(
"/organization/:organizationId/risks/:riskId/status",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
param("organizationId").exists().trim(),
param("riskId").exists().trim(),

View File

@ -8,13 +8,13 @@ import {
validateRequest,
} from "../../../middleware";
import { param } from "express-validator";
import { ADMIN, MEMBER } from "../../../variables";
import { ADMIN, AuthMode, MEMBER } from "../../../variables";
import { secretSnapshotController } from "../../controllers/v1";
router.get(
"/:secretSnapshotId",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireSecretSnapshotAuth({
acceptedRoles: [ADMIN, MEMBER],

View File

@ -1,6 +1,9 @@
import express from "express";
const router = express.Router();
import passport from "passport";
import {
AuthProvider
} from "../../models";
import {
requireAuth,
requireOrganizationAuth,
@ -12,6 +15,7 @@ import { authLimiter } from "../../../helpers/rateLimiter";
import {
ACCEPTED,
ADMIN,
AuthMode,
OWNER
} from "../../../variables";
@ -38,6 +42,29 @@ router.get(
ssoController.redirectSSO
);
router.get(
"/redirect/github",
authLimiter,
(req, res, next) => {
passport.authenticate("github", {
session: false,
...(req.query.callback_port ? {
state: req.query.callback_port as string
} : {})
})(req, res, next);
}
);
router.get(
"/github",
authLimiter,
passport.authenticate("github", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get(
"/redirect/saml2/:ssoIdentifier",
authLimiter,
@ -64,7 +91,7 @@ router.post("/saml2/:ssoIdentifier",
router.get(
"/config",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
@ -79,7 +106,7 @@ router.get(
router.post(
"/config",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
@ -87,12 +114,11 @@ router.post(
locationOrganizationId: "body"
}),
body("organizationId").exists().trim(),
body("authProvider").exists().isString(),
body("authProvider").exists().isString().isIn([AuthProvider.OKTA_SAML]),
body("isActive").exists().isBoolean(),
body("entryPoint").exists().isString(),
body("issuer").exists().isString(),
body("cert").exists().isString(),
body("audience").exists().isString(),
validateRequest,
ssoController.createSSOConfig
);
@ -100,7 +126,7 @@ router.post(
router.patch(
"/config",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
@ -113,7 +139,6 @@ router.patch(
body("entryPoint").optional().isString(),
body("issuer").optional().isString(),
body("cert").optional().isString(),
body("audience").optional().isString(),
validateRequest,
ssoController.updateSSOConfig
);

View File

@ -3,13 +3,13 @@ const router = express.Router();
import {
requireAuth
} from "../../../middleware";
import { AUTH_MODE_API_KEY, AUTH_MODE_JWT } from "../../../variables";
import { AuthMode } from "../../../variables";
import { usersController } from "../../controllers/v1";
router.get(
"/me/ip",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
usersController.getMyIp
);

View File

@ -8,16 +8,16 @@ import {
import { body, param, query } from "express-validator";
import {
ADMIN,
AUTH_MODE_API_KEY,
AUTH_MODE_JWT,
AuthMode,
MEMBER
} from "../../../variables";
import { workspaceController } from "../../controllers/v1";
import { EventType, UserAgentType } from "../../models";
router.get(
"/:workspaceId/secret-snapshots",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -35,7 +35,7 @@ router.get(
router.get(
"/:workspaceId/secret-snapshots/count",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -51,7 +51,7 @@ router.get(
router.post(
"/:workspaceId/secret-snapshots/rollback",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -68,7 +68,7 @@ router.post(
router.get(
"/:workspaceId/logs",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -84,11 +84,46 @@ router.get(
workspaceController.getWorkspaceLogs
);
router.get(
"/:workspaceId/audit-logs",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
param("workspaceId").exists().trim(),
query("eventType").isString().isIn(Object.values(EventType)).optional({ nullable: true }),
query("userAgentType").isString().isIn(Object.values(UserAgentType)).optional({ nullable: true }),
query("actor").optional({ nullable: true }),
query("startDate").isISO8601().withMessage("Invalid start date format").optional({ nullable: true }),
query("endDate").isISO8601().withMessage("Invalid end date format").optional({ nullable: true }),
query("offset"),
query("limit"),
validateRequest,
workspaceController.getWorkspaceAuditLogs
);
router.get(
"/:workspaceId/audit-logs/filters/actors",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
param("workspaceId").exists().trim(),
validateRequest,
workspaceController.getWorkspaceAuditLogActorFilterOpts
);
router.get(
"/:workspaceId/trusted-ips",
param("workspaceId").exists().isString().trim(),
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -105,7 +140,7 @@ router.post(
body("isActive").exists().isBoolean(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
@ -122,7 +157,7 @@ router.patch(
body("comment").default("").isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
@ -137,7 +172,7 @@ router.delete(
param("trustedIpId").exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],

View File

@ -0,0 +1,50 @@
import { Types } from "mongoose";
import { AuditLog, Event } from "../models";
import { AuthData } from "../../interfaces/middleware";
import EELicenseService from "./EELicenseService";
import { Workspace } from "../../models";
import { OrganizationNotFoundError } from "../../utils/errors";
interface EventScope {
workspaceId?: Types.ObjectId;
organizationId?: Types.ObjectId;
}
type ValidEventScope =
| Required<Pick<EventScope, "workspaceId">>
| Required<Pick<EventScope, "organizationId">>
| Required<EventScope>
export default class EEAuditLogService {
static async createAuditLog(authData: AuthData, event: Event, eventScope: ValidEventScope, shouldSave = true) {
const MS_IN_DAY = 24 * 60 * 60 * 1000;
const organizationId = ("organizationId" in eventScope)
? eventScope.organizationId
: (await Workspace.findById(eventScope.workspaceId).select("organization").lean())?.organization;
if (!organizationId) throw OrganizationNotFoundError({
message: "createAuditLog: Failed to create audit log due to missing organizationId"
});
const ttl = (await EELicenseService.getPlan(organizationId)).auditLogsRetentionDays * MS_IN_DAY;
const auditLog = await new AuditLog({
actor: authData.actor,
organization: organizationId,
workspace: ("workspaceId" in eventScope) ? eventScope.workspaceId : undefined,
ipAddress: authData.ipAddress,
event,
userAgent: authData.userAgent,
userAgentType: authData.userAgentType,
expiresAt: new Date(Date.now() + ttl)
});
if (shouldSave) {
await auditLog.save();
}
return auditLog;
}
}

View File

@ -1,3 +1,4 @@
import { Types } from "mongoose";
import * as Sentry from "@sentry/node";
import NodeCache from "node-cache";
import {
@ -31,6 +32,7 @@ interface FeatureSet {
customRateLimits: boolean;
customAlerts: boolean;
auditLogs: boolean;
auditLogsRetentionDays: number;
samlSSO: boolean;
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
trial_end: number | null;
@ -63,9 +65,10 @@ class EELicenseService {
pitRecovery: false,
ipAllowlisting: false,
rbac: true,
customRateLimits: true,
customAlerts: true,
customRateLimits: false,
customAlerts: false,
auditLogs: false,
auditLogsRetentionDays: 0,
samlSSO: false,
status: null,
trial_end: null,
@ -81,10 +84,10 @@ class EELicenseService {
});
}
public async getPlan(organizationId: string, workspaceId?: string): Promise<FeatureSet> {
public async getPlan(organizationId: Types.ObjectId, workspaceId?: Types.ObjectId): Promise<FeatureSet> {
try {
if (this.instanceType === "cloud") {
const cachedPlan = this.localFeatureSet.get<FeatureSet>(`${organizationId}-${workspaceId ?? ""}`);
const cachedPlan = this.localFeatureSet.get<FeatureSet>(`${organizationId.toString()}-${workspaceId?.toString() ?? ""}`);
if (cachedPlan) {
return cachedPlan;
}
@ -101,7 +104,7 @@ class EELicenseService {
const { data: { currentPlan } } = await licenseServerKeyRequest.get(url);
// cache fetched plan for organization
this.localFeatureSet.set(`${organizationId}-${workspaceId ?? ""}`, currentPlan);
this.localFeatureSet.set(`${organizationId.toString()}-${workspaceId?.toString() ?? ""}`, currentPlan);
return currentPlan;
}
@ -112,16 +115,16 @@ class EELicenseService {
return this.globalFeatureSet;
}
public async refreshPlan(organizationId: string, workspaceId?: string) {
public async refreshPlan(organizationId: Types.ObjectId, workspaceId?: Types.ObjectId) {
if (this.instanceType === "cloud") {
this.localFeatureSet.del(`${organizationId}-${workspaceId ?? ""}`);
this.localFeatureSet.del(`${organizationId.toString()}-${workspaceId?.toString() ?? ""}`);
await this.getPlan(organizationId, workspaceId);
}
}
public async delPlan(organizationId: string) {
public async delPlan(organizationId: Types.ObjectId) {
if (this.instanceType === "cloud") {
this.localFeatureSet.del(`${organizationId}-`);
this.localFeatureSet.del(`${organizationId.toString()}-`);
}
}

View File

@ -10,7 +10,7 @@ import EELicenseService from "./EELicenseService";
/**
* Class to handle Enterprise Edition secret actions
*/
class EESecretService {
export default class EESecretService {
/**
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
* [workspaceId] under a new snapshot with incremented version under the
@ -71,5 +71,3 @@ class EESecretService {
});
}
}
export default EESecretService;

View File

@ -0,0 +1,45 @@
import { Probot } from "probot";
import GitRisks from "../../models/gitRisks";
import GitAppOrganizationInstallation from "../../models/gitAppOrganizationInstallation";
import { scanGithubPushEventForSecretLeaks } from "../../../queues/secret-scanning/githubScanPushEvent";
export default async (app: Probot) => {
app.on("installation.deleted", async (context) => {
const { payload } = context;
const { installation, repositories } = payload;
if (repositories) {
for (const repository of repositories) {
await GitRisks.deleteMany({ repositoryId: repository.id })
}
await GitAppOrganizationInstallation.deleteOne({ installationId: installation.id })
}
})
app.on("installation", async (context) => {
const { payload } = context;
payload.repositories
const { installation, repositories } = payload;
// TODO: start full repo scans
})
app.on("push", async (context) => {
const { payload } = context;
const { commits, repository, installation, pusher } = payload;
if (!commits || !repository || !installation || !pusher) {
return
}
const installationLinkToOrgExists = await GitAppOrganizationInstallation.findOne({ installationId: installation?.id }).lean()
if (!installationLinkToOrgExists) {
return
}
scanGithubPushEventForSecretLeaks({
commits: commits,
pusher: { name: pusher.name, email: pusher.email },
repository: { fullName: repository.full_name, id: repository.id },
organizationId: installationLinkToOrgExists.organizationId,
installationId: installation.id
})
});
};

View File

@ -0,0 +1,125 @@
import { exec } from "child_process";
import { mkdir, readFile, rm, writeFile } from "fs";
import { tmpdir } from "os";
import { join } from "path"
import { SecretMatch } from "./types";
import { Octokit } from "@octokit/rest";
export async function scanContentAndGetFindings(textContent: string): Promise<SecretMatch[]> {
const tempFolder = await createTempFolder();
const filePath = join(tempFolder, "content.txt");
const findingsPath = join(tempFolder, "findings.json");
try {
await writeTextToFile(filePath, textContent);
await runInfisicalScan(filePath, findingsPath);
const findingsData = await readFindingsFile(findingsPath);
return JSON.parse(findingsData);
} finally {
await deleteTempFolder(tempFolder);
}
}
export function createTempFolder(): Promise<string> {
return new Promise((resolve, reject) => {
const tempDir = tmpdir()
const tempFolderName = Math.random().toString(36).substring(2);
const tempFolderPath = join(tempDir, tempFolderName);
mkdir(tempFolderPath, (err: any) => {
if (err) {
reject(err);
} else {
resolve(tempFolderPath);
}
});
});
}
export function writeTextToFile(filePath: string, content: string): Promise<void> {
return new Promise((resolve, reject) => {
writeFile(filePath, content, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
export function runInfisicalScan(inputPath: string, outputPath: string): Promise<void> {
return new Promise((resolve, reject) => {
const command = `cat "${inputPath}" | infisical scan --exit-code=77 --pipe -r "${outputPath}"`;
exec(command, (error) => {
if (error && error.code != 77) {
reject(error);
} else {
resolve();
}
});
});
}
export function readFindingsFile(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
readFile(filePath, "utf8", (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
export function deleteTempFolder(folderPath: string): Promise<void> {
return new Promise((resolve, reject) => {
rm(folderPath, { recursive: true }, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
export function convertKeysToLowercase<T>(obj: T): T {
const convertedObj = {} as T;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const lowercaseKey = key.charAt(0).toLowerCase() + key.slice(1);
convertedObj[lowercaseKey as keyof T] = obj[key];
}
}
return convertedObj;
}
export async function getCommits(octokit: Octokit, owner: string, repo: string) {
let commits: { sha: string }[] = [];
let page = 1;
while (true) {
const response = await octokit.repos.listCommits({
owner,
repo,
per_page: 100,
page,
});
commits = commits.concat(response.data);
if (response.data.length == 0) break;
page++;
}
return commits;
}
export async function getFilesFromCommit(octokit: any, owner: string, repo: string, sha: string) {
const response = await octokit.repos.getCommit({
owner,
repo,
ref: sha,
});
}

View File

@ -0,0 +1,21 @@
export type SecretMatch = {
Description: string;
StartLine: number;
EndLine: number;
StartColumn: number;
EndColumn: number;
Match: string;
Secret: string;
File: string;
SymlinkFile: string;
Commit: string;
Entropy: number;
Author: string;
Email: string;
Date: string;
Message: string;
Tags: string[];
RuleID: string;
Fingerprint: string;
FingerPrintWithoutCommitId: string
};

View File

@ -1,9 +1,13 @@
import EELicenseService from "./EELicenseService";
import EESecretService from "./EESecretService";
import EELogService from "./EELogService";
import EEAuditLogService from "./EEAuditLogService";
import GithubSecretScanningService from "./GithubSecretScanning/GithubSecretScanningService"
export {
EELicenseService,
EESecretService,
EELogService,
EEAuditLogService,
GithubSecretScanningService
}

View File

@ -1,3 +1,4 @@
import { Request } from "express";
import { Types } from "mongoose";
import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
@ -5,7 +6,6 @@ import {
APIKeyData,
ITokenVersion,
IUser,
ServiceAccount,
ServiceTokenData,
TokenVersion,
User,
@ -14,7 +14,6 @@ import {
APIKeyDataNotFoundError,
AccountNotFoundError,
BadRequestError,
ServiceAccountNotFoundError,
ServiceTokenDataNotFoundError,
UnauthorizedRequestError,
} from "../utils/errors";
@ -26,11 +25,15 @@ import {
getJwtRefreshSecret,
} from "../config";
import {
AUTH_MODE_API_KEY,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AuthMode
} from "../variables";
import {
ServiceTokenAuthData,
UserAuthData
} from "../interfaces/middleware";
import { ActorType } from "../ee/models";
import { getUserAgentType } from "../utils/posthog";
/**
*
@ -42,7 +45,7 @@ export const validateAuthMode = ({
acceptedAuthModes,
}: {
headers: { [key: string]: string | string[] | undefined },
acceptedAuthModes: string[]
acceptedAuthModes: AuthMode[]
}) => {
const apiKey = headers["x-api-key"];
const authHeader = headers["authorization"];
@ -55,7 +58,7 @@ export const validateAuthMode = ({
if (typeof apiKey === "string") {
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
authMode = AUTH_MODE_API_KEY;
authMode = AuthMode.API_KEY;
authTokenValue = apiKey;
}
@ -71,13 +74,10 @@ export const validateAuthMode = ({
switch (tokenValue.split(".", 1)[0]) {
case "st":
authMode = AUTH_MODE_SERVICE_TOKEN;
break;
case "sa":
authMode = AUTH_MODE_SERVICE_ACCOUNT;
authMode = AuthMode.SERVICE_TOKEN;
break;
default:
authMode = AUTH_MODE_JWT;
authMode = AuthMode.JWT;
}
authTokenValue = tokenValue;
@ -100,10 +100,12 @@ export const validateAuthMode = ({
* @returns {User} user - user corresponding to JWT token
*/
export const getAuthUserPayload = async ({
req,
authTokenValue,
}: {
req: Request,
authTokenValue: string;
}) => {
}): Promise<UserAuthData> => {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, await getJwtAuthSecret())
);
@ -122,19 +124,33 @@ export const getAuthUserPayload = async ({
}, {
lastUsed: new Date(),
});
if (!tokenVersion) throw UnauthorizedRequestError({
message: "Failed to validate access token",
});
if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError({
message: "Failed to validate access token",
});
return ({
user,
tokenVersionId: tokenVersion._id,
});
return {
actor: {
type: ActorType.USER,
metadata: {
userId: user._id.toString(),
email: user.email
}
},
authPayload: user,
ipAddress: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgentType: getUserAgentType(req.headers["user-agent"])
}
// return ({
// user,
// tokenVersionId: tokenVersion._id, // what to do with this? // move this out
// });
}
/**
@ -144,14 +160,16 @@ export const getAuthUserPayload = async ({
* @returns {ServiceTokenData} serviceTokenData - service token data
*/
export const getAuthSTDPayload = async ({
req,
authTokenValue,
}: {
req: Request,
authTokenValue: string;
}) => {
}): Promise<ServiceTokenAuthData> => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3);
let serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt");
const serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt")
if (!serviceTokenData) {
throw ServiceTokenDataNotFoundError({ message: "Failed to find service token data" });
@ -168,7 +186,7 @@ export const getAuthSTDPayload = async ({
message: "Failed to authenticate service token",
});
serviceTokenData = await ServiceTokenData
const serviceTokenDataToReturn = await ServiceTokenData
.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER),
}, {
@ -176,40 +194,25 @@ export const getAuthSTDPayload = async ({
}, {
new: true,
})
.select("+encryptedKey +iv +tag");
.select("+encryptedKey +iv +tag")
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: "Failed to find service token data" });
if (!serviceTokenDataToReturn) throw ServiceTokenDataNotFoundError({ message: "Failed to find service token data" });
return serviceTokenData;
}
/**
* Return service account access key payload
* @param {Object} obj
* @param {String} obj.authTokenValue - service account access token value
* @returns {ServiceAccount} serviceAccount
*/
export const getAuthSAAKPayload = async ({
authTokenValue,
}: {
authTokenValue: string;
}) => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3);
const serviceAccount = await ServiceAccount.findById(
Buffer.from(TOKEN_IDENTIFIER, "base64").toString("hex")
).select("+secretHash");
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: "Failed to find service account" });
return {
actor: {
type: ActorType.SERVICE,
metadata: {
serviceId: serviceTokenDataToReturn._id.toString(),
name: serviceTokenDataToReturn.name
}
},
authPayload: serviceTokenDataToReturn,
ipAddress: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgentType: getUserAgentType(req.headers["user-agent"])
}
const result = await bcrypt.compare(TOKEN_SECRET, serviceAccount.secretHash);
if (!result) throw UnauthorizedRequestError({
message: "Failed to authenticate service account access key",
});
return serviceAccount;
// return serviceTokenDataToReturn;
}
/**
@ -219,10 +222,12 @@ export const getAuthSAAKPayload = async ({
* @returns {APIKeyData} apiKeyData - API key data
*/
export const getAuthAPIKeyPayload = async ({
req,
authTokenValue,
}: {
req: Request,
authTokenValue: string;
}) => {
}): Promise<UserAuthData> => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3);
let apiKeyData = await APIKeyData
@ -264,7 +269,19 @@ export const getAuthAPIKeyPayload = async ({
});
}
return user;
return {
actor: {
type: ActorType.USER,
metadata: {
userId: user._id.toString(),
email: user.email
}
},
authPayload: user,
ipAddress: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgentType: getUserAgentType(req.headers["user-agent"])
}
}
/**
@ -275,11 +292,11 @@ export const getAuthAPIKeyPayload = async ({
* @return {String} obj.token - issued JWT token
* @return {String} obj.refreshToken - issued refresh token
*/
export const issueAuthTokens = async ({
export const issueAuthTokens = async ({
userId,
ip,
userAgent,
}: {
}: {
userId: Types.ObjectId;
ip: string;
userAgent: string;
@ -292,7 +309,7 @@ export const issueAuthTokens = async ({
ip,
userAgent,
});
if (!tokenVersion) {
// case: no existing ip and user agent exists
// -> create new (session) token version for ip and user agent
@ -375,11 +392,9 @@ export const createToken = ({
export const validateProviderAuthToken = async ({
email,
user,
providerAuthToken,
}: {
email: string;
user: IUser,
providerAuthToken?: string;
}) => {
if (!providerAuthToken) {
@ -389,11 +404,8 @@ export const validateProviderAuthToken = async ({
const decodedToken = <jwt.ProviderAuthJwtPayload>(
jwt.verify(providerAuthToken, await getJwtProviderAuthSecret())
);
if (
decodedToken.authProvider !== user.authProvider ||
decodedToken.email !== email
) {
if (decodedToken.email !== email) {
throw new Error("Invalid authentication credentials.")
}
}
}

View File

@ -4,18 +4,20 @@ import {
decryptAsymmetric,
decryptSymmetric128BitHexKeyUTF8,
encryptSymmetric128BitHexKeyUTF8,
generateKeyPair,
generateKeyPair
} from "../utils/crypto";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8,
SECRET_SHARED,
SECRET_SHARED
} from "../variables";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { InternalServerError } from "../utils/errors";
import Folder from "../models/folder";
import { getFolderByPath } from "../services/FolderService";
import { getAllImportedSecrets } from "../services/SecretImportService";
import { expandSecrets } from "./secrets";
/**
* Create an inactive bot with name [name] for workspace with id [workspaceId]
@ -25,7 +27,7 @@ import { getFolderByPath } from "../services/FolderService";
*/
export const createBot = async ({
name,
workspaceId,
workspaceId
}: {
name: string;
workspaceId: Types.ObjectId;
@ -36,10 +38,7 @@ export const createBot = async ({
const { publicKey, privateKey } = generateKeyPair();
if (rootEncryptionKey) {
const { ciphertext, iv, tag } = client.encryptSymmetric(
privateKey,
rootEncryptionKey
);
const { ciphertext, iv, tag } = client.encryptSymmetric(privateKey, rootEncryptionKey);
return await new Bot({
name,
@ -50,12 +49,12 @@ export const createBot = async ({
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64,
keyEncoding: ENCODING_SCHEME_BASE64
}).save();
} else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey,
key: await getEncryptionKey(),
key: await getEncryptionKey()
});
return await new Bot({
@ -67,12 +66,12 @@ export const createBot = async ({
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
keyEncoding: ENCODING_SCHEME_UTF8
}).save();
}
throw InternalServerError({
message: "Failed to create new bot due to missing encryption key",
message: "Failed to create new bot due to missing encryption key"
});
};
@ -82,7 +81,7 @@ export const createBot = async ({
*/
export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
const botKey = await BotKey.exists({
workspace: workspaceId,
workspace: workspaceId
});
return botKey ? false : true;
@ -98,19 +97,19 @@ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
export const getSecretsBotHelper = async ({
workspaceId,
environment,
secretPath,
secretPath
}: {
workspaceId: Types.ObjectId;
environment: string;
secretPath: string;
}) => {
const content = {} as any;
const content: Record<string, { value: string; comment?: string }> = {};
const key = await getKey({ workspaceId: workspaceId });
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
environment
});
if (!folders && secretPath !== "/") {
@ -129,7 +128,43 @@ export const getSecretsBotHelper = async ({
workspace: workspaceId,
environment,
type: SECRET_SHARED,
folder: folderId,
folder: folderId
});
const importedSecrets = await getAllImportedSecrets(
workspaceId.toString(),
environment,
folderId
);
importedSecrets.forEach(({ secrets }) => {
secrets.forEach((secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
content[secretKey] = { value: secretValue };
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
const commentValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key
});
content[secretKey].comment = commentValue;
}
});
});
secrets.forEach((secret: ISecret) => {
@ -137,19 +172,31 @@ export const getSecretsBotHelper = async ({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key,
key
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key,
key
});
content[secretKey] = secretValue;
content[secretKey] = { value: secretValue };
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
const commentValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key
});
content[secretKey].comment = commentValue;
}
});
await expandSecrets(workspaceId.toString(), key, content);
return content;
};
@ -160,22 +207,18 @@ export const getSecretsBotHelper = async ({
* @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key
*/
export const getKey = async ({
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) => {
export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const botKey = await BotKey.findOne({
workspace: workspaceId,
workspace: workspaceId
}).populate<{ sender: IUser }>("sender", "publicKey");
if (!botKey) throw new Error("Failed to find bot key");
const bot = await Bot.findOne({
workspace: workspaceId,
workspace: workspaceId
}).select("+encryptedPrivateKey +iv +tag +algorithm +keyEncoding");
if (!bot) throw new Error("Failed to find bot");
@ -194,7 +237,7 @@ export const getKey = async ({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
privateKey: privateKeyBot
});
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
// case: encoding scheme is utf8
@ -202,20 +245,19 @@ export const getKey = async ({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: encryptionKey,
key: encryptionKey
});
return decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
privateKey: privateKeyBot
});
}
throw InternalServerError({
message:
"Failed to obtain bot's copy of workspace key needed for bot operations",
message: "Failed to obtain bot's copy of workspace key needed for bot operations"
});
};
@ -228,7 +270,7 @@ export const getKey = async ({
*/
export const encryptSymmetricHelper = async ({
workspaceId,
plaintext,
plaintext
}: {
workspaceId: Types.ObjectId;
plaintext: string;
@ -236,13 +278,13 @@ export const encryptSymmetricHelper = async ({
const key = await getKey({ workspaceId: workspaceId });
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext,
key,
key
});
return {
ciphertext,
iv,
tag,
tag
};
};
/**
@ -258,7 +300,7 @@ export const decryptSymmetricHelper = async ({
workspaceId,
ciphertext,
iv,
tag,
tag
}: {
workspaceId: Types.ObjectId;
ciphertext: string;
@ -270,7 +312,7 @@ export const decryptSymmetricHelper = async ({
ciphertext,
iv,
tag,
key,
key
});
return plaintext;
@ -281,24 +323,24 @@ export const decryptSymmetricHelper = async ({
* and [envionment] using bot
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment
* @param {String} obj.environment - environment
*/
export const getSecretsCommentBotHelper = async ({
workspaceId,
environment,
secretPath
} : {
}: {
workspaceId: Types.ObjectId;
environment: string;
secretPath: string;
}) => {
const content = {} as any;
const key = await getKey({ workspaceId: workspaceId });
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
environment
});
if (!folders && secretPath !== "/") {
@ -317,23 +359,23 @@ export const getSecretsCommentBotHelper = async ({
workspace: workspaceId,
environment,
type: SECRET_SHARED,
folder: folderId,
folder: folderId
});
secrets.forEach((secret: ISecret) => {
if(secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key,
key
});
const commentValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key,
key
});
content[secretKey] = commentValue;
@ -341,4 +383,4 @@ export const getSecretsCommentBotHelper = async ({
});
return content;
}
};

View File

@ -32,7 +32,7 @@ export const handleEventHelper = async ({ event }: { event: Event }) => {
switch (event.name) {
case EVENT_PUSH_SECRETS:
if (bot) {
await IntegrationService.syncIntegrations({
IntegrationService.syncIntegrations({
workspaceId,
environment
});

View File

@ -1,15 +1,15 @@
import { Types } from "mongoose";
import { Bot, Integration, IntegrationAuth } from "../models";
import { exchangeCode, exchangeRefresh, syncSecrets } from "../integrations";
import { Bot, IntegrationAuth } from "../models";
import { exchangeCode, exchangeRefresh } from "../integrations";
import { BotService } from "../services";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
INTEGRATION_NETLIFY,
INTEGRATION_VERCEL,
INTEGRATION_VERCEL
} from "../variables";
import { UnauthorizedRequestError } from "../utils/errors";
import * as Sentry from "@sentry/node";
import { syncSecretsToActiveIntegrationsQueue } from "../queues/integrations/syncSecretsToThirdPartyServices"
interface Update {
workspace: string;
@ -34,7 +34,7 @@ export const handleOAuthExchangeHelper = async ({
workspaceId,
integration,
code,
environment,
environment
}: {
workspaceId: string;
integration: string;
@ -43,21 +43,20 @@ export const handleOAuthExchangeHelper = async ({
}) => {
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true,
isActive: true
});
if (!bot)
throw new Error("Bot must be enabled for OAuth2 code-token exchange");
if (!bot) throw new Error("Bot must be enabled for OAuth2 code-token exchange");
// exchange code for access and refresh tokens
const res = await exchangeCode({
integration,
code,
code
});
const update: Update = {
workspace: workspaceId,
integration,
integration
};
switch (integration) {
@ -72,12 +71,12 @@ export const handleOAuthExchangeHelper = async ({
const integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
workspace: workspaceId,
integration,
integration
},
update,
{
new: true,
upsert: true,
upsert: true
}
);
@ -86,7 +85,7 @@ export const handleOAuthExchangeHelper = async ({
// set integration auth refresh token
await setIntegrationAuthRefreshHelper({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: res.refreshToken,
refreshToken: res.refreshToken
});
}
@ -97,81 +96,12 @@ export const handleOAuthExchangeHelper = async ({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt,
accessExpiresAt: res.accessExpiresAt
});
}
return integrationAuth;
};
/**
* Sync/push environment variables in workspace with id [workspaceId] to
* all active integrations for that workspace
* @param {Object} obj
* @param {Object} obj.workspaceId - id of workspace
*/
export const syncIntegrationsHelper = async ({
workspaceId,
environment,
}: {
workspaceId: Types.ObjectId;
environment?: string;
}) => {
try {
const integrations = await Integration.find({
workspace: workspaceId,
...(environment
? {
environment,
}
: {}),
isActive: true,
app: { $ne: null },
});
// for each workspace integration, sync/push secrets
// to that integration
for await (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({
workspaceId: integration.workspace,
environment: integration.environment,
secretPath: integration.secretPath,
});
// get workspace, environment (shared) secrets comments
const secretComments = await BotService.getSecretComments({
workspaceId: integration.workspace,
environment: integration.environment,
secretPath: integration.secretPath,
})
const integrationAuth = await IntegrationAuth.findById(
integration.integrationAuth
);
if (!integrationAuth) throw new Error("Failed to find integration auth");
// get integration auth access token
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth,
});
// sync secrets to integration
await syncSecrets({
integration,
integrationAuth,
secrets,
accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken,
secretComments
});
}
} catch (err) {
Sentry.captureException(err);
console.log(`syncIntegrationsHelper: failed with [workspaceId=${workspaceId}] [environment=${environment}]`, err) // eslint-disable-line no-use-before-define
throw err
}
};
/**
* Return decrypted refresh token using the bot's copy
@ -182,24 +112,24 @@ export const syncIntegrationsHelper = async ({
* @param {String} refreshToken - decrypted refresh token
*/
export const getIntegrationAuthRefreshHelper = async ({
integrationAuthId,
integrationAuthId
}: {
integrationAuthId: Types.ObjectId;
}) => {
const integrationAuth = await IntegrationAuth.findById(
integrationAuthId
).select("+refreshCiphertext +refreshIV +refreshTag");
const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select(
"+refreshCiphertext +refreshIV +refreshTag"
);
if (!integrationAuth)
throw UnauthorizedRequestError({
message: "Failed to locate Integration Authentication credentials",
message: "Failed to locate Integration Authentication credentials"
});
const refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string,
tag: integrationAuth.refreshTag as string
});
return refreshToken;
@ -214,28 +144,26 @@ export const getIntegrationAuthRefreshHelper = async ({
* @returns {String} accessToken - decrypted access token
*/
export const getIntegrationAuthAccessHelper = async ({
integrationAuthId,
integrationAuthId
}: {
integrationAuthId: Types.ObjectId;
}) => {
let accessId;
let accessToken;
const integrationAuth = await IntegrationAuth.findById(
integrationAuthId
).select(
const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select(
"workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag"
);
if (!integrationAuth)
throw UnauthorizedRequestError({
message: "Failed to locate Integration Authentication credentials",
message: "Failed to locate Integration Authentication credentials"
});
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string,
tag: integrationAuth.accessTag as string
});
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
@ -245,11 +173,11 @@ export const getIntegrationAuthAccessHelper = async ({
if (integrationAuth.accessExpiresAt < new Date()) {
// access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({
integrationAuthId,
integrationAuthId
});
accessToken = await exchangeRefresh({
integrationAuth,
refreshToken,
refreshToken
});
}
}
@ -263,13 +191,13 @@ export const getIntegrationAuthAccessHelper = async ({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessIdCiphertext as string,
iv: integrationAuth.accessIdIV as string,
tag: integrationAuth.accessIdTag as string,
tag: integrationAuth.accessIdTag as string
});
}
return {
accessId,
accessToken,
accessToken
};
};
@ -283,7 +211,7 @@ export const getIntegrationAuthAccessHelper = async ({
*/
export const setIntegrationAuthRefreshHelper = async ({
integrationAuthId,
refreshToken,
refreshToken
}: {
integrationAuthId: string;
refreshToken: string;
@ -294,22 +222,22 @@ export const setIntegrationAuthRefreshHelper = async ({
const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: refreshToken,
plaintext: refreshToken
});
integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
_id: integrationAuthId,
_id: integrationAuthId
},
{
refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv,
refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
keyEncoding: ENCODING_SCHEME_UTF8
},
{
new: true,
new: true
}
);
@ -329,7 +257,7 @@ export const setIntegrationAuthAccessHelper = async ({
integrationAuthId,
accessId,
accessToken,
accessExpiresAt,
accessExpiresAt
}: {
integrationAuthId: string;
accessId: string | null;
@ -342,20 +270,20 @@ export const setIntegrationAuthAccessHelper = async ({
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken,
plaintext: accessToken
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessId,
plaintext: accessId
});
}
integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
_id: integrationAuthId,
_id: integrationAuthId
},
{
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
@ -366,10 +294,10 @@ export const setIntegrationAuthAccessHelper = async ({
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
keyEncoding: ENCODING_SCHEME_UTF8
},
{
new: true,
new: true
}
);

View File

@ -115,5 +115,5 @@ export const updateSubscriptionOrgQuantity = async ({
);
}
await EELicenseService.refreshPlan(organizationId);
await EELicenseService.refreshPlan(new Types.ObjectId(organizationId));
};

View File

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

View File

@ -13,7 +13,7 @@ import {
SecretBlindIndexData,
ServiceTokenData
} from "../models";
import { SecretVersion } from "../ee/models";
import { EventType, SecretVersion } from "../ee/models";
import {
BadRequestError,
InternalServerError,
@ -29,6 +29,7 @@ import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8,
K8_USER_AGENT_NAME,
SECRET_PERSONAL,
SECRET_SHARED
} from "../variables";
@ -40,11 +41,12 @@ import {
} from "../utils/crypto";
import { TelemetryService } from "../services";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { EELogService, EESecretService } from "../ee/services";
import { EEAuditLogService, EELogService, EESecretService } from "../ee/services";
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth";
import { getFolderIdFromServiceToken } from "../services/FolderService";
import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService";
import picomatch from "picomatch";
import path from "path";
import Folder, { TFolderRootSchema } from "../models/folder";
export const isValidScope = (
authPayload: IServiceTokenData,
@ -64,10 +66,9 @@ export const isValidScope = (
export function containsGlobPatterns(secretPath: string) {
const globChars = ["*", "?", "[", "]", "{", "}", "**"];
const normalizedPath = path.normalize(secretPath);
return globChars.some(char => normalizedPath.includes(char));
return globChars.some((char) => normalizedPath.includes(char));
}
/**
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
*
@ -326,7 +327,8 @@ export const createSecretHelper = async ({
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretPath = "/"
secretPath = "/",
metadata
}: CreateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
@ -392,7 +394,8 @@ export const createSecretHelper = async ({
secretCommentTag,
folder: folderId,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
keyEncoding: ENCODING_SCHEME_UTF8,
metadata
}).save();
const secretVersion = new SecretVersion({
@ -433,10 +436,27 @@ export const createSecretHelper = async ({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
channel: authData.userAgentType,
ipAddress: authData.ipAddress
}));
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.CREATE_SECRET,
metadata: {
environment,
secretPath,
secretId: secret._id.toString(),
secretKey: secretName,
secretVersion: secret.version
}
},
{
workspaceId
}
);
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId,
@ -446,7 +466,7 @@ export const createSecretHelper = async ({
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
if (postHogClient && metadata?.source !== "signup") {
postHogClient.capture({
event: "secrets added",
distinctId: await TelemetryService.getDistinctId({
@ -457,8 +477,8 @@ export const createSecretHelper = async ({
environment,
workspaceId,
folderId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
@ -528,27 +548,54 @@ export const getSecretsHelper = async ({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
channel: authData.userAgentType,
ipAddress: authData.ipAddress
}));
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.GET_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secrets.length
}
},
{
workspaceId
}
);
const postHogClient = await TelemetryService.getPostHogClient();
// reduce the number of events captured
let shouldRecordK8Event = false
if (authData instanceof ServiceTokenData && authData.userAgent == K8_USER_AGENT_NAME) {
const randomNumber = Math.random();
if (randomNumber > 0.9) {
shouldRecordK8Event = true
}
}
if (postHogClient) {
postHogClient.capture({
event: "secrets pulled",
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
folderId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
}
});
const shouldCapture = authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event;
const approximateForNoneCapturedEvents = secrets.length * 10
if (shouldCapture) {
postHogClient.capture({
event: "secrets pulled",
distinctId: await TelemetryService.getDistinctId({ authData }),
properties: {
numberOfSecrets: shouldRecordK8Event ? approximateForNoneCapturedEvents : secrets.length,
environment,
workspaceId,
folderId,
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
}
return secrets;
@ -622,10 +669,27 @@ export const getSecretHelper = async ({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
channel: authData.userAgentType,
ipAddress: authData.ipAddress
}));
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.GET_SECRET,
metadata: {
environment,
secretPath,
secretId: secret._id.toString(),
secretKey: secretName,
secretVersion: secret.version
}
},
{
workspaceId
}
);
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
@ -639,8 +703,8 @@ export const getSecretHelper = async ({
environment,
workspaceId,
folderId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
@ -771,10 +835,27 @@ export const updateSecretHelper = async ({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
channel: authData.userAgentType,
ipAddress: authData.ipAddress
}));
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.UPDATE_SECRET,
metadata: {
environment,
secretPath,
secretId: secret._id.toString(),
secretKey: secretName,
secretVersion: secret.version
}
},
{
workspaceId
}
);
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId,
@ -795,8 +876,8 @@ export const updateSecretHelper = async ({
environment,
workspaceId,
folderId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
@ -841,14 +922,14 @@ export const deleteSecretHelper = async ({
if (type === SECRET_SHARED) {
secrets = await Secret.find({
secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId),
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId
}).lean();
secret = await Secret.findOneAndDelete({
secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId),
workspace: new Types.ObjectId(workspaceId),
environment,
type,
folder: folderId
@ -864,7 +945,7 @@ export const deleteSecretHelper = async ({
secret = await Secret.findOneAndDelete({
secretBlindIndex,
folder: folderId,
workspaceId: new Types.ObjectId(workspaceId),
workspace: new Types.ObjectId(workspaceId),
environment,
type,
...getAuthDataPayloadUserObj(authData)
@ -894,10 +975,27 @@ export const deleteSecretHelper = async ({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
channel: authData.userAgentType,
ipAddress: authData.ipAddress
}));
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.DELETE_SECRET,
metadata: {
environment,
secretPath,
secretId: secret._id.toString(),
secretKey: secretName,
secretVersion: secret.version
}
},
{
workspaceId
}
);
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId,
@ -918,8 +1016,8 @@ export const deleteSecretHelper = async ({
environment,
workspaceId,
folderId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
@ -929,3 +1027,165 @@ export const deleteSecretHelper = async ({
secret
};
};
const fetchSecretsCrossEnv = (workspaceId: string, folders: TFolderRootSchema[], key: string) => {
const fetchCache: Record<string, Record<string, string>> = {};
return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => {
const secRefPathUrl = path.join("/", ...secRefPath);
const uniqKey = `${secRefEnv}-${secRefPathUrl}`;
if (fetchCache?.[uniqKey]) {
return fetchCache[uniqKey][secRefKey];
}
let folderId = "root";
const folder = folders.find(({ environment }) => environment === secRefEnv);
if (!folder && secRefPathUrl !== "/") {
throw BadRequestError({ message: "Folder not found" });
}
if (folder) {
const selectedFolder = getFolderByPath(folder.nodes, secRefPathUrl);
if (!selectedFolder) {
throw BadRequestError({ message: "Folder not found" });
}
folderId = selectedFolder.id;
}
const secrets = await Secret.find({
workspace: workspaceId,
environment: secRefEnv,
type: SECRET_SHARED,
folder: folderId
});
const decryptedSec = secrets.reduce<Record<string, string>>((prev, secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
prev[secretKey] = secretValue;
return prev;
}, {});
fetchCache[uniqKey] = decryptedSec;
return fetchCache[uniqKey][secRefKey];
};
};
const INTERPOLATION_SYNTAX_REG = new RegExp(/\${([^}]+)}/g);
const recursivelyExpandSecret = async (
expandedSec: Record<string, string>,
interpolatedSec: Record<string, string>,
fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise<string>,
recursionChainBreaker: Record<string, boolean>,
key: string
) => {
if (expandedSec?.[key]) {
return expandedSec[key];
}
if (recursionChainBreaker?.[key]) {
return "";
}
recursionChainBreaker[key] = true;
let interpolatedValue = interpolatedSec[key];
if (!interpolatedValue) {
console.error(`Couldn't find referenced value - ${key}`);
return "";
}
const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG);
if (refs) {
for (const interpolationSyntax of refs) {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
if (entities.length === 1) {
const val = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
fetchCrossEnv,
recursionChainBreaker,
interpolationKey
);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
}
continue;
}
if (entities.length > 1) {
const secRefEnv = entities[0];
const secRefPath = entities.slice(1, entities.length - 1);
const secRefKey = entities[entities.length - 1];
const val = await fetchCrossEnv(secRefEnv, secRefPath, secRefKey);
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
}
}
}
expandedSec[key] = interpolatedValue;
return interpolatedValue;
};
// used to convert multi line ones to quotes ones with \n
const formatMultiValueEnv = (val?: string) => {
if (!val) return "";
if (!val.match("\n")) return val;
return `"${val.replace(/\n/g, "\\n")}"`;
};
export const expandSecrets = async (
workspaceId: string,
rootEncKey: string,
secrets: Record<string, { value: string; comment?: string }>
) => {
const expandedSec: Record<string, string> = {};
const interpolatedSec: Record<string, string> = {};
const folders = await Folder.find({ workspace: workspaceId });
const crossSecEnvFetch = fetchSecretsCrossEnv(workspaceId, folders, rootEncKey);
Object.keys(secrets).forEach((key) => {
if (secrets[key].value.match(INTERPOLATION_SYNTAX_REG)) {
interpolatedSec[key] = secrets[key].value;
} else {
expandedSec[key] = secrets[key].value;
}
});
for (const key of Object.keys(secrets)) {
if (expandedSec?.[key]) {
secrets[key].value = formatMultiValueEnv(expandedSec[key]);
continue;
}
// this is to avoid recursion loop. So the graph should be direct graph rather than cyclic
// so for any recursion building if there is an entity two times same key meaning it will be looped
const recursionChainBreaker: Record<string, boolean> = {};
const expandedVal = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
crossSecEnvFetch,
recursionChainBreaker,
key
);
secrets[key].value = formatMultiValueEnv(expandedVal);
}
return secrets;
};

View File

@ -1,3 +1,4 @@
import { Types } from "mongoose";
import {
Bot,
Key,
@ -25,7 +26,7 @@ export const createWorkspace = async ({
organizationId,
}: {
name: string;
organizationId: string;
organizationId: Types.ObjectId;
}) => {
// create workspace
const workspace = await new Workspace({

View File

@ -5,8 +5,8 @@ import express from "express";
require("express-async-errors");
import helmet from "helmet";
import cors from "cors";
import { DatabaseService, GithubSecretScanningService } from "./services";
import { EELicenseService } from "./ee/services";
import { DatabaseService } from "./services";
import { EELicenseService, GithubSecretScanningService } from "./ee/services";
import { setUpHealthEndpoint } from "./services/health";
import cookieParser from "cookie-parser";
import swaggerUi = require("swagger-ui-express");
@ -24,6 +24,7 @@ import {
secretSnapshot as eeSecretSnapshotRouter,
users as eeUsersRouter,
workspace as eeWorkspaceRouter,
secretScanning as v1SecretScanningRouter,
} from "./ee/routes/v1";
import {
auth as v1AuthRouter,
@ -38,7 +39,6 @@ import {
password as v1PasswordRouter,
secretImport as v1SecretImportRouter,
secret as v1SecretRouter,
secretScanning as v1SecretScanningRouter,
secretsFolder as v1SecretsFolder,
serviceToken as v1ServiceTokenRouter,
signup as v1SignupRouter,
@ -72,6 +72,8 @@ import { RouteNotFoundError } from "./utils/errors";
import { requestErrorHandler } from "./middleware/requestErrorHandler";
import { getNodeEnv, getPort, getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookProxy, getSecretScanningWebhookSecret, getSiteURL } from "./config";
import { setup } from "./utils/setup";
import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices";
import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent";
const SmeeClient = require('smee-client') // eslint-disable-line
const main = async () => {
@ -205,6 +207,8 @@ const main = async () => {
server.on("close", async () => {
await DatabaseService.closeDatabase();
syncSecretsToThirdPartyServices.close()
githubPushEventSecretScan.close()
});
return server;

View File

@ -35,6 +35,7 @@ import {
INTEGRATION_RENDER_API_URL,
INTEGRATION_SUPABASE,
INTEGRATION_SUPABASE_API_URL,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TERRAFORM_CLOUD_API_URL,
INTEGRATION_TRAVISCI,
@ -151,6 +152,12 @@ const getApps = async ({
accessToken,
});
break;
case INTEGRATION_TEAMCITY:
apps = await getAppsTeamCity({
integrationAuth,
accessToken,
});
break;
case INTEGRATION_SUPABASE:
apps = await getAppsSupabase({
accessToken,
@ -722,6 +729,39 @@ const getAppsGitlab = async ({
return apps;
};
/**
* Return list of projects for TeamCity integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for TeamCity API
* @returns {Object[]} apps - names and ids of TeamCity projects
* @returns {String} apps.name - name of TeamCity projects
*/
const getAppsTeamCity = async ({
integrationAuth,
accessToken,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
const res = (
await standardRequest.get(`${integrationAuth.url}/app/rest/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
})
).data.project.slice(1);
const apps = res.map((a: any) => {
return {
name: a.name,
appId: a.id,
};
});
return apps;
};
/**
* Return list of projects for Supabase integration
* @param {Object} obj

View File

@ -2,7 +2,6 @@ import { exchangeCode } from "./exchange";
import { exchangeRefresh } from "./refresh";
import { getApps } from "./apps";
import { getTeams } from "./teams";
import { syncSecrets } from "./sync";
import { revokeAccess } from "./revoke";
export {
@ -10,6 +9,5 @@ export {
exchangeRefresh,
getApps,
getTeams,
syncSecrets,
revokeAccess,
}

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,31 @@
import { Types } from "mongoose";
import {
IServiceAccount,
IServiceTokenData,
IUser,
} from "../../models";
import {
ServiceActor,
UserActor,
UserAgentType
} from "../../ee/models";
export interface AuthData {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
authChannel: string;
authIP: string;
authUserAgent: string;
interface BaseAuthData {
ipAddress: string;
userAgent: string;
userAgentType: UserAgentType;
tokenVersionId?: Types.ObjectId;
}
}
export interface UserAuthData extends BaseAuthData {
actor: UserActor;
authPayload: IUser;
}
export interface ServiceTokenAuthData extends BaseAuthData {
actor: ServiceActor;
authPayload: IServiceTokenData;
}
export type AuthData =
| UserAuthData
| ServiceTokenAuthData;

View File

@ -17,6 +17,9 @@ export interface CreateSecretParams {
secretCommentIV?: string;
secretCommentTag?: string;
secretPath: string;
metadata?: {
source?: string;
}
}
export interface GetSecretsParams {

View File

@ -1,25 +1,13 @@
import jwt from "jsonwebtoken";
import { Types } from "mongoose";
import { NextFunction, Request, Response } from "express";
import {
getAuthAPIKeyPayload,
getAuthSAAKPayload,
getAuthSTDPayload,
getAuthUserPayload,
validateAuthMode,
} from "../helpers/auth";
import {
IServiceAccount,
IServiceTokenData,
IUser,
} from "../models";
import {
AUTH_MODE_API_KEY,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
} from "../variables";
import { getChannelFromUserAgent } from "../utils/posthog";
import { AuthMode } from "../variables";
import { AuthData } from "../interfaces/middleware";
declare module "jsonwebtoken" {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -38,9 +26,9 @@ declare module "jsonwebtoken" {
* @returns
*/
const requireAuth = ({
acceptedAuthModes = [AUTH_MODE_JWT],
acceptedAuthModes = [AuthMode.JWT],
}: {
acceptedAuthModes: string[];
acceptedAuthModes: AuthMode[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
@ -50,55 +38,36 @@ const requireAuth = ({
headers: req.headers,
acceptedAuthModes,
});
let authPayload: IUser | IServiceAccount | IServiceTokenData;
let authUserPayload: {
user: IUser;
tokenVersionId: Types.ObjectId;
};
let authData: AuthData;
switch (authMode) {
case AUTH_MODE_SERVICE_ACCOUNT:
authPayload = await getAuthSAAKPayload({
case AuthMode.SERVICE_TOKEN:
authData = await getAuthSTDPayload({
req,
authTokenValue,
});
req.serviceAccount = authPayload;
req.serviceTokenData = authData.authPayload;
break;
case AUTH_MODE_SERVICE_TOKEN:
authPayload = await getAuthSTDPayload({
authTokenValue,
case AuthMode.API_KEY:
authData = await getAuthAPIKeyPayload({
req,
authTokenValue
});
req.serviceTokenData = authPayload;
req.user = authData.authPayload;
break;
case AUTH_MODE_API_KEY:
authPayload = await getAuthAPIKeyPayload({
authTokenValue,
case AuthMode.JWT:
authData = await getAuthUserPayload({
req,
authTokenValue
});
req.user = authPayload;
break;
default:
authUserPayload = await getAuthUserPayload({
authTokenValue,
});
authPayload = authUserPayload.user;
req.user = authUserPayload.user;
req.tokenVersionId = authUserPayload.tokenVersionId;
// authPayload = authUserPayload.user;
req.user = authData.authPayload;
// req.tokenVersionId = authUserPayload.tokenVersionId; // TODO
break;
}
req.requestData = {
...req.params,
...req.query,
...req.body,
}
req.authData = {
authMode,
authPayload, // User, ServiceAccount, ServiceTokenData
authChannel: getChannelFromUserAgent(req.headers["user-agent"]),
authIP: req.realIP,
authUserAgent: req.headers["user-agent"] ?? "other",
tokenVersionId: req.tokenVersionId,
}
req.authData = authData;
return next();
}

View File

@ -10,6 +10,8 @@ import Membership, { IMembership } from "./membership";
import MembershipOrg, { IMembershipOrg } from "./membershipOrg";
import Organization, { IOrganization } from "./organization";
import Secret, { ISecret } from "./secret";
import Folder, { TFolderRootSchema, TFolderSchema } from "./folder";
import SecretImport, { ISecretImports } from "./secretImports";
import SecretBlindIndexData, { ISecretBlindIndexData } from "./secretBlindIndexData";
import ServiceToken, { IServiceToken } from "./serviceToken";
import ServiceAccount, { IServiceAccount } from "./serviceAccount"; // new
@ -17,17 +19,16 @@ import ServiceAccountKey, { IServiceAccountKey } from "./serviceAccountKey"; //
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from "./serviceAccountOrganizationPermission"; // new
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from "./serviceAccountWorkspacePermission"; // new
import TokenData, { ITokenData } from "./tokenData";
import User, { AuthProvider, IUser } from "./user";
import User, { AuthMethod, IUser } from "./user";
import UserAction, { IUserAction } from "./userAction";
import Workspace, { IWorkspace } from "./workspace";
import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData";
import APIKeyData, { IAPIKeyData } from "./apiKeyData";
import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail";
import TokenVersion, { ITokenVersion } from "./tokenVersion";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE } from "./gitRisks";
export {
AuthProvider,
AuthMethod,
BackupPrivateKey,
IBackupPrivateKey,
Bot,
@ -52,6 +53,11 @@ export {
IOrganization,
Secret,
ISecret,
Folder,
TFolderRootSchema,
TFolderSchema,
SecretImport,
ISecretImports,
SecretBlindIndexData,
ISecretBlindIndexData,
ServiceToken,
@ -79,7 +85,5 @@ export {
LoginSRPDetail,
ILoginSRPDetail,
TokenVersion,
ITokenVersion,
GitRisks,
STATUS_RESOLVED_FALSE_POSITIVE
ITokenVersion
};

View File

@ -20,6 +20,7 @@ import {
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
@ -61,6 +62,7 @@ export interface IIntegration {
| "supabase"
| "checkly"
| "terraform-cloud"
| "teamcity"
| "hashicorp-vault"
| "cloudflare-pages"
| "bitbucket"
@ -157,6 +159,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TEAMCITY,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH,

View File

@ -22,6 +22,7 @@ import {
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
@ -55,6 +56,7 @@ export interface IIntegrationAuth extends Document {
| "bitbucket"
| "cloud-66"
| "terraform-cloud"
| "teamcity"
| "northflank"
| "windmill";
teamId: string;
@ -99,6 +101,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_CIRCLECI,
INTEGRATION_LARAVELFORGE,
INTEGRATION_TRAVISCI,
INTEGRATION_TEAMCITY,
INTEGRATION_SUPABASE,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT,

View File

@ -31,6 +31,9 @@ export interface ISecret {
keyEncoding: "utf8" | "base64";
tags?: string[];
folder?: string;
metadata?: {
[key: string]: string;
}
}
const secretSchema = new Schema<ISecret>(
@ -131,6 +134,9 @@ const secretSchema = new Schema<ISecret>(
type: String,
default: "root",
},
metadata: {
type: Schema.Types.Mixed
}
},
{
timestamps: true,

View File

@ -3,6 +3,7 @@ import { Schema, Types, model } from "mongoose";
export interface ITag {
_id: Types.ObjectId;
name: string;
tagColor: string;
slug: string;
user: Types.ObjectId;
workspace: Types.ObjectId;
@ -15,6 +16,11 @@ const tagSchema = new Schema<ITag>(
required: true,
trim: true,
},
tagColor: {
type: String,
required: false,
trim: true,
},
slug: {
type: String,
required: true,

View File

@ -1,15 +1,18 @@
import { Document, Schema, Types, model } from "mongoose";
export enum AuthProvider {
export enum AuthMethod {
EMAIL = "email",
GOOGLE = "google",
OKTA_SAML = "okta-saml"
GITHUB = "github",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml",
}
export interface IUser extends Document {
_id: Types.ObjectId;
authId?: string;
authProvider?: AuthProvider;
authProvider?: AuthMethod;
authMethods: AuthMethod[];
email: string;
firstName?: string;
lastName?: string;
@ -33,12 +36,17 @@ export interface IUser extends Document {
const userSchema = new Schema<IUser>(
{
authId: {
authProvider: { // TODO field: deprecate
type: String,
enum: AuthMethod,
},
authProvider: {
type: String,
enum: AuthProvider,
authMethods: {
type: [{
type: String,
enum: AuthMethod,
}],
default: [AuthMethod.EMAIL],
required: true
},
email: {
type: String,

View File

@ -0,0 +1,76 @@
import Queue, { Job } from "bull";
import Integration from "../../models/integration";
import IntegrationAuth from "../../models/integrationAuth";
import { BotService } from "../../services";
import { getIntegrationAuthAccessHelper } from "../../helpers";
import { syncSecrets } from "../../integrations/sync"
type TSyncSecretsToThirdPartyServices = {
workspaceId: string
environment?: string
}
export const syncSecretsToThirdPartyServices = new Queue("sync-secrets-to-third-party-services", process.env.REDIS_URL as string);
syncSecretsToThirdPartyServices.process(async (job: Job) => {
const { workspaceId, environment }: TSyncSecretsToThirdPartyServices = job.data
const integrations = await Integration.find({
workspace: workspaceId,
...(environment
? {
environment
}
: {}),
isActive: true,
app: { $ne: null }
});
// for each workspace integration, sync/push secrets
// to that integration
for (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({
workspaceId: integration.workspace,
environment: integration.environment,
secretPath: integration.secretPath
});
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
if (!integrationAuth) throw new Error("Failed to find integration auth");
// get integration auth access token
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth
});
// sync secrets to integration
await syncSecrets({
integration,
integrationAuth,
secrets,
accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken
});
}
})
syncSecretsToThirdPartyServices.on("error", (error) => {
console.log("QUEUE ERROR:", error) // eslint-disable-line
})
export const syncSecretsToActiveIntegrationsQueue = (jobDetails: TSyncSecretsToThirdPartyServices) => {
syncSecretsToThirdPartyServices.add(jobDetails, {
attempts: 5,
backoff: {
type: "exponential",
delay: 3000
},
removeOnComplete: true,
removeOnFail: {
count: 20 // keep the most recent 20 jobs
}
})
}

View File

@ -0,0 +1,201 @@
// import Queue, { Job } from "bull";
// import { ProbotOctokit } from "probot"
// import { Commit, Committer, Repository } from "@octokit/webhooks-types";
// import TelemetryService from "../../services/TelemetryService";
// import { sendMail } from "../../helpers";
// import GitRisks from "../../ee/models/gitRisks";
// import { MembershipOrg, User } from "../../models";
// import { OWNER, ADMIN } from "../../variables";
// import { convertKeysToLowercase, getFilesFromCommit, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper";
// import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
// const githubFullRepositoryScan = new Queue('github-historical-secret-scanning', 'redis://redis:6379');
// type TScanFullRepositoryDetails = {
// organizationId: string,
// repositories: {
// id: number;
// node_id: string;
// name: string;
// full_name: string;
// private: boolean;
// }[] | undefined
// installationId: number
// }
// type SecretMatch = {
// Description: string;
// StartLine: number;
// EndLine: number;
// StartColumn: number;
// EndColumn: number;
// Match: string;
// Secret: string;
// File: string;
// SymlinkFile: string;
// Commit: string;
// Entropy: number;
// Author: string;
// Email: string;
// Date: string;
// Message: string;
// Tags: string[];
// RuleID: string;
// Fingerprint: string;
// FingerPrintWithoutCommitId: string
// };
// type Helllo = {
// url: string;
// sha: string;
// node_id: string;
// html_url: string;
// comments_url: string;
// commit: {
// url: string;
// author: {
// name?: string | undefined;
// email?: string | undefined;
// date?: string | undefined;
// } | null;
// verification?: {
// } | undefined;
// };
// files?: {}[] | undefined;
// }[]
// githubFullRepositoryScan.process(async (job: Job, done: Queue.DoneCallback) => {
// const { organizationId, repositories, installationId }: TScanFullRepositoryDetails = job.data
// const repositoryFullNamesList = repositories ? repositories.map(repoDetails => repoDetails.full_name) : []
// const octokit = new ProbotOctokit({
// auth: {
// appId: await getSecretScanningGitAppId(),
// privateKey: await getSecretScanningPrivateKey(),
// installationId: installationId
// },
// });
// for (const repositoryFullName of repositoryFullNamesList) {
// const [owner, repo] = repositoryFullName.split("/");
// let page = 1;
// while (true) {
// // octokit.repos.getco
// const { data } = await octokit.repos.listCommits({
// owner,
// repo,
// per_page: 100,
// page
// });
// await getFilesFromCommit(octokit, owner, repo, "646b386605177ed0a2cc0a596eeee0cf57666342")
// page++;
// }
// }
// done()
// // const allFindingsByFingerprint: { [key: string]: SecretMatch; } = {}
// // for (const commit of commits) {
// // for (const filepath of [...commit.added, ...commit.modified]) {
// // try {
// // const fileContentsResponse = await octokit.repos.getContent({
// // owner,
// // repo,
// // path: filepath,
// // });
// // const data: any = fileContentsResponse.data;
// // const fileContent = Buffer.from(data.content, "base64").toString();
// // const findings = await scanContentAndGetFindings(`\n${fileContent}`) // extra line to count lines correctly
// // for (const finding of findings) {
// // const fingerPrintWithCommitId = `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`
// // const fingerPrintWithoutCommitId = `${filepath}:${finding.RuleID}:${finding.StartLine}`
// // finding.Fingerprint = fingerPrintWithCommitId
// // finding.FingerPrintWithoutCommitId = fingerPrintWithoutCommitId
// // finding.Commit = commit.id
// // finding.File = filepath
// // finding.Author = commit.author.name
// // finding.Email = commit?.author?.email ? commit?.author?.email : ""
// // allFindingsByFingerprint[fingerPrintWithCommitId] = finding
// // }
// // } catch (error) {
// // done(new Error(`gitHubHistoricalScanning.process: unable to fetch content for [filepath=${filepath}] because [error=${error}]`), null)
// // }
// // }
// // }
// // // change to update
// // for (const key in allFindingsByFingerprint) {
// // await GitRisks.findOneAndUpdate({ fingerprint: allFindingsByFingerprint[key].Fingerprint },
// // {
// // ...convertKeysToLowercase(allFindingsByFingerprint[key]),
// // installationId: installationId,
// // organization: organizationId,
// // repositoryFullName: repository.fullName,
// // repositoryId: repository.id
// // }, {
// // upsert: true
// // }).lean()
// // }
// // // get emails of admins
// // const adminsOfWork = await MembershipOrg.find({
// // organization: organizationId,
// // $or: [
// // { role: OWNER },
// // { role: ADMIN }
// // ]
// // }).lean()
// // const userEmails = await User.find({
// // _id: {
// // $in: [adminsOfWork.map(orgMembership => orgMembership.user)]
// // }
// // }).select("email").lean()
// // const adminOrOwnerEmails = userEmails.map(userObject => userObject.email)
// // const usersToNotify = pusher?.email ? [pusher.email, ...adminOrOwnerEmails] : [...adminOrOwnerEmails]
// // if (Object.keys(allFindingsByFingerprint).length) {
// // await sendMail({
// // template: "secretLeakIncident.handlebars",
// // subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`,
// // recipients: usersToNotify,
// // substitutions: {
// // numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
// // pusher_email: pusher.email,
// // pusher_name: pusher.name
// // }
// // });
// // }
// // const postHogClient = await TelemetryService.getPostHogClient();
// // if (postHogClient) {
// // postHogClient.capture({
// // event: "cloud secret scan",
// // distinctId: pusher.email,
// // properties: {
// // numberOfCommitsScanned: commits.length,
// // numberOfRisksFound: Object.keys(allFindingsByFingerprint).length,
// // }
// // });
// // }
// // done(null, allFindingsByFingerprint)
// })
// export const scanGithubFullRepositoryForSecretLeaks = (scanFullRepositoryDetails: TScanFullRepositoryDetails) => {
// console.log("full repo scan started")
// githubFullRepositoryScan.add(scanFullRepositoryDetails)
// }

View File

@ -0,0 +1,148 @@
import Queue, { Job } from "bull";
import { ProbotOctokit } from "probot"
import { Commit, Committer, Repository } from "@octokit/webhooks-types";
import TelemetryService from "../../services/TelemetryService";
import { sendMail } from "../../helpers";
import GitRisks from "../../ee/models/gitRisks";
import { MembershipOrg, User } from "../../models";
import { OWNER, ADMIN } from "../../variables";
import { convertKeysToLowercase, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper";
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
import { SecretMatch } from "../../ee/services/GithubSecretScanning/types";
export const githubPushEventSecretScan = new Queue('github-push-event-secret-scanning', 'redis://redis:6379');
type TScanPushEventQueueDetails = {
organizationId: string,
commits: Commit[]
pusher: {
name: string,
email: string | null
},
repository: {
id: number,
fullName: string,
},
installationId: number
}
githubPushEventSecretScan.process(async (job: Job, done: Queue.DoneCallback) => {
const { organizationId, commits, pusher, repository, installationId }: TScanPushEventQueueDetails = job.data
const [owner, repo] = repository.fullName.split("/");
const octokit = new ProbotOctokit({
auth: {
appId: await getSecretScanningGitAppId(),
privateKey: await getSecretScanningPrivateKey(),
installationId: installationId
},
});
const allFindingsByFingerprint: { [key: string]: SecretMatch; } = {}
for (const commit of commits) {
for (const filepath of [...commit.added, ...commit.modified]) {
try {
const fileContentsResponse = await octokit.repos.getContent({
owner,
repo,
path: filepath,
});
const data: any = fileContentsResponse.data;
const fileContent = Buffer.from(data.content, "base64").toString();
const findings = await scanContentAndGetFindings(`\n${fileContent}`) // extra line to count lines correctly
for (const finding of findings) {
const fingerPrintWithCommitId = `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`
const fingerPrintWithoutCommitId = `${filepath}:${finding.RuleID}:${finding.StartLine}`
finding.Fingerprint = fingerPrintWithCommitId
finding.FingerPrintWithoutCommitId = fingerPrintWithoutCommitId
finding.Commit = commit.id
finding.File = filepath
finding.Author = commit.author.name
finding.Email = commit?.author?.email ? commit?.author?.email : ""
allFindingsByFingerprint[fingerPrintWithCommitId] = finding
}
} catch (error) {
done(new Error(`gitHubHistoricalScanning.process: unable to fetch content for [filepath=${filepath}] because [error=${error}]`), null)
}
}
}
// change to update
for (const key in allFindingsByFingerprint) {
await GitRisks.findOneAndUpdate({ fingerprint: allFindingsByFingerprint[key].Fingerprint },
{
...convertKeysToLowercase(allFindingsByFingerprint[key]),
installationId: installationId,
organization: organizationId,
repositoryFullName: repository.fullName,
repositoryId: repository.id
}, {
upsert: true
}).lean()
}
// get emails of admins
const adminsOfWork = await MembershipOrg.find({
organization: organizationId,
$or: [
{ role: OWNER },
{ role: ADMIN }
]
}).lean()
const userEmails = await User.find({
_id: {
$in: [adminsOfWork.map(orgMembership => orgMembership.user)]
}
}).select("email").lean()
const adminOrOwnerEmails = userEmails.map(userObject => userObject.email)
const usersToNotify = pusher?.email ? [pusher.email, ...adminOrOwnerEmails] : [...adminOrOwnerEmails]
if (Object.keys(allFindingsByFingerprint).length) {
await sendMail({
template: "secretLeakIncident.handlebars",
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`,
recipients: usersToNotify,
substitutions: {
numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
pusher_email: pusher.email,
pusher_name: pusher.name
}
});
}
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "cloud secret scan",
distinctId: pusher.email,
properties: {
numberOfCommitsScanned: commits.length,
numberOfRisksFound: Object.keys(allFindingsByFingerprint).length,
}
});
}
done(null, allFindingsByFingerprint)
})
export const scanGithubPushEventForSecretLeaks = (pushEventPayload: TScanPushEventQueueDetails) => {
githubPushEventSecretScan.add(pushEventPayload, {
attempts: 3,
backoff: {
type: "exponential",
delay: 5000
},
removeOnComplete: true,
removeOnFail: {
count: 20 // keep the most recent 20 jobs
}
})
}

View File

@ -1,16 +1,26 @@
import express, { Request, Response } from "express";
import { getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookSecret, getSmtpConfigured } from "../../config";
import { getInviteOnlySignup, getRedisUrl, getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookSecret, getSmtpConfigured } from "../../config";
const router = express.Router();
router.get(
"/status",
async (req: Request, res: Response) => {
const gitAppId = await getSecretScanningGitAppId()
const gitSecretScanningWebhookSecret = await getSecretScanningWebhookSecret()
const gitSecretScanningPrivateKey = await getSecretScanningPrivateKey()
let secretScanningConfigured = false
if (gitAppId && gitSecretScanningPrivateKey && gitSecretScanningWebhookSecret) {
secretScanningConfigured = true
}
res.status(200).json({
date: new Date(),
message: "Ok",
emailConfigured: await getSmtpConfigured(),
secretScanningConfigured: await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey(),
inviteOnlySignup: await getInviteOnlySignup(),
redisConfigured: await getRedisUrl() !== "" && await getRedisUrl() !== undefined,
secretScanningConfigured: secretScanningConfigured,
})
}
);

View File

@ -4,23 +4,23 @@ import { body } from "express-validator";
import { requireAuth, validateRequest } from "../../middleware";
import { authController } from "../../controllers/v1";
import { authLimiter } from "../../helpers/rateLimiter";
import { AUTH_MODE_JWT } from "../../variables";
import { AuthMode } from "../../variables";
router.post("/token", validateRequest, authController.getNewToken);
router.post( // deprecated (moved to api/v2/auth/login1)
router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login1)
"/login1",
authLimiter,
body("email").exists().trim().notEmpty(),
body("email").exists().trim().notEmpty().toLowerCase(),
body("clientPublicKey").exists().trim().notEmpty(),
validateRequest,
authController.login1
);
router.post( // deprecated (moved to api/v2/auth/login2)
router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login2)
"/login2",
authLimiter,
body("email").exists().trim().notEmpty(),
body("email").exists().trim().notEmpty().toLowerCase(),
body("clientProof").exists().trim().notEmpty(),
validateRequest,
authController.login2
@ -30,7 +30,7 @@ router.post(
"/logout",
authLimiter,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
authController.logout
);
@ -38,7 +38,7 @@ router.post(
router.post(
"/checkAuth",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
authController.checkAuth
);
@ -49,13 +49,13 @@ router.get(
authController.getCommonPasswords
);
router.delete(
router.delete( // TODO endpoint: deprecate (moved to DELETE v2/users/me/sessions)
"/sessions",
authLimiter,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
authController.revokeAllSessions
);
export default router;
export default router;

View File

@ -8,12 +8,12 @@ import {
validateRequest,
} from "../../middleware";
import { botController } from "../../controllers/v1";
import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables";
import { ADMIN, AuthMode, MEMBER } from "../../variables";
router.get(
"/:workspaceId",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -27,7 +27,7 @@ router.get(
router.patch(
"/:botId/active",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireBotAuth({
acceptedRoles: [ADMIN, MEMBER],

View File

@ -15,7 +15,6 @@ import password from "./password";
import integration from "./integration";
import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder";
import secretScanning from "./secretScanning";
import webhooks from "./webhook";
import secretImport from "./secretImport";
@ -37,7 +36,6 @@ export {
integration,
integrationAuth,
secretsFolder,
secretScanning,
webhooks,
secretImport
};

View File

@ -4,13 +4,13 @@ import {
requireAuth,
requireIntegrationAuth,
requireIntegrationAuthorizationAuth,
requireWorkspaceAuth,
validateRequest,
} from "../../middleware";
import {
ADMIN,
AUTH_MODE_API_KEY,
AUTH_MODE_JWT,
MEMBER,
AuthMode,
MEMBER
} from "../../variables";
import { body, param } from "express-validator";
import { integrationController } from "../../controllers/v1";
@ -18,7 +18,7 @@ import { integrationController } from "../../controllers/v1";
router.post(
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -44,7 +44,7 @@ router.post(
router.patch(
"/:integrationId",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT]
}),
requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -64,7 +64,7 @@ router.patch(
router.delete(
"/:integrationId",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT]
}),
requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -74,4 +74,19 @@ router.delete(
integrationController.deleteIntegration
);
router.post(
"/manual-sync",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body",
}),
body("environment").isString().exists().trim(),
body("workspaceId").exists().trim(),
validateRequest,
integrationController.manualSync
);
export default router;

View File

@ -9,16 +9,15 @@ import {
} from "../../middleware";
import {
ADMIN,
AUTH_MODE_API_KEY,
AUTH_MODE_JWT,
MEMBER,
AuthMode,
MEMBER
} from "../../variables";
import { integrationAuthController } from "../../controllers/v1";
router.get(
"/integration-options",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
integrationAuthController.getIntegrationOptions
);
@ -26,7 +25,7 @@ router.get(
router.get(
"/:integrationAuthId",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -39,7 +38,7 @@ router.get(
router.post(
"/oauth-token",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -62,7 +61,7 @@ router.post(
body("integration").exists().trim().notEmpty(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -74,7 +73,7 @@ router.post(
router.get(
"/:integrationAuthId/apps",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -89,7 +88,7 @@ router.get(
router.get(
"/:integrationAuthId/teams",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -102,7 +101,7 @@ router.get(
router.get(
"/:integrationAuthId/vercel/branches",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -117,7 +116,7 @@ router.get(
router.get(
"/:integrationAuthId/railway/environments",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -131,7 +130,7 @@ router.get(
router.get(
"/:integrationAuthId/railway/services",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AuthMode.JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -145,7 +144,7 @@ router.get(
router.get(
"/:integrationAuthId/bitbucket/workspaces",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -158,7 +157,7 @@ router.get(
router.get(
"/:integrationAuthId/northflank/secret-groups",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -172,7 +171,7 @@ router.get(
router.delete(
"/:integrationAuthId",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],

View File

@ -3,12 +3,14 @@ const router = express.Router();
import { body } from "express-validator";
import { requireAuth, validateRequest } from "../../middleware";
import { membershipOrgController } from "../../controllers/v1";
import { AUTH_MODE_JWT } from "../../variables";
import { AuthMode } from "../../variables";
// TODO endpoint: consider moving these endpoints to be under /organization to be more RESTful
router.post(
"/signup",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
body("inviteeEmail").exists().trim().notEmpty().isEmail(),
body("organizationId").exists().trim().notEmpty(),

View File

@ -6,13 +6,15 @@ import {
validateRequest,
} from "../../middleware";
import { body, param } from "express-validator";
import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables";
import { ADMIN, AuthMode, MEMBER } from "../../variables";
import { keyController } from "../../controllers/v1";
// TODO endpoint: consider moving these endpoints to be under /workspaces to be more RESTful
router.post(
"/:workspaceId",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -24,10 +26,10 @@ router.post(
keyController.uploadKey
);
router.get(
router.get( // TODO endpoint: deprecate (note: move frontend to v2/workspace/key or something)
"/:workspaceId/latest",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],

View File

@ -4,44 +4,45 @@ import { body, param } from "express-validator";
import { requireAuth, validateRequest } from "../../middleware";
import { membershipController } from "../../controllers/v1";
import { membershipController as EEMembershipControllers } from "../../ee/controllers/v1";
import { AUTH_MODE_JWT } from "../../variables";
import { AuthMode } from "../../variables";
// note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId)
// TODO endpoint: consider moving these endpoints to be under /workspace to be more RESTful
router.get( // used for old CLI (deprecate)
router.get( // TODO endpoint: deprecate - used for old CLI (deprecate)
"/:workspaceId/connect",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
param("workspaceId").exists().trim(),
validateRequest,
membershipController.validateMembership
);
router.delete(
router.delete( // TODO endpoint: check dashboard
"/:membershipId",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
param("membershipId").exists().trim(),
validateRequest,
membershipController.deleteMembership
);
router.post(
router.post( // TODO endpoint: check dashboard
"/:membershipId/change-role",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
body("role").exists().trim(),
validateRequest,
membershipController.changeMembershipRole
);
router.post(
router.post( // TODO endpoint: check dashboard
"/:membershipId/deny-permissions",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
param("membershipId").isMongoId().exists().trim(),
body("permissions").isArray().exists(),

View File

@ -3,13 +3,12 @@ const router = express.Router();
import { param } from "express-validator";
import { requireAuth, validateRequest } from "../../middleware";
import { membershipOrgController } from "../../controllers/v1";
import { AUTH_MODE_JWT } from "../../variables";
import { AuthMode } from "../../variables";
router.post(
// TODO
router.post( // TODO endpoint: check dashboard
"/membershipOrg/:membershipOrgId/change-role",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
param("membershipOrgId"),
validateRequest,
@ -17,9 +16,9 @@ router.post(
);
router.delete(
"/:membershipOrgId",
"/:membershipOrgId", // TODO endpoint: check dashboard
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
acceptedAuthModes: [AuthMode.JWT],
}),
param("membershipOrgId").exists().trim(),
validateRequest,

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