1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-04-17 19:37:38 +00:00

Compare commits

..

213 Commits

Author SHA1 Message Date
f79e1d754d update prompt selections to const 2023-04-20 14:08:35 -07:00
5a906d412b show help for update and user sub command 2023-04-20 14:07:35 -07:00
1bb3115880 Merge pull request from quinton11/feat/multi-profile
feat: CLI support for switching between multiple logged in user accounts
2023-04-20 14:00:23 -07:00
7d8c6eb6b7 Merge pull request from Infisical/snyk-upgrade-aba197e4f121a17ffaf02cd20097245f
[Snyk] Upgrade @sentry/tracing from 7.45.0 to 7.46.0
2023-04-20 13:55:34 -07:00
4dd96704f0 Merge branch 'main' into snyk-upgrade-aba197e4f121a17ffaf02cd20097245f 2023-04-20 13:55:24 -07:00
2e428f9d66 Merge pull request from Infisical/snyk-upgrade-93cddd3ce0262fa45d130dffdccf8932
[Snyk] Upgrade @aws-sdk/client-secrets-manager from 3.301.0 to 3.303.0
2023-04-20 13:53:56 -07:00
7a926fbdac Merge pull request from Infisical/snyk-upgrade-a9ff8e4e7d9befabd7d947ce7f0c25b3
[Snyk] Upgrade @sentry/node from 7.45.0 to 7.46.0
2023-04-20 13:53:46 -07:00
0d3999c7e5 fix: upgrade @sentry/node from 7.45.0 to 7.46.0
Snyk has created this PR to upgrade @sentry/node from 7.45.0 to 7.46.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/node

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-20 20:06:11 +00:00
24913217c6 fix: upgrade @aws-sdk/client-secrets-manager from 3.301.0 to 3.303.0
Snyk has created this PR to upgrade @aws-sdk/client-secrets-manager from 3.301.0 to 3.303.0.

See this package in npm:
https://www.npmjs.com/package/@aws-sdk/client-secrets-manager

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-20 20:06:07 +00:00
c581fde65e fix: upgrade @sentry/tracing from 7.45.0 to 7.46.0
Snyk has created this PR to upgrade @sentry/tracing from 7.45.0 to 7.46.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/tracing

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-20 20:06:02 +00:00
fa9a7301d9 minor changes 2023-04-19 23:21:26 +00:00
3add40bfbd Minor changes 2023-04-19 19:49:14 +00:00
d4206cdbd8 login and user update commands support for existing domain override methods 2023-04-19 19:41:14 +00:00
3adbb7316a Merge pull request from Infisical/snyk-upgrade-51cc31a6d1afe5b1d4d65d58bb609257
[Snyk] Upgrade @aws-sdk/client-secrets-manager from 3.299.0 to 3.301.0
2023-04-19 09:52:05 -07:00
3e022346cd fix: upgrade @aws-sdk/client-secrets-manager from 3.299.0 to 3.301.0
Snyk has created this PR to upgrade @aws-sdk/client-secrets-manager from 3.299.0 to 3.301.0.

See this package in npm:
https://www.npmjs.com/package/@aws-sdk/client-secrets-manager

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-19 16:44:21 +00:00
afdf971014 Fixed path in docs 2023-04-19 07:53:22 -07:00
c1b97841cf resolved review concerns 2023-04-19 11:29:04 +00:00
b0107d28d4 update chart value 2023-04-18 16:33:29 -07:00
9f1f709b57 add service token field in helmchart for k8 2023-04-18 16:32:27 -07:00
dd4c4e1473 make hostAPI optional 2023-04-18 16:32:27 -07:00
92e04c45e7 Update Chart.yaml 2023-04-18 15:28:12 -07:00
44a7eb8123 Merge pull request from Infisical/service-accounts-with-k8-operator
update k8 operator to use service account
2023-04-18 14:49:00 -07:00
7a2192cf95 Merge pull request from Infisical/snyk-upgrade-ae9971a130863ea0dd7614699a93f40b
[Snyk] Upgrade @sentry/node from 7.41.0 to 7.45.0
2023-04-18 14:26:49 -07:00
0ad8075197 Merge branch 'main' into snyk-upgrade-ae9971a130863ea0dd7614699a93f40b 2023-04-18 14:26:42 -07:00
b258cbd852 Merge pull request from Infisical/snyk-upgrade-f7e4421cf1dcf4abd7da31a7f2f0269c
[Snyk] Upgrade @aws-sdk/client-secrets-manager from 3.294.0 to 3.299.0
2023-04-18 14:25:44 -07:00
f1c2512600 Merge branch 'main' into snyk-upgrade-f7e4421cf1dcf4abd7da31a7f2f0269c 2023-04-18 14:25:37 -07:00
1348c94154 Merge pull request from Infisical/snyk-upgrade-92cb55bf13238343efcdc817c6e6b2ce
[Snyk] Upgrade @sentry/tracing from 7.41.0 to 7.45.0
2023-04-18 14:24:58 -07:00
11ac5d18ff Merge branch 'main' into snyk-upgrade-92cb55bf13238343efcdc817c6e6b2ce 2023-04-18 14:24:52 -07:00
bb60e1d327 Merge pull request from Infisical/snyk-upgrade-0aba917b89e37535cd36bc3e962221b0
[Snyk] Upgrade mongoose from 6.10.3 to 6.10.4
2023-04-18 14:24:21 -07:00
70668d7783 add docs for using k8 controller with service acounts 2023-04-18 13:36:30 -07:00
be2cf54d6e host API support for login and switch commands 2023-04-18 12:03:03 +00:00
48cd2bddfe Rolled back the dashboard 2023-04-17 12:08:23 -07:00
884394866e Merge branch 'main' of https://github.com/Infisical/infisical 2023-04-17 11:50:01 -07:00
44c716aba3 Fixing minor bugs in dashboard and billing 2023-04-17 11:49:49 -07:00
8f08c4955f Update README.md 2023-04-16 22:26:31 -07:00
d1c62d655d Merge pull request from sheensantoscapadngan/adjustment/allow-multiselect-for-secrets-deletion
[Adjustment][Sheen] removed prompt when deleting secret
2023-04-16 22:24:09 -07:00
8e2837c8e8 fix: upgrade mongoose from 6.10.3 to 6.10.4
Snyk has created this PR to upgrade mongoose from 6.10.3 to 6.10.4.

See this package in npm:
https://www.npmjs.com/package/mongoose

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-17 05:01:23 +00:00
aa27308f5a fix: upgrade @aws-sdk/client-secrets-manager from 3.294.0 to 3.299.0
Snyk has created this PR to upgrade @aws-sdk/client-secrets-manager from 3.294.0 to 3.299.0.

See this package in npm:
https://www.npmjs.com/package/@aws-sdk/client-secrets-manager

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-17 05:01:18 +00:00
2d22c96a97 fix: upgrade @sentry/node from 7.41.0 to 7.45.0
Snyk has created this PR to upgrade @sentry/node from 7.41.0 to 7.45.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/node

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-17 05:01:13 +00:00
b4839eaac8 fix: upgrade @sentry/tracing from 7.41.0 to 7.45.0
Snyk has created this PR to upgrade @sentry/tracing from 7.41.0 to 7.45.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/tracing

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-17 05:01:06 +00:00
92df5e1a2f add docs for new k8 oper with service account 2023-04-16 21:04:28 -07:00
df2e0e03ff add service token to auth methods block for k8 2023-04-16 18:40:33 -07:00
5585893cfe allow service account to fetch secrets 2023-04-16 17:12:07 -07:00
e348e4678e remove unused method 2023-04-16 17:11:18 -07:00
4a36dcd1ed update helm and kubectl install manifests 2023-04-16 17:07:49 -07:00
619fe553ef update k8 operator to use service account 2023-04-16 16:51:36 -07:00
4c41a7f1cf Merge branch 'Infisical:main' into feat/multi-profile 2023-04-15 18:23:21 +00:00
04d46099f6 address package fixes 2023-04-15 10:02:10 -07:00
250428c64f Merge pull request from Infisical/snyk-upgrade-3f3d5368cc3b2bbb1bc7ecf70c71c625
[Snyk] Upgrade @sentry/tracing from 7.39.0 to 7.41.0
2023-04-15 09:56:03 -07:00
d40758a43d Merge branch 'main' into snyk-upgrade-3f3d5368cc3b2bbb1bc7ecf70c71c625 2023-04-15 09:55:55 -07:00
6a3d6ecbe5 Merge pull request from Infisical/snyk-upgrade-0afc777ee2d9380ebff1888a241d4d4a
[Snyk] Upgrade @aws-sdk/client-secrets-manager from 3.287.0 to 3.294.0
2023-04-15 09:54:24 -07:00
d6ed456ebd Merge pull request from Infisical/snyk-upgrade-c74bcdca67fc70a1214aee998010b3e4
[Snyk] Upgrade aws-sdk from 2.1331.0 to 2.1338.0
2023-04-15 09:54:13 -07:00
f99bb253df fix: upgrade @sentry/node from 7.40.0 to 7.41.0
Snyk has created this PR to upgrade @sentry/node from 7.40.0 to 7.41.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/node

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-15 09:53:26 -07:00
0c3c15be91 Merge pull request from Infisical/snyk-upgrade-417b9ee764e0ce7eb85b51db9c2ffdda
[Snyk] Upgrade posthog-node from 2.5.4 to 2.6.0
2023-04-15 09:44:00 -07:00
5fb7b55fdf Merge branch 'main' of https://github.com/Infisical/infisical 2023-04-15 12:30:51 +03:00
49559fbc5f Update contributors in README 2023-04-15 12:30:48 +03:00
12d8e144d1 Merge pull request from sheensantoscapadngan/feature/added-service-token-never-expire-1
[Feature][Sheen] added never expire service token
2023-04-15 11:46:48 +03:00
c1f39b866f [Feature][Sheen] added never expire service token 2023-04-15 15:50:06 +08:00
954335bd37 remove addNewUserPrompt 2023-04-14 21:37:57 -07:00
fe115a7476 Update user menu 2023-04-14 21:37:32 -07:00
00442992d2 Add user main command and add switch as sub command 2023-04-14 20:43:58 -07:00
12e16b4a03 [Adjustment][Sheen] removed additional prompt when deleting secret 2023-04-15 11:21:35 +08:00
56c35293eb hotfix: choose env when opening a dashboard link 2023-04-14 19:39:30 -07:00
d38432e0d6 Merge pull request from Infisical/birdseye-view
feat/birdseye-environment-overview
2023-04-14 19:36:52 -07:00
cfc9470a6f Fixed merge conflicts 2023-04-14 19:33:25 -07:00
3907c99b5b Merge branch 'main' into birdseye-view 2023-04-14 18:35:13 -07:00
903560a2d1 Finished the env overview feature 2023-04-14 18:20:19 -07:00
6d8b16fc85 mark smtp fields as not required 2023-04-14 16:16:37 -07:00
6b1f704a44 Update README.md 2023-04-14 12:49:12 -07:00
0b7508b40c Merge pull request from Infisical/improve-service-accounts
Improve service accounts / middleware + revamp images in documentation
2023-04-14 14:17:31 +03:00
7eae2392fe Merge remote-tracking branch 'origin' into improve-service-accounts 2023-04-14 14:08:28 +03:00
b21a8b4574 Add supabase docs, modify integration docs wording, check integration middleware 2023-04-14 14:08:09 +03:00
3b30095629 update CF link 2023-04-13 15:31:10 -07:00
5c15fab46e correct HTTPS_ENABLED cast 2023-04-13 15:24:41 -07:00
806448a7f9 Correct service token telemetry depending on creating entity 2023-04-13 22:30:42 +03:00
d824305fd6 Begin Supabase docs 2023-04-13 20:53:17 +03:00
83ddba29e2 conditionally set https 2023-04-13 09:03:23 -07:00
4489adeefa set https to false by default aws ec2 deploy 2023-04-13 09:01:57 -07:00
242f362682 Merge remote-tracking branch 'origin' into improve-service-accounts 2023-04-13 15:17:15 +03:00
0a9dc7ac46 Merge pull request from Aashish-Upadhyay-101/Supabase-Integration-Updated
Supabase integration updated
2023-04-13 14:30:17 +03:00
99dd661c56 Update middleware for service token data 2023-04-13 10:18:37 +03:00
1fe1afbb8e updated PR supabase-integration 2023-04-13 12:39:19 +05:45
83be9efee8 Merge remote-tracking branch 'origin' into improve-service-accounts 2023-04-13 09:37:01 +03:00
1b1cb4a1de supabase updated setup 2023-04-13 12:20:31 +05:45
dfa33e63cb remove secure cookie from default install 2023-04-12 17:29:53 -07:00
ac8b13116f Merge pull request from akhilmhdh/rollback/dashboard
feat(ui): rollback to old dashboard page
2023-04-12 14:10:55 -07:00
810554e13c First commit of env overview 2023-04-12 13:41:12 -07:00
3791ba2609 Merge remote-tracking branch 'origin' into improve-service-accounts 2023-04-12 22:41:41 +03:00
ed7dbb655c Updated bot, integration, and integrationAuth middlewares to support multiple clients 2023-04-12 22:36:36 +03:00
dda5f75450 add integration tests for checking service token with overrides 2023-04-12 12:00:27 -07:00
e2c67ffbef allow service tokens to continue to support overrides 2023-04-12 11:59:41 -07:00
2a64d657d3 feat(ui): rollback to old dashboard page 2023-04-12 19:34:27 +05:30
e2139882da Update middleware for requireSecretAuth 2023-04-12 14:17:20 +03:00
c7a402c4cb add integ tests for service token with overrides 2023-04-11 18:50:01 -07:00
73ddad8dac update service token tests 2023-04-11 18:12:20 -07:00
8c450d51da add integration tests for service-tokens 2023-04-11 17:49:45 -07:00
bec80de174 add integ tests for fetching/secrets secrets with jwt/service token 2023-04-11 16:51:50 -07:00
c768383f7e Merge remote-tracking branch 'origin' into improve-service-accounts 2023-04-11 23:58:52 +03:00
9df8e8926d Continue refactoring remaining middleware to be compatible with multiple clients 2023-04-11 23:58:29 +03:00
689ac6a8fe add back .populate(tags) to secrets GET via service token 2023-04-10 15:16:57 -07:00
576381cd58 add back user populate 2023-04-10 13:45:49 -07:00
8a67549ec5 remove user from GetServiceTokenDetailsResponse 2023-04-10 13:45:32 -07:00
5032450b1c Add service account support for organization endpoints and update docs images 2023-04-10 17:30:51 +03:00
802cb80416 Add helm chart video tut 2023-04-09 21:44:46 -07:00
e0ac12be14 default to prod when no node env 2023-04-09 10:41:13 -07:00
afa7b35d50 Add public key to service account creation modal 2023-04-09 19:51:13 +03:00
192e3beb46 Merge pull request from Infisical/fix-railway-sa-errors
Patch uncaught lint error
2023-04-09 16:39:46 +03:00
9c3a426cb1 Patch lint error 2023-04-09 16:38:19 +03:00
7e15e733f8 Merge pull request from Infisical/railway
Railway Integration + Service Accounts
2023-04-09 16:31:00 +03:00
365daa97a8 Remove service accounts from permitted secrets auth modes for now 2023-04-09 15:10:18 +03:00
710364e3a1 Add support for service variables to Railway integration, add docs for Railway 2023-04-09 15:00:40 +03:00
f6e23127ac Fix merge conflicts 2023-04-09 09:55:34 +03:00
5855c859e5 Merge pull request from akhilmhdh/feat/new-dashboard
Feat/new dashboard
2023-04-08 23:44:50 -07:00
12478130d0 Fixing UI, UX, and bugs in the dashboard 2023-04-08 23:36:33 -07:00
ecb182ad03 Fixing UI, UX, and bugs in the dashboard 2023-04-08 17:30:28 -07:00
553703decb add ingress controller to helm chart 2023-04-08 16:57:38 -07:00
cbf05b7c31 Finish first iteration of Railway integration 2023-04-09 01:31:40 +03:00
0fe4a3c033 fix: upgrade posthog-node from 2.5.4 to 2.6.0
Snyk has created this PR to upgrade posthog-node from 2.5.4 to 2.6.0.

See this package in npm:
https://www.npmjs.com/package/posthog-node

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-08 21:29:16 +00:00
e8e8ff5563 fix: upgrade @aws-sdk/client-secrets-manager from 3.287.0 to 3.294.0
Snyk has created this PR to upgrade @aws-sdk/client-secrets-manager from 3.287.0 to 3.294.0.

See this package in npm:
https://www.npmjs.com/package/@aws-sdk/client-secrets-manager

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-08 21:29:12 +00:00
dbe75eeecb fix: upgrade aws-sdk from 2.1331.0 to 2.1338.0
Snyk has created this PR to upgrade aws-sdk from 2.1331.0 to 2.1338.0.

See this package in npm:
https://www.npmjs.com/package/aws-sdk

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-08 21:29:07 +00:00
57ee287dd2 Merge remote-tracking branch 'origin' into railway 2023-04-08 22:17:58 +03:00
98dcc42db2 Begin Railway integration, frontend, getApps 2023-04-08 22:17:38 +03:00
aa108d575d Begin v3 secrets 2023-04-08 20:34:27 +03:00
56cc77e0e8 Merge pull request from Infisical/vercel-preview-branches
Add support for syncing to Vercel preview branches
2023-04-08 16:08:21 +03:00
c175519d70 Add support for syncing to Vercel preview branches 2023-04-08 16:01:58 +03:00
a3093de55b Patch GitHub integration organization owner 2023-04-08 00:17:24 +03:00
00b17d250e Merge pull request from Infisical/snyk-upgrade-0ea5952cd096302b197acdddf41b8188
[Snyk] Upgrade aws-sdk from 2.1324.0 to 2.1331.0
2023-04-07 11:43:42 -07:00
eb94ad5ba4 Merge pull request from Infisical/snyk-upgrade-8427871e6a576319fcd05f240276f7da
[Snyk] Upgrade @aws-sdk/client-secrets-manager from 3.281.0 to 3.287.0
2023-04-07 11:43:28 -07:00
a3b35a9228 Merge pull request from Infisical/snyk-upgrade-107238710f6bc2ba87fa8a7314c55fe2
[Snyk] Upgrade mongoose from 6.10.2 to 6.10.3
2023-04-07 11:41:46 -07:00
5bbe09e4be Substitute hardcoded auth modes for variables 2023-04-07 16:50:05 +03:00
a7880db871 Patch service account UI in audit logs, add lastUsed for API keys and service accounts/tokens 2023-04-06 23:52:06 +03:00
db8ce00536 Docs to point cli to selfhost on windows 2023-04-06 10:43:11 -07:00
d54753289a Add required endpoints/functions for service account to create service tokens 2023-04-06 16:59:55 +03:00
c40546945f Minor dashboard updates 2023-04-05 18:58:59 -07:00
5508434563 feat: CLI support for multiple user accounts logins
See 
2023-04-04 16:27:47 +00:00
a3b2d1c838 Merge remote-tracking branch 'origin' into service-account 2023-04-04 11:08:57 +03:00
35d345f17e Merge remote-tracking branch 'origin' into service-account 2023-04-04 11:08:28 +03:00
aa53de9070 Begin refactoring middleware for service accounts 2023-04-04 11:08:03 +03:00
b7142a1f24 fix: upgrade mongoose from 6.10.2 to 6.10.3
Snyk has created this PR to upgrade mongoose from 6.10.2 to 6.10.3.

See this package in npm:
https://www.npmjs.com/package/mongoose

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-04 00:27:11 +00:00
cca9476975 Update helm chart docs 2023-04-03 13:06:40 -07:00
7ec5f0d342 Merge pull request from Grraahaam/fix/helm-charts-improvements
feat: helm auto-generated variables
2023-04-03 11:27:30 -07:00
bdb71d1051 feat(chart): added updatedAt annotation to apps dpl and pod 2023-04-03 09:10:54 +02:00
278f1caa19 chore(doc): updated scripts and docs 2023-04-01 00:18:00 +02:00
dd10bf1702 fix(chart): simplified ingress rules configuration 2023-04-01 00:17:06 +02:00
7c33b6159f Merge branch 'heads/main' into fix/helm-charts-improvements 2023-03-31 23:32:54 +02:00
510d5f0ffd chore(chart): discard secret-operator changes 2023-03-31 23:29:03 +02:00
84d46a428c Merge pull request from Aashish-Upadhyay-101/user-remove-notification-typo
Typo: User remove from Organization notification typo
2023-03-30 12:41:20 -07:00
520c294e45 Merge pull request from Infisical/snyk-upgrade-1afcc79e653f80864b072e202c5af918
[Snyk] Upgrade mongoose from 6.10.1 to 6.10.2
2023-03-30 11:50:48 -07:00
c797901778 Merge remote-tracking branch 'origin' into service-account 2023-03-31 00:04:57 +07:00
9c18adf35f Begin service account middleware 2023-03-30 23:55:24 +07:00
6d7628cdc0 fix: upgrade mongoose from 6.10.1 to 6.10.2
Snyk has created this PR to upgrade mongoose from 6.10.1 to 6.10.2.

See this package in npm:
https://www.npmjs.com/package/mongoose

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-03-29 21:29:40 +00:00
381652cbb2 fix: upgrade @aws-sdk/client-secrets-manager from 3.281.0 to 3.287.0
Snyk has created this PR to upgrade @aws-sdk/client-secrets-manager from 3.281.0 to 3.287.0.

See this package in npm:
https://www.npmjs.com/package/@aws-sdk/client-secrets-manager

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-03-29 21:29:32 +00:00
acc0198637 fix: upgrade @sentry/tracing from 7.39.0 to 7.41.0
Snyk has created this PR to upgrade @sentry/tracing from 7.39.0 to 7.41.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/tracing

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-03-29 21:29:28 +00:00
6af326685b fix: upgrade aws-sdk from 2.1324.0 to 2.1331.0
Snyk has created this PR to upgrade aws-sdk from 2.1324.0 to 2.1331.0.

See this package in npm:
https://www.npmjs.com/package/aws-sdk

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-03-29 21:29:24 +00:00
91c83e04be fix typo notification message 2023-03-30 01:08:30 +05:45
c9e12d33bd Revert "typo fixed"
This reverts commit 9e2a03244ed54f1a1c83b2343088f8adc2144a04.
2023-03-30 01:06:43 +05:45
9e2a03244e typo fixed 2023-03-30 01:05:15 +05:45
2a8a90c0c5 Merge pull request from Aashish-Upadhyay-101/test-crypto-and-posthog
unit tests for utils/posthog and utils/crypto
2023-03-29 20:58:51 +07:00
54e099f8a8 Merge remote-tracking branch 'origin' into service-account 2023-03-29 18:08:11 +07:00
88c0a46de3 Clean up and weave crypto into service account permissions 2023-03-29 18:07:15 +07:00
356f0ac860 improvement tests 2023-03-29 12:27:13 +05:45
49a690b7b2 Update envars.mdx 2023-03-28 20:16:33 -07:00
830368b812 Merge pull request from Infisical/snyk-upgrade-5364ad43aff0d54873feea82482cb023
[Snyk] Upgrade @sentry/tracing from 7.38.0 to 7.39.0
2023-03-28 20:03:38 -07:00
d19c2936e6 Merge pull request from Infisical/snyk-upgrade-68a4ecfcd221d249c48614d7d7a47ab6
[Snyk] Upgrade @aws-sdk/client-secrets-manager from 3.279.0 to 3.281.0
2023-03-28 20:03:07 -07:00
592cef67bc Merge pull request from Infisical/snyk-upgrade-5b74ea7bb92da36a422bbf22d2389650
[Snyk] Upgrade aws-sdk from 2.1323.0 to 2.1324.0
2023-03-28 20:02:51 -07:00
676d0a7bf2 Merge branch 'main' into snyk-upgrade-5364ad43aff0d54873feea82482cb023 2023-03-28 20:01:38 -07:00
5149f526d4 Merge pull request from Infisical/snyk-upgrade-15aecde6d4745facead24cb7fb372005
[Snyk] Upgrade @sentry/node from 7.39.0 to 7.40.0
2023-03-28 19:59:46 -07:00
4875cac4ef Merge pull request from Infisical/snyk-upgrade-363d8de4e07190af10bb7d948bd05cf8
[Snyk] Upgrade mongoose from 6.10.0 to 6.10.1
2023-03-28 19:59:31 -07:00
729aacc154 Resolve merge conflicts 2023-03-28 14:43:56 +07:00
68deea28b7 Make host name optional 2023-03-27 14:19:05 -07:00
d36d7bfce6 Checkpoint service account functionality, added UI and general backend structure 2023-03-27 22:00:10 +07:00
1c4649cc9e unit tests for utils/posthog and utils/crypto 2023-03-26 17:51:08 +05:45
77e537c35d fix: upgrade mongoose from 6.10.0 to 6.10.1
Snyk has created this PR to upgrade mongoose from 6.10.0 to 6.10.1.

See this package in npm:
https://www.npmjs.com/package/mongoose

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-03-24 19:36:50 +00:00
49f26d591b fix: upgrade @sentry/node from 7.39.0 to 7.40.0
Snyk has created this PR to upgrade @sentry/node from 7.39.0 to 7.40.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/node

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-03-23 18:26:14 +00:00
94b18e6fc4 fix: upgrade @aws-sdk/client-secrets-manager from 3.279.0 to 3.281.0
Snyk has created this PR to upgrade @aws-sdk/client-secrets-manager from 3.279.0 to 3.281.0.

See this package in npm:
https://www.npmjs.com/package/@aws-sdk/client-secrets-manager

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-03-22 18:02:16 +00:00
0008ff9a98 chore(chart): improved NOTES.txt commands format/layout 2023-03-22 00:27:27 +01:00
5cb6c663bb Merge branch 'heads/main' into fix/helm-charts-improvements 2023-03-22 00:19:02 +01:00
a90375ea3d revert: secret-operator changes 2023-03-22 00:17:19 +01:00
9cf921bb1c fix(conf): add MONGO_URL to backend variables 2023-03-22 00:17:19 +01:00
5ec1a1eedf fix: upgrade aws-sdk from 2.1323.0 to 2.1324.0
Snyk has created this PR to upgrade aws-sdk from 2.1323.0 to 2.1324.0.

See this package in npm:
https://www.npmjs.com/package/aws-sdk

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-03-21 18:17:28 +00:00
6cb036bb38 Merge branch 'main' into snyk-upgrade-5364ad43aff0d54873feea82482cb023 2023-03-20 19:32:04 -07:00
30472505ce fix: upgrade @sentry/tracing from 7.38.0 to 7.39.0
Snyk has created this PR to upgrade @sentry/tracing from 7.38.0 to 7.39.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/tracing

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-03-20 19:12:42 +00:00
fef1adb34f expand backup explanation 2023-03-19 10:34:15 -07:00
93d07c34ab chore(doc): restore link to the web doc 2023-03-19 12:07:39 +01:00
3a0ce7c084 Service account checkpoint 2023-03-19 17:01:36 +07:00
4cce75d128 feat(ui): implemented the new dashboard page 2023-03-19 00:40:13 +05:30
b42c33107e feat(ui): added api hooks and state hooks for dashboard 2023-03-19 00:39:18 +05:30
686d3c409d feat(ui): added ui components for new dashboard 2023-03-19 00:39:13 +05:30
8fb473c57c Checkpoint service accounts 2023-03-18 13:52:38 +07:00
ebdcccb6ca Checkpoint service accounts 2023-03-18 13:34:06 +07:00
ba240f9e29 chore(doc): add INVITE_ONLY_SIGNUP description 2023-03-17 09:41:49 +01:00
95bb9e2586 fix(chart): add INVITE_ONLY_SIGNUP variable 2023-03-17 09:39:57 +01:00
273f4228d7 Init models for permission, service account, and service account key 2023-03-17 10:43:14 +07:00
c8b6eb0d6c fix(chart): NOTES.txt typos 2023-03-16 01:49:53 +01:00
fc41be9db8 chore(script): add local installation example scripts 2023-03-16 01:35:43 +01:00
164da9d8e0 chore(doc): updated helm parameters doc + 0.1.16 upgrade instructions 2023-03-16 01:34:40 +01:00
767943368e fix(conf): mongodb probes + docs 2023-03-16 01:32:54 +01:00
e37f584d75 fix(chart): upgrade deps and bump the verison 2023-03-16 01:30:55 +01:00
b2663fb3e0 chore(pr): pull request template comments 2023-03-16 01:29:58 +01:00
4899c4de5b fix(chart): secret data to stringData for cross-type compatibility 2023-03-14 01:45:00 +01:00
744caf8c79 chore(script): remove auto-generated variables from setup script 2023-03-14 01:43:43 +01:00
da888e27ad fix(chart): ingressClassName cross-version compatibility 2023-03-13 20:18:21 +01:00
28369411f7 fix(chart): update chart dependencies 2023-03-13 20:16:37 +01:00
b7a1689aeb feat(chart): env variables auto-generation 2023-03-13 19:58:03 +01:00
c034b62b71 fix(chart): default secret name to the service fullname 2023-03-13 19:57:13 +01:00
a6b9400a4a fix(chart): wrap secret values into quotes automatically 2023-03-13 19:57:13 +01:00
aa5d761081 fix(chart): image tags default to latest 2023-03-13 19:56:51 +01:00
2cc8e59ca8 fix(chart): kubeSecretRef disables and overwrites default secrets 2023-03-13 19:56:51 +01:00
a031e84ab8 fix(chart): uses envFrom only to inject secrets and conf 2023-03-13 19:55:40 +01:00
e2df6e94a5 chore(chart): breaking change version bump + docs 2023-03-13 19:55:40 +01:00
9db69430b5 fix(chart): ingress shorten names with variables + ingressClassName 2023-03-13 19:51:16 +01:00
00feee6903 fix(chart): shorten names with variables + improved secrets support 2023-03-13 19:51:16 +01:00
10dd747899 fix(chart): truncated secrets-operator resource name 2023-03-13 19:49:25 +01:00
349 changed files with 15328 additions and 2175 deletions
.env.example
.github
README.md
backend
package-lock.jsonpackage.json
src
config
controllers
ee
helpers
index.ts
integrations
interfaces/serviceAccounts/dto
middleware
models
routes
services
types/express
utils
variables
test-resources
tests
cli/packages
cloudformation/ec2-deployment
docs
frontend
package-lock.jsonpackage.json
public
src
components
ee/components
hooks
layouts/AppLayout
pages
_app.tsx
activity
api
dashboard
integrations
[id].tsx
aws-parameter-store
aws-secret-manager
azure-key-vault
circleci
flyio
github
gitlab
heroku
netlify
railway
render
supabase
travisci
vercel
settings/org/[id]
styles
views
tailwind.config.js
helm-charts
k8-operator
nginx

@ -31,12 +31,12 @@ MONGO_PASSWORD=example
SITE_URL=http://localhost:8080
# Mail/SMTP
SMTP_HOST= # required
SMTP_USERNAME= # required
SMTP_PASSWORD= # required
SMTP_HOST=
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_PORT=587
SMTP_SECURE=false
SMTP_FROM_ADDRESS= # required
SMTP_FROM_ADDRESS=
SMTP_FROM_NAME=Infisical
# Integration
@ -66,4 +66,4 @@ STRIPE_WEBHOOK_SECRET=
STRIPE_PRODUCT_STARTER=
STRIPE_PRODUCT_TEAM=
STRIPE_PRODUCT_PRO=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=

@ -1,6 +1,6 @@
# Description 📣
*Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.*
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
## Type ✨
@ -11,7 +11,7 @@
# Tests 🛠️
*Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. You may want to add screenshots when relevant and possible*
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. You may want to add screenshots when relevant and possible -->
```sh
# Here's some code block to paste some code snippets

@ -9,6 +9,12 @@ jobs:
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: 🧪 Run tests
run: npm run test:ci
working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
@ -45,8 +51,8 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: backend
tags: infisical/backend:${{ steps.commit.outputs.short }},
infisical/backend:latest
tags: infisical/backend:${{ steps.commit.outputs.short }},
infisical/backend:latest
platforms: linux/amd64,linux/arm64
frontend-image:
@ -94,8 +100,8 @@ jobs:
push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend
tags: infisical/frontend:${{ steps.commit.outputs.short }},
infisical/frontend:latest
tags: infisical/frontend:${{ steps.commit.outputs.short }},
infisical/frontend:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
@ -122,7 +128,7 @@ jobs:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Save DigitalOcean kubeconfig with short-lived credentials
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179
- name: switch to gamma namespace
- name: switch to gamma namespace
run: kubectl config set-context --current --namespace=gamma
- name: test kubectl
run: kubectl get ingress
@ -135,4 +141,4 @@ jobs:
exit 1
else
echo "Helm upgrade was successful"
fi
fi

File diff suppressed because one or more lines are too long

2144
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,14 +1,15 @@
{
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.267.0",
"@aws-sdk/client-secrets-manager": "^3.303.0",
"@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.39.0",
"@sentry/tracing": "^7.19.0",
"@sentry/node": "^7.45.0",
"@sentry/tracing": "^7.46.0",
"@sentry/node": "^7.41.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
"aws-sdk": "^2.1311.0",
"aws-sdk": "^2.1338.0",
"axios": "^1.1.3",
"axios-retry": "^3.4.0",
"bcrypt": "^5.1.0",
@ -29,9 +30,9 @@
"jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.7.2",
"mongoose": "^6.10.4",
"nodemailer": "^6.8.0",
"posthog-node": "^2.5.4",
"posthog-node": "^2.6.0",
"query-string": "^7.1.3",
"request-ip": "^3.3.0",
"rimraf": "^3.0.2",
@ -57,7 +58,7 @@
"lint-and-fix": "eslint . --ext .ts --fix",
"lint-staged": "lint-staged",
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
"test": "cross-env NODE_ENV=test jest --testTimeout=10000 --detectOpenHandles",
"test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles; npm run posttest",
"test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json",
"posttest": "docker compose -f test-resources/docker-compose.test.yml down"
},
@ -80,7 +81,7 @@
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
"@types/jest": "^29.2.4",
"@types/jest": "^29.5.0",
"@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.3",

@ -13,7 +13,7 @@ export const getJwtServiceSecret = () => infisical.get('JWT_SERVICE_SECRET')!;
export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m';
export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!;
export const getMongoURL = () => infisical.get('MONGO_URL')!;
export const getNodeEnv = () => infisical.get('NODE_ENV')!;
export const getNodeEnv = () => infisical.get('NODE_ENV')! || 'production';
export const getVerboseErrorOutput = () => infisical.get('VERBOSE_ERROR_OUTPUT')! === 'true' && true;
export const getLokiHost = () => infisical.get('LOKI_HOST')!;
export const getClientIdAzure = () => infisical.get('CLIENT_ID_AZURE')!;
@ -48,4 +48,17 @@ export const getStripeSecretKey = () => infisical.get('STRIPE_SECRET_KEY')!;
export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!;
export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true;
export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!;
export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true
export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true
export const getHttpsEnabled = () => {
if (getNodeEnv() != "production") {
// no https for anything other than prod
return false
}
if (infisical.get('HTTPS_ENABLED') == undefined || infisical.get('HTTPS_ENABLED') == "") {
// default when no value present
return true
}
return infisical.get('HTTPS_ENABLED') === 'true' && true
}

@ -15,10 +15,10 @@ import { BadRequestError } from '../../utils/errors';
import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getNodeEnv,
getJwtRefreshSecret,
getJwtAuthLifetime,
getJwtAuthSecret
getJwtAuthSecret,
getHttpsEnabled
} from '../../config';
declare module 'jsonwebtoken' {
@ -126,21 +126,21 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: getHttpsEnabled()
});
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
// return (access) token in response
return res.status(200).send({
token: tokens.token,
@ -182,14 +182,14 @@ export const logout = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: getHttpsEnabled() as boolean
});
const logoutAction = await EELogService.createAction({
name: ACTION_LOGOUT,
userId: req.user._id
});
logoutAction && await EELogService.createLog({
userId: req.user._id,
actions: [logoutAction],

@ -12,6 +12,11 @@ import {
getTeams,
revokeAccess
} from '../../integrations';
import {
INTEGRATION_VERCEL_API_URL,
INTEGRATION_RAILWAY_API_URL
} from '../../variables';
import request from '../../config/request';
/***
* Return integration authorization with id [integrationAuthId]
@ -188,22 +193,203 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
* @returns
*/
export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
let teams;
try {
teams = await getTeams({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
const teams = await getTeams({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
});
return res.status(200).send({
teams
});
}
/**
* Return list of available Vercel (preview) branches for Vercel project with
* id [appId]
* @param req
* @param res
*/
export const getIntegrationAuthVercelBranches = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface VercelBranch {
ref: string;
lastCommit: string;
isProtected: boolean;
}
const params = new URLSearchParams({
projectId: appId,
...(req.integrationAuth.teamId ? {
teamId: req.integrationAuth.teamId
} : {})
});
let branches: string[] = [];
if (appId && appId !== '') {
const { data }: { data: VercelBranch[] } = await request.get(
`${INTEGRATION_VERCEL_API_URL}/v1/integrations/git-branches`,
{
params,
headers: {
Authorization: `Bearer ${req.accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
branches = data.map((b) => b.ref);
}
return res.status(200).send({
branches
});
}
/**
* Return list of Railway environments for Railway project with
* id [appId]
* @param req
* @param res
*/
export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface RailwayEnvironment {
node: {
id: string;
name: string;
isEphemeral: boolean;
}
}
interface Environment {
environmentId: string;
name: string;
}
let environments: Environment[] = [];
if (appId && appId !== '') {
const query = `
query GetEnvironments($projectId: String!, $after: String, $before: String, $first: Int, $isEphemeral: Boolean, $last: Int) {
environments(projectId: $projectId, after: $after, before: $before, first: $first, isEphemeral: $isEphemeral, last: $last) {
edges {
node {
id
name
isEphemeral
}
}
}
}
`;
const variables = {
projectId: appId
}
const { data: { data: { environments: { edges } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables,
}, {
headers: {
'Authorization': `Bearer ${req.accessToken}`,
'Content-Type': 'application/json',
},
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get integration authorization teams"
environments = edges.map((e: RailwayEnvironment) => {
return ({
name: e.node.name,
environmentId: e.node.id
});
});
}
return res.status(200).send({
teams
environments
});
}
/**
* Return list of Railway services for Railway project with id
* [appId]
* @param req
* @param res
*/
export const getIntegrationAuthRailwayServices = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface RailwayService {
node: {
id: string;
name: string;
}
}
interface Service {
name: string;
serviceId: string;
}
let services: Service[] = [];
const query = `
query project($id: String!) {
project(id: $id) {
createdAt
deletedAt
id
description
expiredAt
isPublic
isTempProject
isUpdatable
name
prDeploys
teamId
updatedAt
upstreamUrl
services {
edges {
node {
id
name
}
}
}
}
}
`;
if (appId && appId !== '') {
const variables = {
id: appId
}
const { data: { data: { project: { services: { edges } } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables
}, {
headers: {
'Authorization': `Bearer ${req.accessToken}`,
'Content-Type': 'application/json',
},
});
services = edges.map((e: RailwayService) => ({
name: e.node.name,
serviceId: e.node.id
}));
}
return res.status(200).send({
services
});
}

@ -24,6 +24,9 @@ export const createIntegration = async (req: Request, res: Response) => {
isActive,
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region
@ -39,12 +42,15 @@ export const createIntegration = async (req: Request, res: Response) => {
app,
appId,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region,
integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId)
}).save();
}).save();
if (integration) {
// trigger event - push secrets

@ -9,7 +9,7 @@ import {
import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../../events';
import { EventService } from '../../services';
import { getPostHogClient } from '../../services';
import { TelemetryService } from '../../services';
interface PushSecret {
ciphertextKey: string;
@ -38,7 +38,7 @@ export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
try {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
@ -112,7 +112,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
let key;
try {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
@ -181,7 +181,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
let secrets;
let key;
try {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;

@ -50,6 +50,7 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
apiKeyData = await new APIKeyData({
name,
lastUsed: new Date(),
expiresAt,
user: req.user._id,
secretHash

@ -17,9 +17,9 @@ import {
} from '../../variables';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getNodeEnv,
getJwtMfaLifetime,
getJwtMfaSecret
getJwtMfaSecret,
getHttpsEnabled
} from '../../config';
declare module 'jsonwebtoken' {
@ -163,7 +163,7 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: getHttpsEnabled()
});
// case: user does not have MFA enablgged
@ -302,7 +302,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: getHttpsEnabled()
});
interface VerifyMfaTokenRes {

@ -11,7 +11,7 @@ import {
import { SecretVersion } from '../../ee/models';
import { BadRequestError } from '../../utils/errors';
import _ from 'lodash';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
/**
* Create new workspace environment named [environmentName] under workspace with id
@ -244,8 +244,8 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (
throw BadRequestError()
}
relatedWorkspace.environments.forEach(environment => {
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ })
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE })
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS })
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS })
if (isReadBlocked && isWriteBlocked) {
return
} else {

@ -7,6 +7,7 @@ import * as serviceTokenDataController from './serviceTokenDataController';
import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController';
import * as secretsController from './secretsController';
import * as serviceAccountsController from './serviceAccountsController';
import * as environmentController from './environmentController';
import * as tagController from './tagController';
@ -20,6 +21,7 @@ export {
apiKeyDataController,
secretController,
secretsController,
serviceAccountsController,
environmentController,
tagController
}

@ -1,9 +1,11 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
MembershipOrg,
Membership,
Workspace
Workspace,
ServiceAccount
} from '../../models';
import { deleteMembershipOrg } from '../../helpers/membershipOrg';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
@ -260,37 +262,45 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
}
}
*/
let workspaces;
try {
const { organizationId } = req.params;
const { organizationId } = req.params;
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
'_id'
)
).map((w) => w._id.toString())
);
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
'_id'
)
).map((w) => w._id.toString())
);
workspaces = (
await Membership.find({
user: req.user._id
}).populate('workspace')
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization workspaces'
});
}
return res.status(200).send({
const workspaces = (
await Membership.find({
user: req.user._id
}).populate('workspace')
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
return res.status(200).send({
workspaces
});
}
/**
* Return service accounts for organization with id [organizationId]
* @param req
* @param res
*/
export const getOrganizationServiceAccounts = async (req: Request, res: Response) => {
const { organizationId } = req.params;
const serviceAccounts = await ServiceAccount.find({
organization: new Types.ObjectId(organizationId)
});
return res.status(200).send({
serviceAccounts
});
}

@ -7,7 +7,7 @@ const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { getPostHogClient } from '../../services';
import { TelemetryService } from '../../services';
/**
* Create secret for workspace with id [workspaceId] and environment [environment]
@ -15,7 +15,7 @@ import { getPostHogClient } from '../../services';
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environment } = req.params
const sanitizedSecret: SanitizedSecretForCreate = {
@ -68,7 +68,7 @@ export const createSecret = async (req: Request, res: Response) => {
* @param res
*/
export const createSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environment } = req.params
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
@ -130,7 +130,7 @@ export const createSecrets = async (req: Request, res: Response) => {
* @param res
*/
export const deleteSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params
const secretIdsToDelete: string[] = req.body.secretIds
@ -184,7 +184,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
* @param res
*/
export const deleteSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
await Secret.findByIdAndDelete(req._secret._id)
if (postHogClient) {
@ -213,7 +213,7 @@ export const deleteSecret = async (req: Request, res: Response) => {
* @returns
*/
export const updateSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
@ -281,7 +281,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
* @returns
*/
export const updateSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
@ -335,7 +335,7 @@ export const updateSecret = async (req: Request, res: Response) => {
* @returns
*/
export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const { environment } = req.query;
const { workspaceId } = req.params;

@ -15,12 +15,12 @@ import { UnauthorizedRequestError, ValidationError } from '../../utils/errors';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { EESecretService, EELogService } from '../../ee/services';
import { getPostHogClient } from '../../services';
import { TelemetryService } from '../../services';
import { getChannelFromUserAgent } from '../../utils/posthog';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
import { PERMISSION_WRITE_SECRETS } from '../../variables';
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
import Tag from '../../models/tag';
import _ from 'lodash';
import _, { eq } from 'lodash';
import {
BatchSecretRequest,
BatchSecret
@ -28,12 +28,13 @@ import {
/**
* Peform a batch of any specified CUD secret operations
* (used by dashboard)
* @param req
* @param res
*/
export const batchSecrets = async (req: Request, res: Response) => {
const channel = getChannelFromUserAgent(req.headers['user-agent']);
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const {
workspaceId,
@ -91,7 +92,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
userId: req.user._id,
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId),
secretIds: createdSecrets.map((n) => n._id)
}) as IAction;
@ -328,14 +331,15 @@ export const createSecrets = async (req: Request, res: Response) => {
}
}
*/
const postHogClient = getPostHogClient();
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body;
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_WRITE)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
if (req.user) {
const hasAccess = await userHasWorkspaceAccess(req.user, new Types.ObjectId(workspaceId), environment, PERMISSION_WRITE_SECRETS)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
}
let listOfSecretsToCreate;
@ -378,7 +382,7 @@ export const createSecrets = async (req: Request, res: Response) => {
version: 1,
workspace: new Types.ObjectId(workspaceId),
type,
user: type === SECRET_PERSONAL ? req.user : undefined,
user: (req.user && type === SECRET_PERSONAL) ? req.user : undefined,
environment,
secretKeyCiphertext,
secretKeyIV,
@ -391,7 +395,7 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentTag,
tags
});
})
});
const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject());
@ -447,14 +451,18 @@ export const createSecrets = async (req: Request, res: Response) => {
const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
userId: req.user._id,
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId),
secretIds: newlyCreatedSecrets.map((n) => n._id)
});
// (EE) create (audit) log
addAction && await EELogService.createLog({
userId: req.user._id.toString(),
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId),
actions: [addAction],
channel,
@ -466,10 +474,15 @@ export const createSecrets = async (req: Request, res: Response) => {
workspaceId
});
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
distinctId: req.user.email,
distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: {
numberOfSecrets: listOfSecretsToCreate.length,
environment,
@ -533,91 +546,120 @@ export const getSecrets = async (req: Request, res: Response) => {
}
*/
const postHogClient = getPostHogClient();
const { tagSlugs } = req.query;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const { workspaceId, environment, tagSlugs } = req.query;
// secrets to return
let secrets: ISecret[] = [];
// query tags table to get all tags ids for the tag names for the given workspace
let tagIds = [];
const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : [];
let userId = "" // used for getting personal secrets for user
let userEmail = "" // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
}
// none service token case as service tokens are already scoped to env and project
let hasWriteOnlyAccess
if (!req.serviceTokenData) {
hasWriteOnlyAccess = await userHasWriteOnlyAbility(userId, workspaceId, environment)
const hasNoAccess = await userHasNoAbility(userId, workspaceId, environment)
if (hasNoAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
}
let secrets: any
let secretQuery: any
if (tagNamesList != undefined && tagNamesList.length != 0) {
const workspaceFromDB = await Tag.find({ workspace: workspaceId })
const tagIds = _.map(tagNamesList, (tagName) => {
const workspaceFromDB = await Tag.find({ workspace: workspaceId });
tagIds = _.map(tagNamesList, (tagName) => {
const tag = _.find(workspaceFromDB, { slug: tagName });
return tag ? tag.id : null;
});
}
secretQuery = {
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
tags: { $in: tagIds },
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
if (req.user) {
// case: client authorization is via JWT
const hasWriteOnlyAccess = await userHasWriteOnlyAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
const hasNoAccess = await userHasNoAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
if (hasNoAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
} else {
secretQuery = {
const secretQuery: any = {
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
{ user: req.user._id }, // personal secrets for this user
{ user: { $exists: false } } // shared secrets from workspace
]
}
if (tagIds.length > 0) {
secretQuery.tags = { $in: tagIds };
}
if (hasWriteOnlyAccess) {
// only return the secret keys and not the values since user does not have right to see values
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag").populate("tags")
} else {
secrets = await Secret.find(secretQuery).populate("tags")
}
}
if (hasWriteOnlyAccess) {
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag")
} else {
secrets = await Secret.find(secretQuery).populate("tags")
// case: client authorization is via service token
if (req.serviceTokenData) {
const userId = req.serviceTokenData.user._id
const secretQuery: any = {
workspace: workspaceId,
environment,
$or: [
{ user: userId }, // personal secrets for this user
{ user: { $exists: false } } // shared secrets from workspace
]
}
if (tagIds.length > 0) {
secretQuery.tags = { $in: tagIds };
}
// TODO check if service token has write only permission
secrets = await Secret.find(secretQuery).populate("tags");
}
// case: client authorization is via service account
if (req.serviceAccount) {
const secretQuery: any = {
workspace: workspaceId,
environment,
user: { $exists: false } // shared secrets only from workspace
}
if (tagIds.length > 0) {
secretQuery.tags = { $in: tagIds };
}
secrets = await Secret.find(secretQuery).populate("tags");
}
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const readAction = await EELogService.createAction({
name: ACTION_READ_SECRETS,
userId: new Types.ObjectId(userId),
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId as string),
secretIds: secrets.map((n: any) => n._id)
});
readAction && await EELogService.createLog({
userId: new Types.ObjectId(userId),
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId as string),
actions: [readAction],
channel,
ipAddress: req.ip
});
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets pulled',
distinctId: userEmail,
distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: {
numberOfSecrets: secrets.length,
environment,
@ -633,59 +675,6 @@ export const getSecrets = async (req: Request, res: Response) => {
});
}
export const getOnlySecretKeys = async (req: Request, res: Response) => {
const { workspaceId, environment } = req.query;
let userId = "" // used for getting personal secrets for user
let userEmail = "" // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
}
// none service token case as service tokens are already scoped
if (!req.serviceTokenData) {
const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
}
const [err, secretKeys] = await to(Secret.find(
{
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
)
.select("secretKeyIV secretKeyTag secretKeyCiphertext")
.then())
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
// readAction && await EELogService.createLog({
// userId: new Types.ObjectId(userId),
// workspaceId: new Types.ObjectId(workspaceId as string),
// actions: [readAction],
// channel,
// ipAddress: req.ip
// });
return res.status(200).send({
secretKeys
});
}
/**
* Update secret(s)
* @param req
@ -736,10 +725,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
}
}
*/
const postHogClient = getPostHogClient();
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
// TODO: move type
interface PatchSecret {
id: string;
secretKeyCiphertext: string;
@ -865,14 +852,18 @@ export const updateSecrets = async (req: Request, res: Response) => {
const updateAction = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS,
userId: req.user._id,
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
});
// (EE) create (audit) log
updateAction && await EELogService.createLog({
userId: req.user._id.toString(),
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key),
actions: [updateAction],
channel,
@ -884,10 +875,15 @@ export const updateSecrets = async (req: Request, res: Response) => {
workspaceId: key
})
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
distinctId: req.user.email,
distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: {
numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment,
@ -909,7 +905,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
}
/**
* Delete secret(s) with id [workspaceId] and environment [environment]
* Delete secret(s)
* @param req
* @param res
*/
@ -958,7 +954,11 @@ export const deleteSecrets = async (req: Request, res: Response) => {
}
}
*/
const postHogClient = getPostHogClient();
return res.status(200).send({
message: 'delete secrets!!'
});
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const toDelete = req.secrets.map((s: any) => s._id);
@ -992,14 +992,18 @@ export const deleteSecrets = async (req: Request, res: Response) => {
});
const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS,
userId: req.user._id,
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
});
// (EE) create (audit) log
deleteAction && await EELogService.createLog({
userId: req.user._id.toString(),
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key),
actions: [deleteAction],
channel,
@ -1011,10 +1015,15 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: key
})
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
distinctId: req.user.email,
distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: {
numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment,

@ -0,0 +1,306 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
ServiceAccount,
ServiceAccountKey,
ServiceAccountOrganizationPermission,
ServiceAccountWorkspacePermission
} from '../../models';
import {
CreateServiceAccountDto
} from '../../interfaces/serviceAccounts/dto';
import { BadRequestError, ServiceAccountNotFoundError } from '../../utils/errors';
import { getSaltRounds } from '../../config';
/**
* Return service account tied to the request (service account) client
* @param req
* @param res
*/
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
});
}
/**
* Return service account with id [serviceAccountId]
* @param req
* @param res
*/
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
});
}
/**
* Create a new service account under organization with id [organizationId]
* that has access to workspaces [workspaces]
* @param req
* @param res
* @returns
*/
export const createServiceAccount = async (req: Request, res: Response) => {
const {
name,
organizationId,
publicKey,
expiresIn,
}: CreateServiceAccountDto = req.body;
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
const secret = crypto.randomBytes(16).toString('base64');
const secretHash = await bcrypt.hash(secret, getSaltRounds());
// create service account
const serviceAccount = await new ServiceAccount({
name,
organization: new Types.ObjectId(organizationId),
user: req.user,
publicKey,
lastUsed: new Date(),
expiresAt,
secretHash
}).save();
const serviceAccountObj = serviceAccount.toObject();
delete serviceAccountObj.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({
serviceAccountAccessKey: `sa.${secretId}.${secret}`,
serviceAccount: serviceAccountObj
});
}
/**
* Change name of service account with id [serviceAccountId] to [name]
* @param req
* @param res
* @returns
*/
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)
},
{
name
},
{
new: true
}
);
return res.status(200).send({
serviceAccount
});
}
/**
* Add a service account key to service account with id [serviceAccountId]
* for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const addServiceAccountKey = async (req: Request, res: Response) => {
const {
workspaceId,
encryptedKey,
nonce
} = req.body;
const serviceAccountKey = await new ServiceAccountKey({
encryptedKey,
nonce,
sender: req.user._id,
serviceAccount: req.serviceAccount._d,
workspace: new Types.ObjectId(workspaceId)
}).save();
return serviceAccountKey;
}
/**
* Return workspace-level permission for service account with id [serviceAccountId]
* @param req
* @param res
*/
export const getServiceAccountWorkspacePermissions = async (req: Request, res: Response) => {
const serviceAccountWorkspacePermissions = await ServiceAccountWorkspacePermission.find({
serviceAccount: req.serviceAccount._id
}).populate('workspace');
return res.status(200).send({
serviceAccountWorkspacePermissions
});
}
/**
* Add a workspace permission to service account with id [serviceAccountId]
* @param req
* @param res
*/
export const addServiceAccountWorkspacePermission = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const {
environment,
workspaceId,
read = false,
write = false,
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({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId),
environment,
read,
write
}).save();
const existingServiceAccountKey = await ServiceAccountKey.findOne({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId)
});
if (!existingServiceAccountKey) {
await new ServiceAccountKey({
encryptedKey,
nonce,
sender: req.user._id,
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId)
}).save();
}
return res.status(200).send({
serviceAccountWorkspacePermission
});
}
/**
* Delete workspace permission from service account with id [serviceAccountId]
* @param req
* @param res
*/
export const deleteServiceAccountWorkspacePermission = async (req: Request, res: Response) => {
const { serviceAccountWorkspacePermissionId } = req.params;
const serviceAccountWorkspacePermission = await ServiceAccountWorkspacePermission.findByIdAndDelete(serviceAccountWorkspacePermissionId);
if (serviceAccountWorkspacePermission) {
const { serviceAccount, workspace } = serviceAccountWorkspacePermission;
const count = await ServiceAccountWorkspacePermission.countDocuments({
serviceAccount,
workspace
});
if (count === 0) {
await ServiceAccountKey.findOneAndDelete({
serviceAccount,
workspace
});
}
}
return res.status(200).send({
serviceAccountWorkspacePermission
});
}
/**
* Delete service account with id [serviceAccountId]
* @param req
* @param res
* @returns
*/
export const deleteServiceAccount = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const serviceAccount = await ServiceAccount.findByIdAndDelete(serviceAccountId);
if (serviceAccount) {
await ServiceAccountKey.deleteMany({
serviceAccount: serviceAccount._id
});
await ServiceAccountOrganizationPermission.deleteMany({
serviceAccount: new Types.ObjectId(serviceAccountId)
});
await ServiceAccountWorkspacePermission.deleteMany({
serviceAccount: new Types.ObjectId(serviceAccountId)
});
}
return res.status(200).send({
serviceAccount
});
}
/**
* Return service account keys for service account with id [serviceAccountId]
* @param req
* @param res
* @returns
*/
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
});
}

@ -3,10 +3,16 @@ import { Request, Response } from 'express';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
User,
ServiceAccount,
ServiceTokenData
} from '../../models';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
import { ABILITY_READ } from '../../variables/organization';
import {
PERMISSION_READ_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT
} from '../../variables';
import { getSaltRounds } from '../../config';
/**
@ -53,59 +59,60 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
* @returns
*/
export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceToken, serviceTokenData;
let serviceTokenData;
try {
const {
name,
workspaceId,
environment,
encryptedKey,
iv,
tag,
expiresIn,
permissions
} = req.body;
const {
name,
workspaceId,
environment,
encryptedKey,
iv,
tag,
expiresIn,
permissions
} = req.body;
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, getSaltRounds());
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, getSaltRounds());
const expiresAt = new Date();
let expiresAt;
if (!!expiresIn) {
expiresAt = new Date()
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
environment,
user: req.user._id,
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
permissions
}).save();
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (!serviceTokenData) throw new Error('Failed to find service token data');
serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create service token data'
});
}
let user, serviceAccount;
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
user = req.authData.authPayload._id;
}
if (req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && req.authData.authPayload instanceof ServiceAccount) {
serviceAccount = req.authData.authPayload._id;
}
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
environment,
user,
serviceAccount,
lastUsed: new Date(),
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
permissions
}).save();
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (!serviceTokenData) throw new Error('Failed to find service token data');
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
return res.status(200).send({
serviceToken,
serviceTokenData
@ -119,25 +126,11 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
* @returns
*/
export const deleteServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData;
try {
const { serviceTokenDataId } = req.params;
const { serviceTokenDataId } = req.params;
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete service token data'
});
}
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
return res.status(200).send({
serviceTokenData
});
}
function UnauthorizedRequestError(arg0: { message: string; }) {
throw new Error('Function not implemented.');
}

@ -8,7 +8,7 @@ import {
import { issueAuthTokens } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables';
import request from '../../config/request';
import { getNodeEnv, getLoopsApiKey } from '../../config';
import { getLoopsApiKey, getHttpsEnabled } from '../../config';
/**
* Complete setting up user by adding their personal and auth information as part of the
@ -24,9 +24,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
@ -38,9 +38,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
@ -48,11 +48,11 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
salt: string;
verifier: string;
organizationName: string;
} = req.body;
} = req.body;
// get user
user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
@ -66,10 +66,10 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
@ -127,7 +127,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: getHttpsEnabled()
});
} catch (err) {
Sentry.setUser(null);
@ -158,9 +158,9 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
@ -192,10 +192,10 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
@ -232,7 +232,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
secure: getHttpsEnabled()
});
} catch (err) {
Sentry.setUser(null);
@ -241,7 +241,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
message: 'Failed to complete account setup'
});
}
return res.status(200).send({
message: 'Successfully set up account',
user,

@ -19,7 +19,7 @@ import {
reformatPullSecrets
} from '../../helpers/secret';
import { pushKeys } from '../../helpers/key';
import { getPostHogClient, EventService } from '../../services';
import { TelemetryService, EventService } from '../../services';
import { eventPushSecrets } from '../../events';
interface V2PushSecret {
@ -48,7 +48,7 @@ interface V2PushSecret {
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
try {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
@ -122,7 +122,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
try {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
@ -506,5 +506,4 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => {
message: 'Successfully changed autoCapitalization setting',
workspace
});
};
};

@ -2,7 +2,8 @@ import { Request, Response } from "express";
import { Membership, Workspace } from "../../../models";
import { IMembershipPermission } from "../../../models/membership";
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
import { ABILITY_READ, ABILITY_WRITE, ADMIN, MEMBER } from "../../../variables/organization";
import { ADMIN, MEMBER } from "../../../variables/organization";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../../variables';
import { Builder } from "builder-pattern"
import _ from "lodash";
@ -10,7 +11,7 @@ export const denyMembershipPermissions = async (req: Request, res: Response) =>
const { membershipId } = req.params;
const { permissions } = req.body;
const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => {
if (!permission.ability || !permission.environmentSlug || ![ABILITY_READ, ABILITY_WRITE].includes(permission.ability)) {
if (!permission.ability || !permission.environmentSlug || ![PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS].includes(permission.ability)) {
throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" })
}

@ -418,7 +418,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
.skip(offset)
.limit(limit)
.populate('actions')
.populate('user');
.populate('user serviceAccount serviceTokenData');
} catch (err) {
Sentry.setUser({ email: req.user.email });

@ -24,11 +24,15 @@ import {
const createActionUpdateSecret = async ({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds
}: {
name: string;
userId: Types.ObjectId;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[];
}) => {
@ -46,6 +50,8 @@ const createActionUpdateSecret = async ({
action = await new Action({
name,
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
@ -72,11 +78,15 @@ const createActionUpdateSecret = async ({
const createActionSecret = async ({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds
}: {
name: string;
userId: Types.ObjectId;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[];
}) => {
@ -94,6 +104,8 @@ const createActionSecret = async ({
action = await new Action({
name,
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
@ -110,29 +122,36 @@ const createActionSecret = async ({
}
/**
* Create an (audit) action for user with id [userId]
* Create an (audit) action for client with id [userId],
* [serviceAccountId], or [serviceTokenDataId]
* @param {Object} obj
* @param {String} obj.name - name of action
* @param {String} obj.userId - id of user associated with action
* @returns
*/
const createActionUser = ({
const createActionClient = ({
name,
userId
userId,
serviceAccountId,
serviceTokenDataId
}: {
name: string;
userId: Types.ObjectId;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
}) => {
let action;
try {
action = new Action({
name,
user: userId
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create user action');
throw new Error('Failed to create client action');
}
return action;
@ -149,11 +168,15 @@ const createActionUser = ({
const createActionHelper = async ({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds,
}: {
name: string;
userId: Types.ObjectId;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[];
}) => {
@ -162,7 +185,7 @@ const createActionHelper = async ({
switch (name) {
case ACTION_LOGIN:
case ACTION_LOGOUT:
action = await createActionUser({
action = await createActionClient({
name,
userId
});

@ -1,8 +1,9 @@
import { Types } from 'mongoose';
import _ from "lodash";
import { Membership } from "../../models";
import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => {
export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) {
return false
@ -18,15 +19,15 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi
return true
}
export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, environment: any) => {
export const userHasWriteOnlyAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) {
return false
}
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE });
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ });
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
// case: you have write only if read is blocked and write is not
if (isReadDisallowed && !isWriteDisallowed) {
@ -36,15 +37,15 @@ export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, env
return false
}
export const userHasNoAbility = async (userId: any, workspaceId: any, environment: any) => {
export const userHasNoAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) {
return true
}
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE });
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ });
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
if (isReadBlocked && isWriteDisallowed) {
return true

@ -16,12 +16,16 @@ import {
*/
const createLogHelper = async ({
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
actions,
channel,
ipAddress
}: {
userId: Types.ObjectId;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId;
actions: IAction[];
channel: string;
@ -31,6 +35,8 @@ const createLogHelper = async ({
try {
log = await new Log({
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId ?? undefined,
actionNames: actions.map((a) => a.name),
actions,

@ -15,32 +15,28 @@ import {
const requireSecretSnapshotAuth = ({
acceptedRoles,
}: {
acceptedRoles: string[];
acceptedRoles: Array<'admin' | 'member'>;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { secretSnapshotId } = req.params;
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
if (!secretSnapshot) {
return next(SecretSnapshotNotFoundError({
message: 'Failed to find secret snapshot'
}));
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: secretSnapshot.workspace.toString(),
acceptedRoles
});
req.secretSnapshot = secretSnapshot as any;
next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret snapshot' }));
const { secretSnapshotId } = req.params;
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
if (!secretSnapshot) {
return next(SecretSnapshotNotFoundError({
message: 'Failed to find secret snapshot'
}));
}
await validateMembership({
userId: req.user._id,
workspaceId: secretSnapshot.workspace,
acceptedRoles
});
req.secretSnapshot = secretSnapshot as any;
next();
}
}

@ -11,6 +11,8 @@ import {
export interface IAction {
name: string;
user?: Types.ObjectId,
serviceAccount?: Types.ObjectId,
serviceTokenData?: Types.ObjectId,
workspace?: Types.ObjectId,
payload?: {
secretVersions?: Types.ObjectId[]
@ -33,8 +35,15 @@ const actionSchema = new Schema<IAction>(
},
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
ref: 'User'
},
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: 'ServiceTokenData'
},
workspace: {
type: Schema.Types.ObjectId,

@ -11,6 +11,8 @@ import {
export interface ILog {
_id: Types.ObjectId;
user?: Types.ObjectId;
serviceAccount?: Types.ObjectId;
serviceTokenData?: Types.ObjectId;
workspace?: Types.ObjectId;
actionNames: string[];
actions: Types.ObjectId[];
@ -24,6 +26,14 @@ const logSchema = new Schema<ILog>(
type: Schema.Types.ObjectId,
ref: 'User'
},
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: 'ServiceTokenData'
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace'

@ -7,7 +7,12 @@ import {
} from '../../../middleware';
import { query, param, body } from 'express-validator';
import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER } from '../../../variables';
import {
ADMIN,
MEMBER,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../../../variables';
router.get(
'/:secretId/secret-versions',
@ -15,7 +20,8 @@ router.get(
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS]
}),
param('secretId').exists().trim(),
query('offset').exists().isInt(),
@ -30,7 +36,8 @@ router.post(
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS]
}),
param('secretId').exists().trim(),
body('version').exists().isInt(),

@ -15,7 +15,8 @@ router.get(
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
query('offset').exists().isInt(),
@ -30,7 +31,8 @@ router.get(
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
validateRequest,
@ -43,7 +45,8 @@ router.post(
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
body('version').exists().isInt(),
@ -57,7 +60,8 @@ router.get(
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
query('offset').exists().isInt(),

@ -26,12 +26,16 @@ class EELogService {
*/
static async createLog({
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
actions,
channel,
ipAddress
}: {
userId: Types.ObjectId;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId;
actions: IAction[];
channel: string;
@ -40,6 +44,8 @@ class EELogService {
if (!EELicenseService.isLicenseValid) return null;
return await createLogHelper({
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
actions,
channel,
@ -59,17 +65,23 @@ class EELogService {
static async createAction({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds
}: {
name: string;
userId: Types.ObjectId;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[];
}) {
return await createActionHelper({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds
});

@ -1,15 +1,18 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import {
IUser,
User,
ServiceTokenData,
ServiceAccount,
APIKeyData
} from '../models';
import {
AccountNotFoundError,
ServiceTokenDataNotFoundError,
ServiceAccountNotFoundError,
APIKeyDataNotFoundError,
UnauthorizedRequestError,
BadRequestError
@ -20,6 +23,12 @@ import {
getJwtRefreshLifetime,
getJwtRefreshSecret
} from '../config';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
*
@ -37,7 +46,7 @@ const validateAuthMode = ({
const apiKey = headers['x-api-key'];
const authHeader = headers['authorization'];
let authTokenType, authTokenValue;
let authMode, authTokenValue;
if (apiKey === undefined && authHeader === undefined) {
// case: no auth or X-API-KEY header present
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
@ -45,7 +54,7 @@ const validateAuthMode = ({
if (typeof apiKey === 'string') {
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
authTokenType = 'apiKey';
authMode = AUTH_MODE_API_KEY;
authTokenValue = apiKey;
}
@ -61,20 +70,24 @@ const validateAuthMode = ({
switch (tokenValue.split('.', 1)[0]) {
case 'st':
authTokenType = 'serviceToken';
authMode = AUTH_MODE_SERVICE_TOKEN;
break;
case 'sa':
authMode = AUTH_MODE_SERVICE_ACCOUNT;
break;
default:
authTokenType = 'jwt';
authMode = AUTH_MODE_JWT;
}
authTokenValue = tokenValue;
}
if (!authTokenType || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
if (!authMode || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
if (!acceptedAuthModes.includes(authTokenType)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
if (!acceptedAuthModes.includes(authMode)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
return ({
authTokenType,
authMode,
authTokenValue
});
}
@ -90,25 +103,17 @@ const getAuthUserPayload = async ({
}: {
authTokenValue: string;
}) => {
let user;
try {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, getJwtAuthSecret())
);
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, getJwtAuthSecret())
);
user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });
} catch (err) {
throw UnauthorizedRequestError({
message: 'Failed to authenticate JWT token'
});
}
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });
return user;
}
@ -124,45 +129,70 @@ const getAuthSTDPayload = async ({
}: {
authTokenValue: string;
}) => {
let serviceTokenData;
try {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
// TODO: optimize double query
serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
let serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
if (!serviceTokenData) {
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate expired service token'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
});
serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER)
.select('+encryptedKey +iv +tag')
.populate<{user: IUser}>('user');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} catch (err) {
if (!serviceTokenData) {
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
message: 'Failed to authenticate expired service token'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
});
serviceTokenData = await ServiceTokenData
.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
}, {
lastUsed: new Date()
}, {
new: true
})
.select('+encryptedKey +iv +tag').populate('user serviceAccount');
if (!serviceTokenData) 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
*/
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' });
}
const result = await bcrypt.compare(TOKEN_SECRET, serviceAccount.secretHash);
if (!result) throw UnauthorizedRequestError({
message: 'Failed to authenticate service account access key'
});
return serviceAccount;
}
/**
* Return API key data payload corresponding to API key [authTokenValue]
* @param {Object} obj
@ -174,33 +204,44 @@ const getAuthAPIKeyPayload = async ({
}: {
authTokenValue: string;
}) => {
let user;
try {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
const apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate('user', '+publicKey');
let apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate<{ user: IUser }>('user', '+publicKey');
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
// case: API key expired
await APIKeyData.findByIdAndDelete(apiKeyData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate expired API key'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
user = apiKeyData.user;
} catch (err) {
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
// case: API key expired
await APIKeyData.findByIdAndDelete(apiKeyData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
message: 'Failed to authenticate expired API key'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
apiKeyData = await APIKeyData.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
}, {
lastUsed: new Date()
}, {
new: true
});
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
}
const user = await User.findById(apiKeyData.user).select('+publicKey');
if (!user) {
throw AccountNotFoundError({
message: 'Failed to find user'
});
}
@ -216,30 +257,23 @@ const getAuthAPIKeyPayload = async ({
* @return {String} obj.refreshToken - issued refresh token
*/
const issueAuthTokens = async ({ userId }: { userId: string }) => {
let token: string;
let refreshToken: string;
try {
// issue tokens
token = createToken({
payload: {
userId
},
expiresIn: getJwtAuthLifetime(),
secret: getJwtAuthSecret()
});
refreshToken = createToken({
payload: {
userId
},
expiresIn: getJwtRefreshLifetime(),
secret: getJwtRefreshSecret()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to issue tokens');
}
// issue tokens
const token = createToken({
payload: {
userId
},
expiresIn: getJwtAuthLifetime(),
secret: getJwtAuthSecret()
});
const refreshToken = createToken({
payload: {
userId
},
expiresIn: getJwtRefreshLifetime(),
secret: getJwtRefreshSecret()
});
return {
token,
@ -253,19 +287,14 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
* @param {String} obj.userId - id of user whose tokens are cleared.
*/
const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
try {
// increment refreshVersion on user by 1
User.findOneAndUpdate({
_id: userId
}, {
$inc: {
refreshVersion: 1
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
// increment refreshVersion on user by 1
User.findOneAndUpdate({
_id: userId
}, {
$inc: {
refreshVersion: 1
}
});
};
/**
@ -285,21 +314,16 @@ const createToken = ({
expiresIn: string | number;
secret: string;
}) => {
try {
return jwt.sign(payload, secret, {
expiresIn
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create a token');
}
return jwt.sign(payload, secret, {
expiresIn
});
};
export {
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
getAuthSAAKPayload,
getAuthAPIKeyPayload,
createToken,
issueAuthTokens,

@ -1,10 +1,16 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Bot,
BotKey,
Secret,
ISecret,
IUser
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import {
generateKeyPair,
@ -12,8 +18,88 @@ import {
decryptSymmetric,
decryptAsymmetric
} from '../utils/crypto';
import { SECRET_SHARED } from '../variables';
import {
SECRET_SHARED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import { getEncryptionKey } from '../config';
import { BotNotFoundError, UnauthorizedRequestError } from '../utils/errors';
import {
validateMembership
} from '../helpers/membership';
import {
validateUserClientForWorkspace
} from '../helpers/user';
import {
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
/**
* Validate authenticated clients for bot with id [botId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.botId - id of bot to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
*/
const validateClientForBot = async ({
authData,
botId,
acceptedRoles
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
botId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
}) => {
const bot = await Bot.findById(botId);
if (!bot) throw BotNotFoundError();
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: bot.workspace,
acceptedRoles
});
return bot;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: bot.workspace
});
return bot;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for bot'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: bot.workspace,
acceptedRoles
});
return bot;
}
throw BotNotFoundError({
message: 'Failed client authorization for bot'
});
}
/**
* Create an inactive bot with name [name] for workspace with id [workspaceId]
@ -222,6 +308,7 @@ const decryptSymmetricHelper = async ({
}
export {
validateClientForBot,
createBot,
getSecretsHelper,
encryptSymmetricHelper,

@ -1,17 +1,42 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Bot,
Integration,
IntegrationAuth,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService } from '../services';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
} from '../variables';
import { UnauthorizedRequestError } from '../utils/errors';
import {
UnauthorizedRequestError,
IntegrationAuthNotFoundError,
IntegrationNotFoundError
} from '../utils/errors';
import RequestError from '../utils/requestError';
import {
validateClientForIntegrationAuth
} from '../helpers/integrationAuth';
import {
validateUserClientForWorkspace
} from '../helpers/user';
import {
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
import { IntegrationService } from '../services';
interface Update {
workspace: string;
@ -20,6 +45,84 @@ interface Update {
accountId?: string;
}
/**
* Validate authenticated clients for integration with id [integrationId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.integrationId - id of integration to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForIntegration = async ({
authData,
integrationId,
acceptedRoles
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
integrationId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
}) => {
const integration = await Integration.findById(integrationId);
if (!integration) throw IntegrationNotFoundError();
const integrationAuth = await IntegrationAuth
.findById(integration.integrationAuth)
.select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) throw IntegrationAuthNotFoundError();
const accessToken = (await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id
})).accessToken;
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: integration.workspace,
acceptedRoles
});
return ({ integration, accessToken });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: integration.workspace
});
return ({ integration, accessToken });
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for integration'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: integration.workspace,
acceptedRoles
});
return ({ integration, accessToken });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for integration'
});
}
/**
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
* named [integration]
@ -140,7 +243,7 @@ const syncIntegrationsHelper = async ({
// get integration auth access token
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth.toString()
integrationAuthId: integration.integrationAuth
});
// sync secrets to integration
@ -167,7 +270,7 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} refreshToken - decrypted refresh token
*/
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let refreshToken;
try {
@ -204,7 +307,7 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token
*/
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let accessId;
let accessToken;
try {
@ -367,6 +470,7 @@ const setIntegrationAuthAccessHelper = async ({
}
export {
validateClientForIntegration,
handleOAuthExchangeHelper,
syncIntegrationsHelper,
getIntegrationAuthRefreshHelper,

@ -0,0 +1,108 @@
import { Types } from 'mongoose';
import {
IntegrationAuth,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData,
IWorkspace
} from '../models';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import {
IntegrationAuthNotFoundError,
UnauthorizedRequestError
} from '../utils/errors';
import { IntegrationService } from '../services';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
/**
* Validate authenticated clients for integration authorization with id [integrationAuthId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.integrationAuthId - id of integration authorization to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForIntegrationAuth = async ({
authData,
integrationAuthId,
acceptedRoles,
attachAccessToken
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
integrationAuthId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
attachAccessToken?: boolean;
}) => {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.populate<{ workspace: IWorkspace }>('workspace')
.select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) throw IntegrationAuthNotFoundError();
let accessToken;
if (attachAccessToken) {
accessToken = (await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id
})).accessToken;
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: integrationAuth.workspace._id,
acceptedRoles
});
return ({ integrationAuth, accessToken });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: integrationAuth.workspace._id
});
return ({ integrationAuth, accessToken });
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for integration authorization'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: integrationAuth.workspace._id,
acceptedRoles
});
return ({ integrationAuth, accessToken });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for integration authorization'
});
}
export {
validateClientForIntegrationAuth
};

@ -1,5 +1,106 @@
import * as Sentry from '@sentry/node';
import { Membership, Key } from '../models';
import { Types } from 'mongoose';
import {
Membership,
Key,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import {
MembershipNotFoundError,
BadRequestError,
UnauthorizedRequestError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import {
validateUserClientForWorkspace
} from '../helpers/user';
import {
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
import {
validateServiceTokenDataClientForWorkspace
} from '../helpers/serviceTokenData';
/**
* Validate authenticated clients for membership with id [membershipId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.membershipId - id of membership to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspaceRoles
* @returns {Membership} - validated membership
*/
const validateClientForMembership = async ({
authData,
membershipId,
acceptedRoles
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
membershipId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
}) => {
const membership = await Membership.findById(membershipId);
if (!membership) throw MembershipNotFoundError({
message: 'Failed to find membership'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: membership.workspace,
acceptedRoles
});
return membership;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: membership.workspace
});
return membership;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId: new Types.ObjectId(membership.workspace)
});
return membership;
}
if (authData.authMode == AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: membership.workspace,
acceptedRoles
});
return membership;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for membership'
});
}
/**
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
@ -14,28 +115,24 @@ const validateMembership = async ({
workspaceId,
acceptedRoles,
}: {
userId: string;
workspaceId: string;
acceptedRoles: string[];
userId: Types.ObjectId;
workspaceId: Types.ObjectId;
acceptedRoles?: Array<'admin' | 'member'>;
}) => {
let membership;
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
try {
membership = await Membership.findOne({
user: userId,
workspace: workspaceId
}).populate("workspace");
if (!membership) throw new Error('Failed to find membership');
const membership = await Membership.findOne({
user: userId,
workspace: workspaceId
}).populate("workspace");
if (!membership) {
throw MembershipNotFoundError({ message: 'Failed to find workspace membership' });
}
if (acceptedRoles) {
if (!acceptedRoles.includes(membership.role)) {
throw new Error('Failed to validate membership role');
throw BadRequestError({ message: 'Failed authorization for membership role' });
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to validate membership');
}
return membership;
@ -133,6 +230,7 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
};
export {
validateClientForMembership,
validateMembership,
addMemberships,
findMembership,

@ -1,40 +1,140 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { MembershipOrg, Workspace, Membership, Key } from '../models';
import {
MembershipOrg,
Workspace,
Membership,
Key,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import {
MembershipOrgNotFoundError,
BadRequestError,
UnauthorizedRequestError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
* Validate authenticated clients for organization membership with id [membershipOrgId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.membershipOrgId - id of organization membership to validate against
* @param {Array<'owner' | 'admin' | 'member'>} obj.acceptedRoles - accepted organization roles
* @param {MembershipOrg} - validated organization membership
*/
const validateClientForMembershipOrg = async ({
authData,
membershipOrgId,
acceptedRoles,
acceptedStatuses
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
membershipOrgId: Types.ObjectId;
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
}) => {
const membershipOrg = await MembershipOrg.findById(membershipOrgId);
if (!membershipOrg) throw MembershipOrgNotFoundError({
message: 'Failed to find organization membership '
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateMembershipOrg({
userId: authData.authPayload._id,
organizationId: membershipOrg.organization,
acceptedRoles,
acceptedStatuses
});
return membershipOrg;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
if (!authData.authPayload.organization.equals(membershipOrg.organization)) throw UnauthorizedRequestError({
message: 'Failed service account client authorization for organization membership'
});
return membershipOrg;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service account client authorization for organization membership'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateMembershipOrg({
userId: authData.authPayload._id,
organizationId: membershipOrg.organization,
acceptedRoles,
acceptedStatuses
});
return membershipOrg;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for organization membership'
});
}
/**
* Validate that user with id [userId] is a member of organization with id [organizationId]
* and has at least one of the roles in [acceptedRoles]
*
* @param {Object} obj
* @param {Types.ObjectId} obj.userId
* @param {Types.ObjectId} obj.organizationId
* @param {String[]} obj.acceptedRoles
*/
const validateMembership = async ({
const validateMembershipOrg = async ({
userId,
organizationId,
acceptedRoles
acceptedRoles,
acceptedStatuses
}: {
userId: string;
organizationId: string;
acceptedRoles: string[];
userId: Types.ObjectId;
organizationId: Types.ObjectId;
acceptedRoles?: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses?: Array<'invited' | 'accepted'>;
}) => {
let membership;
try {
membership = await MembershipOrg.findOne({
user: new Types.ObjectId(userId),
organization: new Types.ObjectId(organizationId)
});
if (!membership) throw new Error('Failed to find organization membership');
if (!acceptedRoles.includes(membership.role)) {
throw new Error('Failed to validate organization membership role');
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to validate organization membership');
const membershipOrg = await MembershipOrg.findOne({
user: userId,
organization: organizationId
});
if (!membershipOrg) {
throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' });
}
return membership;
if (acceptedRoles) {
if (!acceptedRoles.includes(membershipOrg.role)) {
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership role' });
}
}
if (acceptedStatuses) {
if (!acceptedStatuses.includes(membershipOrg.status)) {
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership status' });
}
}
return membershipOrg;
}
/**
@ -156,7 +256,8 @@ const deleteMembershipOrg = async ({
};
export {
validateMembership,
validateClientForMembershipOrg,
validateMembershipOrg,
findMembershipOrg,
addMembershipsOrg,
deleteMembershipOrg

@ -1,14 +1,110 @@
import * as Sentry from '@sentry/node';
import Stripe from 'stripe';
import { Types } from 'mongoose';
import { ACCEPTED } from '../variables';
import {
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import { Organization, MembershipOrg } from '../models';
import {
ACCEPTED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
OWNER
} from '../variables';
import {
getStripeSecretKey,
getStripeProductPro,
getStripeProductTeam,
getStripeProductStarter
} from '../config';
import {
UnauthorizedRequestError,
OrganizationNotFoundError
} from '../utils/errors';
import {
validateUserClientForOrganization
} from '../helpers/user';
import {
validateServiceAccountClientForOrganization
} from '../helpers/serviceAccount';
/**
* Validate accepted clients for organization with id [organizationId]
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.organizationId - id of organization to validate against
*/
const validateClientForOrganization = async ({
authData,
organizationId,
acceptedRoles,
acceptedStatuses
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
organizationId: Types.ObjectId;
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
}) => {
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: 'Failed to find organization'
});
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
const membershipOrg = await validateUserClientForOrganization({
user: authData.authPayload,
organization,
acceptedRoles,
acceptedStatuses
});
return ({ organization, membershipOrg });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForOrganization({
serviceAccount: authData.authPayload,
organization
});
return ({ organization });
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for organization'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
const membershipOrg = await validateUserClientForOrganization({
user: authData.authPayload,
organization,
acceptedRoles,
acceptedStatuses
});
return ({ organization, membershipOrg });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for organization'
});
}
/**
* Create an organization with name [name]
@ -172,6 +268,7 @@ const updateSubscriptionOrgQuantity = async ({
};
export {
validateClientForOrganization,
createOrganization,
initSubscriptionOrg,
updateSubscriptionOrgQuantity

@ -15,7 +15,7 @@ const apiLimiter = rateLimit({
});
// 10 requests per minute
const authLimiter = rateLimit({
const authLimit = rateLimit({
windowMs: 60 * 1000,
max: 10,
standardHeaders: true,
@ -36,8 +36,16 @@ const passwordLimiter = rateLimit({
}
});
export {
apiLimiter,
authLimiter,
passwordLimiter
const authLimiter = (req: any, res: any, next: any) => {
if (process.env.NODE_ENV === 'production') {
authLimit(req, res, next);
} else {
next();
}
};
export {
apiLimiter,
authLimiter,
passwordLimiter
};

@ -21,60 +21,8 @@ import {
ACTION_READ_SECRETS
} from '../variables';
import _ from 'lodash';
import { ABILITY_WRITE } from '../variables/organization';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
/**
* Validate that user with id [userId] can modify secrets with ids [secretIds]
* @param {Object} obj
* @param {Object} obj.userId - id of user to validate
* @param {Object} obj.secretIds - secret ids
* @returns {Secret[]} secrets
*/
const validateSecrets = async ({
userId,
secretIds
}: {
userId: string;
secretIds: string[];
}) => {
let secrets;
try {
secrets = await Secret.find({
_id: {
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
}
});
if (secrets.length != secretIds.length) {
throw BadRequestError({ message: 'Unable to validate some secrets' })
}
const userMemberships = await Membership.find({ user: userId })
const userMembershipById = _.keyBy(userMemberships, 'workspace');
const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString()));
// for each secret check if the secret belongs to a workspace the user is a member of
secrets.forEach((secret: ISecret) => {
if (workspaceIdsSet.has(secret.workspace.toString())) {
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: ABILITY_WRITE });
if (isDisallowed) {
throw UnauthorizedRequestError({ message: 'You do not have the required permissions to perform this action' });
}
} else {
throw BadRequestError({ message: 'You cannot edit secrets of a workspace you are not a member of' });
}
});
} catch (err) {
throw BadRequestError({ message: 'Unable to validate secrets' })
}
return secrets;
}
interface V1PushSecret {
ciphertextKey: string;
ivKey: string;
@ -714,7 +662,6 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
};
export {
validateSecrets,
v1PushSecrets,
v2PushSecrets,
pullSecrets,

@ -0,0 +1,198 @@
import { Types } from 'mongoose';
import {
User,
IUser,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
Secret,
ISecret
} from '../models';
import {
validateMembership
} from '../helpers/membership';
import {
validateUserClientForSecret,
validateUserClientForSecrets
} from '../helpers/user';
import {
validateServiceTokenDataClientForSecrets, validateServiceTokenDataClientForWorkspace
} from '../helpers/serviceTokenData';
import {
validateServiceAccountClientForSecrets,
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
import {
BadRequestError,
UnauthorizedRequestError,
SecretNotFoundError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
* Validate authenticated clients for secrets with id [secretId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.secretId - id of secret to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForSecret = async ({
authData,
secretId,
acceptedRoles,
requiredPermissions
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
secretId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions: string[];
}) => {
const secret = await Secret.findById(secretId);
if (!secret) throw SecretNotFoundError({
message: 'Failed to find secret'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForSecret({
user: authData.authPayload,
secret,
acceptedRoles,
requiredPermissions
});
return secret;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: secret.workspace,
environment: secret.environment,
requiredPermissions
});
return secret;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId: secret.workspace,
environment: secret.environment
});
return secret;
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForSecret({
user: authData.authPayload,
secret,
acceptedRoles,
requiredPermissions
});
return secret;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for secret'
});
}
/**
* Validate authenticated clients for secrets with ids [secretIds] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId[]} obj.secretIds - id of workspace to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForSecrets = async ({
authData,
secretIds,
requiredPermissions
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
secretIds: Types.ObjectId[];
requiredPermissions: string[];
}) => {
let secrets: ISecret[] = [];
secrets = await Secret.find({
_id: {
$in: secretIds
}
});
if (secrets.length != secretIds.length) {
throw BadRequestError({ message: 'Failed to validate non-existent secrets' })
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForSecrets({
user: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForSecrets({
serviceAccount: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForSecrets({
serviceTokenData: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForSecrets({
user: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for secrets resource'
});
}
export {
validateClientForSecret,
validateClientForSecrets
}

@ -0,0 +1,271 @@
import _ from 'lodash';
import { Types } from 'mongoose';
import {
User,
IUser,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
ISecret,
IOrganization,
IServiceAccountWorkspacePermission,
ServiceAccountWorkspacePermission
} from '../models';
import {
BadRequestError,
UnauthorizedRequestError,
ServiceAccountNotFoundError
} from '../utils/errors';
import {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import {
validateUserClientForServiceAccount
} from '../helpers/user';
const validateClientForServiceAccount = async ({
authData,
serviceAccountId,
requiredPermissions
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
serviceAccountId: Types.ObjectId;
requiredPermissions?: string[];
}) => {
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
if (!serviceAccount) {
throw ServiceAccountNotFoundError({
message: 'Failed to find service account'
});
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForServiceAccount({
user: authData.authPayload,
serviceAccount,
requiredPermissions
});
return serviceAccount;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForServiceAccount({
serviceAccount: authData.authPayload,
targetServiceAccount: serviceAccount,
requiredPermissions
});
return serviceAccount;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for service account resource'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForServiceAccount({
user: authData.authPayload,
serviceAccount,
requiredPermissions
});
return serviceAccount;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for service account resource'
});
}
/**
* Validate that service account (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {ServiceAccount} obj.serviceAccount - service account client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceAccountClientForWorkspace = async ({
serviceAccount,
workspaceId,
environment,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
workspaceId: Types.ObjectId;
environment?: string;
requiredPermissions?: string[];
}) => {
if (environment) {
// case: environment specified ->
// evaluate service account authorization for workspace
// in the context of a specific environment [environment]
const permission = await ServiceAccountWorkspacePermission.findOne({
serviceAccount,
workspace: new Types.ObjectId(workspaceId),
environment
});
if (!permission) throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given workspace environment'
});
let runningIsDisallowed = false;
requiredPermissions?.forEach((requiredPermission: string) => {
switch (requiredPermission) {
case PERMISSION_READ_SECRETS:
if (!permission.read) runningIsDisallowed = true;
break;
case PERMISSION_WRITE_SECRETS:
if (!permission.write) runningIsDisallowed = true;
break;
default:
break;
}
if (runningIsDisallowed) {
throw UnauthorizedRequestError({
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
});
}
});
} else {
// case: no environment specified ->
// evaluate service account authorization for workspace
// without need of environment [environment]
const permission = await ServiceAccountWorkspacePermission.findOne({
serviceAccount,
workspace: new Types.ObjectId(workspaceId)
});
if (!permission) throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given workspace'
});
}
}
/**
* Validate that service account (client) can access secrets
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {ServiceAccount} obj.serviceAccount - service account client
* @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceAccountClientForSecrets = async ({
serviceAccount,
secrets,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
const permissions = await ServiceAccountWorkspacePermission.find({
serviceAccount: serviceAccount._id
});
const permissionsObj = _.keyBy(permissions, (p) => {
return `${p.workspace.toString()}-${p.environment}`
});
secrets.forEach((secret: ISecret) => {
const permission = permissionsObj[`${secret.workspace.toString()}-${secret.environment}`];
if (!permission) throw BadRequestError({
message: 'Failed to find any permission for the secret workspace and environment'
});
requiredPermissions?.forEach((requiredPermission: string) => {
let runningIsDisallowed = false;
requiredPermissions?.forEach((requiredPermission: string) => {
switch (requiredPermission) {
case PERMISSION_READ_SECRETS:
if (!permission.read) runningIsDisallowed = true;
break;
case PERMISSION_WRITE_SECRETS:
if (!permission.write) runningIsDisallowed = true;
break;
default:
break;
}
if (runningIsDisallowed) {
throw UnauthorizedRequestError({
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
});
}
});
});
});
}
/**
* Validate that service account (client) can access target service
* account [serviceAccount] with required permissions [requiredPermissions]
* @param {Object} obj
* @param {SerivceAccount} obj.serviceAccount - service account client
* @param {ServiceAccount} targetServiceAccount - target service account to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceAccountClientForServiceAccount = ({
serviceAccount,
targetServiceAccount,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
targetServiceAccount: IServiceAccount;
requiredPermissions?: string[];
}) => {
if (!serviceAccount.organization.equals(targetServiceAccount.organization)) {
throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given service account'
});
}
}
/**
* Validate that service account (client) can access organization [organization]
* @param {Object} obj
* @param {User} obj.user - service account client
* @param {Organization} obj.organization - organization to validate against
*/
const validateServiceAccountClientForOrganization = async ({
serviceAccount,
organization
}: {
serviceAccount: IServiceAccount;
organization: IOrganization;
}) => {
if (!serviceAccount.organization.equals(organization._id)) {
throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given organization'
});
}
}
export {
validateClientForServiceAccount,
validateServiceAccountClientForWorkspace,
validateServiceAccountClientForSecrets,
validateServiceAccountClientForServiceAccount,
validateServiceAccountClientForOrganization
}

@ -0,0 +1,189 @@
import { Types } from 'mongoose';
import {
ISecret,
IServiceTokenData,
ServiceTokenData,
IUser,
User,
IServiceAccount,
ServiceAccount,
} from '../models';
import {
UnauthorizedRequestError,
ServiceTokenDataNotFoundError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
/**
* Validate authenticated clients for service token with id [serviceTokenId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.serviceTokenData - id of service token to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
*/
const validateClientForServiceTokenData = async ({
authData,
serviceTokenDataId,
acceptedRoles
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
serviceTokenDataId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
}) => {
const serviceTokenData = await ServiceTokenData
.findById(serviceTokenDataId)
.select('+encryptedKey +iv +tag')
.populate<{ user: IUser }>('user');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({
message: 'Failed to find service token data'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: serviceTokenData.workspace,
acceptedRoles
});
return serviceTokenData;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: serviceTokenData.workspace
});
return serviceTokenData;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for service token data'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: serviceTokenData.workspace,
acceptedRoles
});
return serviceTokenData;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for service token data'
});
}
/**
* Validate that service token (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceTokenDataClientForWorkspace = async ({
serviceTokenData,
workspaceId,
environment,
requiredPermissions
}: {
serviceTokenData: IServiceTokenData;
workspaceId: Types.ObjectId;
environment?: string;
requiredPermissions?: string[];
}) => {
if (!serviceTokenData.workspace.equals(workspaceId)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace'
});
}
if (environment) {
// case: environment is specified
if (serviceTokenData.environment !== environment) {
// case: invalid environment passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace environment'
});
}
requiredPermissions?.forEach((permission) => {
if (!serviceTokenData.permissions.includes(permission)) {
throw UnauthorizedRequestError({
message: `Failed service token authorization for the given workspace environment action: ${permission}`
});
}
});
}
}
/**
* Validate that service token (client) can access secrets
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceTokenDataClientForSecrets = async ({
serviceTokenData,
secrets,
requiredPermissions
}: {
serviceTokenData: IServiceTokenData;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
secrets.forEach((secret: ISecret) => {
if (!serviceTokenData.workspace.equals(secret.workspace)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace'
});
}
if (serviceTokenData.environment !== secret.environment) {
// case: invalid environment passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace environment'
});
}
requiredPermissions?.forEach((permission) => {
if (!serviceTokenData.permissions.includes(permission)) {
throw UnauthorizedRequestError({
message: `Failed service token authorization for the given workspace environment action: ${permission}`
});
}
});
});
}
export {
validateClientForServiceTokenData,
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataClientForSecrets
}

@ -1,6 +1,25 @@
import * as Sentry from '@sentry/node';
import { IUser, User } from '../models';
import { Types } from 'mongoose';
import {
IUser,
ISecret,
IServiceAccount,
User,
Membership,
IOrganization,
Organization,
} from '../models';
import { sendMail } from './nodemailer';
import { validateMembership } from './membership';
import _ from 'lodash';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import {
validateMembershipOrg
} from '../helpers/membershipOrg';
import {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../variables';
/**
* Initialize a user under email [email]
@ -146,4 +165,204 @@ const checkUserDevice = async ({
}
}
export { setupAccount, completeAccount, checkUserDevice };
/**
* Validate that user (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForWorkspace = async ({
user,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
}: {
user: IUser;
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions?: string[];
}) => {
// validate user membership in workspace
const membership = await validateMembership({
userId: user._id,
workspaceId,
acceptedRoles
});
let runningIsDisallowed = false;
requiredPermissions?.forEach((requiredPermission: string) => {
switch (requiredPermission) {
case PERMISSION_READ_SECRETS:
runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
break;
case PERMISSION_WRITE_SECRETS:
runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
break;
default:
break;
}
if (runningIsDisallowed) {
throw UnauthorizedRequestError({
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
});
}
});
return membership;
}
/**
* Validate that user (client) can access secret [secret]
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Secret[]} obj.secrets - secrets to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForSecret = async ({
user,
secret,
acceptedRoles,
requiredPermissions
}: {
user: IUser;
secret: ISecret;
acceptedRoles?: Array<'admin' | 'member'>;
requiredPermissions?: string[];
}) => {
const membership = await validateMembership({
userId: user._id,
workspaceId: secret.workspace,
acceptedRoles
});
if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) {
const isDisallowed = _.some(membership.deniedPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS });
if (isDisallowed) {
throw UnauthorizedRequestError({
message: 'You do not have the required permissions to perform this action'
});
}
}
}
/**
* Validate that user (client) can access secrets [secrets]
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Secret[]} obj.secrets - secrets to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForSecrets = async ({
user,
secrets,
requiredPermissions
}: {
user: IUser;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
// TODO: add acceptedRoles?
const userMemberships = await Membership.find({ user: user._id })
const userMembershipById = _.keyBy(userMemberships, 'workspace');
const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString()));
// for each secret check if the secret belongs to a workspace the user is a member of
secrets.forEach((secret: ISecret) => {
if (!workspaceIdsSet.has(secret.workspace.toString())) {
throw BadRequestError({
message: 'Failed authorization for the secret'
});
}
if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) {
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS });
if (isDisallowed) {
throw UnauthorizedRequestError({
message: 'You do not have the required permissions to perform this action'
});
}
}
});
}
/**
* Validate that user (client) can access service account [serviceAccount]
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {ServiceAccount} obj.serviceAccount - service account to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForServiceAccount = async ({
user,
serviceAccount,
requiredPermissions
}: {
user: IUser;
serviceAccount: IServiceAccount;
requiredPermissions?: string[];
}) => {
if (!serviceAccount.user.equals(user._id)) {
// case: user who created service account is not the
// same user that is on the request
await validateMembershipOrg({
userId: user._id,
organizationId: serviceAccount.organization,
acceptedRoles: [],
acceptedStatuses: []
});
}
}
/**
* Validate that user (client) can access organization [organization]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Organization} obj.organization - organization to validate against
*/
const validateUserClientForOrganization = async ({
user,
organization,
acceptedRoles,
acceptedStatuses
}: {
user: IUser;
organization: IOrganization;
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
}) => {
const membershipOrg = await validateMembershipOrg({
userId: user._id,
organizationId: organization._id,
acceptedRoles,
acceptedStatuses
});
return membershipOrg;
}
export {
setupAccount,
completeAccount,
checkUserDevice,
validateUserClientForWorkspace,
validateUserClientForSecrets,
validateUserClientForServiceAccount,
validateUserClientForOrganization,
validateUserClientForSecret
};

@ -1,12 +1,115 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Workspace,
Bot,
Membership,
Key,
Secret
Secret,
User,
IUser,
ServiceAccountWorkspacePermission,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
} from '../models';
import { createBot } from '../helpers/bot';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData';
import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
* Validate authenticated clients for workspace with id [workspaceId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForWorkspace = async ({
authData,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions?: string[];
}) => {
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw WorkspaceNotFoundError({
message: 'Failed to find workspace'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
return ({ membership });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
return {};
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
return {};
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
return ({ membership });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for workspace'
});
}
/**
* Create a workspace with name [name] in organization with id [organizationId]
@ -71,4 +174,8 @@ const deleteWorkspace = async ({ id }: { id: string }) => {
}
};
export { createWorkspace, deleteWorkspace };
export {
validateClientForWorkspace,
createWorkspace,
deleteWorkspace
};

@ -9,7 +9,7 @@ import * as Sentry from '@sentry/node';
import { DatabaseService } from './services';
import { setUpHealthEndpoint } from './services/health';
import { initSmtp } from './services/smtp';
import { logTelemetryMessage } from './services';
import { TelemetryService } from './services';
import { setTransporter } from './helpers/nodemailer';
import { createTestUserForDevelopment } from './utils/addDevelopmentUser';
// eslint-disable-next-line @typescript-eslint/no-var-requires
@ -56,6 +56,7 @@ import {
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
serviceAccounts as v2ServiceAccountsRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
@ -69,7 +70,8 @@ import {
getNodeEnv,
getPort,
getSentryDSN,
getSiteURL
getSiteURL,
getSmtpHost
} from './config';
const main = async () => {
@ -79,7 +81,7 @@ const main = async () => {
});
}
logTelemetryMessage();
TelemetryService.logTelemetryMessage();
setTransporter(initSmtp());
await DatabaseService.initDatabase(getMongoURL());
@ -150,6 +152,7 @@ const main = async () => {
app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new
app.use('/api/v2/api-key', v2APIKeyDataRouter);
// api docs
@ -170,7 +173,7 @@ const main = async () => {
getLogger("backend-main").info(`Server started listening at port ${getPort()}`)
});
createTestUserForDevelopment();
await createTestUserForDevelopment();
setUpHealthEndpoint(server);
server.on('close', async () => {

@ -12,17 +12,21 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL
} from "../variables";
interface App {
@ -94,6 +98,11 @@ const getApps = async ({
accessToken,
});
break;
case INTEGRATION_RAILWAY:
apps = await getAppsRailway({
accessToken
});
break;
case INTEGRATION_FLYIO:
apps = await getAppsFlyio({
accessToken,
@ -109,6 +118,11 @@ const getApps = async ({
accessToken,
})
break;
case INTEGRATION_SUPABASE:
apps = await getAppsSupabase({
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
@ -184,6 +198,7 @@ const getAppsVercel = async ({
apps = res.projects.map((a: any) => ({
name: a.name,
appId: a.id
}));
} catch (err) {
Sentry.setUser(null);
@ -270,10 +285,13 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
apps = repos
.filter((a: any) => a.permissions.admin === true)
.map((a: any) => ({
name: a.name,
owner: a.owner.login,
}));
.map((a: any) => {
return ({
appId: a.id,
name: a.name,
owner: a.owner.login,
});
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
@ -319,6 +337,58 @@ const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
return apps;
};
/**
* Return list of projects for Railway integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Railway API
* @returns {Object[]} apps - names and ids of Railway services
* @returns {String} apps.name - name of Railway project
* @returns {String} apps.appId - id of Railway project
*
*/
const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
let apps: any[] = [];
try {
const query = `
query GetProjects($userId: String, $teamId: String) {
projects(userId: $userId, teamId: $teamId) {
edges {
node {
id
name
}
}
}
}
`;
const variables = {};
const { data: { data: { projects: { edges }}} } = await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables,
}, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
},
});
apps = edges.map((e: any) => ({
name: e.node.name,
appId: e.node.id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Railway services");
}
return apps;
}
/**
* Return list of apps for Fly.io integration
* @param {Object} obj
@ -545,4 +615,40 @@ const getAppsGitlab = async ({
return apps;
}
/**
* Return list of projects for Supabase integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Supabase API
* @returns {Object[]} apps - names of Supabase apps
* @returns {String} apps.name - name of Supabase app
*/
const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => {
let apps: any;
try {
const { data } = await request.get(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
apps = data.map((a: any) => {
return {
name: a.name,
appId: a.id
};
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Supabase projects');
}
return apps;
};
export { getApps };

@ -21,19 +21,24 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL
} from "../variables";
import request from '../config/request';
import axios from "axios";
/**
* Sync/push [secrets] to [app] in integration named [integration]
@ -126,6 +131,13 @@ const syncSecrets = async ({
accessToken,
});
break;
case INTEGRATION_RAILWAY:
await syncSecretsRailway({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_FLYIO:
await syncSecretsFlyio({
integration,
@ -147,6 +159,13 @@ const syncSecrets = async ({
accessToken,
});
break;
case INTEGRATION_SUPABASE:
await syncSecretsSupabase({
integration,
secrets,
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
@ -608,6 +627,7 @@ const syncSecretsVercel = async ({
key: string;
value: string;
target: string[];
gitBranch?: string;
}
try {
@ -621,46 +641,7 @@ const syncSecretsVercel = async ({
}
: {}),
};
// const res = (
// await Promise.all(
// (
// await request.get(
// `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
// {
// params,
// headers: {
// Authorization: `Bearer ${accessToken}`,
// 'Accept-Encoding': 'application/json'
// }
// }
// ))
// .data
// .envs
// .filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment))
// .map(async (secret: VercelSecret) => {
// if (secret.type === 'encrypted') {
// // case: secret is encrypted -> need to decrypt
// const decryptedSecret = (await request.get(
// `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
// {
// params,
// headers: {
// Authorization: `Bearer ${accessToken}`,
// 'Accept-Encoding': 'application/json'
// }
// }
// )).data;
// return decryptedSecret;
// }
// return secret;
// }))).reduce((obj: any, secret: any) => ({
// ...obj,
// [secret.key]: secret
// }), {});
const vercelSecrets: VercelSecret[] = (await request.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
{
@ -673,7 +654,21 @@ const syncSecretsVercel = async ({
))
.data
.envs
.filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment));
.filter((secret: VercelSecret) => {
if (!secret.target.includes(integration.targetEnvironment)) {
// case: secret does not have the same target environment
return false;
}
if (integration.targetEnvironment === 'preview' && integration.path && integration.path !== secret.gitBranch) {
// case: secret on preview environment does not have same target git branch
return false;
}
return true;
});
// return secret.target.includes(integration.targetEnvironment);
const res: { [key: string]: VercelSecret } = {};
@ -696,7 +691,7 @@ const syncSecretsVercel = async ({
res[vercelSecret.key] = vercelSecret;
}
}
const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = [];
@ -710,6 +705,9 @@ const syncSecretsVercel = async ({
value: secrets[key],
type: "encrypted",
target: [integration.targetEnvironment],
...(integration.path ? {
gitBranch: integration.path
} : {})
});
}
});
@ -726,7 +724,10 @@ const syncSecretsVercel = async ({
type: res[key].type,
target: res[key].target.includes(integration.targetEnvironment)
? [...res[key].target]
: [...res[key].target, integration.targetEnvironment]
: [...res[key].target, integration.targetEnvironment],
...(integration.path ? {
gitBranch: integration.path
} : {})
});
}
} else {
@ -737,6 +738,9 @@ const syncSecretsVercel = async ({
value: res[key].value,
type: "encrypted", // value doesn't matter
target: [integration.targetEnvironment],
...(integration.path ? {
gitBranch: integration.path
} : {})
});
}
});
@ -1060,7 +1064,7 @@ const syncSecretsGitHub = async ({
"GET /repos/{owner}/{repo}/actions/secrets/public-key",
{
owner: integration.owner,
repo: integration.app,
repo: integration.app
}
)
).data;
@ -1167,6 +1171,58 @@ const syncSecretsRender = async ({
}
};
/**
* Sync/push [secrets] to Railway project with id [integration.appId]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Railway integration
*/
const syncSecretsRailway = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
const query = `
mutation UpsertVariables($input: VariableCollectionUpsertInput!) {
variableCollectionUpsert(input: $input)
}
`;
const input = {
projectId: integration.appId,
environmentId: integration.targetEnvironmentId,
...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}),
replace: true,
variables: secrets
};
await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables: {
input,
},
}, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
},
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to Railway");
}
}
/**
* Sync/push [secrets] to Fly.io app
* @param {Object} obj
@ -1571,4 +1627,79 @@ const syncSecretsGitLab = async ({
}
}
/**
* Sync/push [secrets] to Supabase with name [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Supabase integration
*/
const syncSecretsSupabase = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
const { data: getSecretsRes } = await request.get(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
// convert the secrets to [{}] format
const modifiedFormatForSecretInjection = Object.keys(secrets).map(
(key) => {
return {
name: key,
value: secrets[key]
};
}
);
await request.post(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
modifiedFormatForSecretInjection,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
const secretsToDelete: any = [];
getSecretsRes?.forEach((secretObj: any) => {
if (!(secretObj.name in secrets)) {
secretsToDelete.push(secretObj.name);
}
});
await request.delete(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
},
data: secretsToDelete
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Supabase');
}
};
export { syncSecrets };

@ -0,0 +1,7 @@
interface AddServiceAccountPermissionDto {
name: string;
workspaceId?: string;
environment?: string;
}
export default AddServiceAccountPermissionDto;

@ -0,0 +1,8 @@
interface CreateServiceAccountDto {
organizationId: string;
name: string;
publicKey: string;
expiresIn: number;
}
export default CreateServiceAccountDto;

@ -0,0 +1,7 @@
import CreateServiceAccountDto from './CreateServiceAccountDto';
import AddServiceAccountPermissionDto from './AddServiceAccountPermissionDto';
export {
CreateServiceAccountDto,
AddServiceAccountPermissionDto
}

@ -10,6 +10,8 @@ import requireIntegrationAuth from './requireIntegrationAuth';
import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth';
import requireServiceTokenAuth from './requireServiceTokenAuth';
import requireServiceTokenDataAuth from './requireServiceTokenDataAuth';
import requireServiceAccountAuth from './requireServiceAccountAuth';
import requireServiceAccountWorkspacePermissionAuth from './requireServiceAccountWorkspacePermissionAuth';
import requireSecretAuth from './requireSecretAuth';
import requireSecretsAuth from './requireSecretsAuth';
import validateRequest from './validateRequest';
@ -27,6 +29,8 @@ export {
requireIntegrationAuthorizationAuth,
requireServiceTokenAuth,
requireServiceTokenDataAuth,
requireServiceAccountAuth,
requireServiceAccountWorkspacePermissionAuth,
requireSecretAuth,
requireSecretsAuth,
validateRequest

@ -4,11 +4,23 @@ import {
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
getAuthAPIKeyPayload
getAuthAPIKeyPayload,
getAuthSAAKPayload
} from '../helpers/auth';
import {
UnauthorizedRequestError
} from '../utils/errors';
import {
IUser,
IServiceAccount,
IServiceTokenData
} from '../models';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -27,50 +39,58 @@ declare module 'jsonwebtoken' {
* @returns
*/
const requireAuth = ({
acceptedAuthModes = ['jwt'],
requiredServiceTokenPermissions = []
acceptedAuthModes = [AUTH_MODE_JWT],
}: {
acceptedAuthModes: string[];
requiredServiceTokenPermissions?: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// validate auth token against accepted auth modes [acceptedAuthModes]
// and return token type [authTokenType] and value [authTokenValue]
const { authTokenType, authTokenValue } = validateAuthMode({
const { authMode, authTokenValue } = validateAuthMode({
headers: req.headers,
acceptedAuthModes
});
// attach auth payloads
let serviceTokenData: any;
switch (authTokenType) {
case 'serviceToken':
serviceTokenData = await getAuthSTDPayload({
let authPayload: IUser | IServiceAccount | IServiceTokenData;
switch (authMode) {
case AUTH_MODE_SERVICE_ACCOUNT:
authPayload = await getAuthSAAKPayload({
authTokenValue
});
requiredServiceTokenPermissions.forEach((requiredServiceTokenPermission) => {
if (!serviceTokenData.permissions.includes(requiredServiceTokenPermission)) {
return next(UnauthorizedRequestError({ message: 'Failed to authorize service token for endpoint' }));
}
});
req.serviceTokenData = serviceTokenData;
req.user = serviceTokenData?.user;
req.serviceAccount = authPayload;
break;
case 'apiKey':
req.user = await getAuthAPIKeyPayload({
case AUTH_MODE_SERVICE_TOKEN:
authPayload = await getAuthSTDPayload({
authTokenValue
});
req.serviceTokenData = authPayload;
break;
case AUTH_MODE_API_KEY:
authPayload = await getAuthAPIKeyPayload({
authTokenValue
});
req.user = authPayload;
break;
default:
req.user = await getAuthUserPayload({
authPayload = await getAuthUserPayload({
authTokenValue
});
req.user = authPayload;
break;
}
req.requestData = {
...req.params,
...req.query,
...req.body,
}
req.authData = {
authMode,
authPayload // User, ServiceAccount, ServiceTokenData
}
return next();
}
}

@ -1,32 +1,28 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { Bot } from '../models';
import { validateMembership } from '../helpers/membership';
import { validateClientForBot } from '../helpers/bot';
import { AccountNotFoundError } from '../utils/errors';
type req = 'params' | 'body' | 'query';
const requireBotAuth = ({
acceptedRoles,
location = 'params'
locationBotId = 'params'
}: {
acceptedRoles: string[];
location?: req;
acceptedRoles: Array<'admin' | 'member'>;
locationBotId?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const bot = await Bot.findById(req[location].botId);
const { botId } = req[locationBotId];
if (!bot) {
return next(AccountNotFoundError({message: 'Failed to locate Bot account'}))
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: bot.workspace.toString(),
req.bot = await validateClientForBot({
authData: req.authData,
botId: new Types.ObjectId(botId),
acceptedRoles
});
req.bot = bot;
next();
}
}

@ -1,7 +1,9 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { Integration, IntegrationAuth } from '../models';
import { IntegrationService } from '../services';
import { validateMembership } from '../helpers/membership';
import { validateClientForIntegration } from '../helpers/integration';
import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors';
/**
@ -13,42 +15,24 @@ import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/err
const requireIntegrationAuth = ({
acceptedRoles
}: {
acceptedRoles: string[];
acceptedRoles: Array<'admin' | 'member'>;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// integration authorization middleware
const { integrationId } = req.params;
// validate integration accessibility
const integration = await Integration.findOne({
_id: integrationId
});
if (!integration) {
return next(IntegrationNotFoundError({message: 'Failed to locate Integration'}))
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: integration.workspace.toString(),
const { integration, accessToken } = await validateClientForIntegration({
authData: req.authData,
integrationId: new Types.ObjectId(integrationId),
acceptedRoles
});
const integrationAuth = await IntegrationAuth.findOne({
_id: integration.integrationAuth
}).select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) {
return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}))
if (integration) {
req.integration = integration;
}
if (accessToken) {
req.accessToken = accessToken;
}
req.integration = integration;
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString()
});
return next();
};

@ -1,7 +1,9 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express';
import { IntegrationAuth, IWorkspace } from '../models';
import { IntegrationService } from '../services';
import { validateClientForIntegrationAuth } from '../helpers/integrationAuth';
import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError } from '../utils/errors';
@ -19,36 +21,26 @@ const requireIntegrationAuthorizationAuth = ({
attachAccessToken = true,
location = 'params'
}: {
acceptedRoles: string[];
acceptedRoles: Array<'admin' | 'member'>;
attachAccessToken?: boolean;
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const { integrationAuthId } = req[location];
const integrationAuth = await IntegrationAuth.findOne({
_id: integrationAuthId
})
.populate<{ workspace: IWorkspace }>('workspace')
.select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) {
return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authorization credentials'}))
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: integrationAuth.workspace._id.toString(),
acceptedRoles
const { integrationAuth, accessToken } = await validateClientForIntegrationAuth({
authData: req.authData,
integrationAuthId: new Types.ObjectId(integrationAuthId),
acceptedRoles,
attachAccessToken
});
if (integrationAuth) {
req.integrationAuth = integrationAuth;
}
req.integrationAuth = integrationAuth;
if (attachAccessToken) {
const access = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString()
});
req.accessToken = access.accessToken;
if (accessToken) {
req.accessToken = accessToken;
}
return next();

@ -1,9 +1,13 @@
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import {
Membership,
} from '../models';
import { validateMembership } from '../helpers/membership';
import {
validateClientForMembership,
validateMembership
} from '../helpers/membership';
type req = 'params' | 'body' | 'query';
@ -16,43 +20,25 @@ type req = 'params' | 'body' | 'query';
*/
const requireMembershipAuth = ({
acceptedRoles,
location = 'params'
locationMembershipId = 'params'
}: {
acceptedRoles: string[];
location?: req;
acceptedRoles: Array<'admin' | 'member'>;
locationMembershipId: req
}) => {
return async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const { membershipId } = req[location];
const membership = await Membership.findById(membershipId);
if (!membership) throw new Error('Failed to find target membership');
const userMembership = await Membership.findOne({
workspace: membership.workspace
});
if (!userMembership) throw new Error('Failed to validate own membership')
const targetMembership = await validateMembership({
userId: req.user._id.toString(),
workspaceId: membership.workspace.toString(),
acceptedRoles
});
req.targetMembership = targetMembership;
return next();
} catch (err) {
return next(UnauthorizedRequestError({
message: 'Unable to validate workspace membership'
}));
}
const { membershipId } = req[locationMembershipId];
req.targetMembership = await validateClientForMembership({
authData: req.authData,
membershipId: new Types.ObjectId(membershipId),
acceptedRoles
});
return next();
}
}

@ -1,11 +1,17 @@
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import {
MembershipOrg
} from '../models';
import { validateMembership } from '../helpers/membershipOrg';
import {
validateClientForMembershipOrg,
validateMembershipOrg
} from '../helpers/membershipOrg';
// TODO: transform
type req = 'params' | 'body' | 'query';
/**
@ -17,32 +23,24 @@ type req = 'params' | 'body' | 'query';
*/
const requireMembershipOrgAuth = ({
acceptedRoles,
location = 'params'
acceptedStatuses,
locationMembershipOrgId = 'params'
}: {
acceptedRoles: string[];
location?: req;
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
locationMembershipOrgId?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { membershipId } = req[location];
const membershipOrg = await MembershipOrg.findById(membershipId);
if (!membershipOrg) throw new Error('Failed to find target organization membership');
const targetMembership = await validateMembership({
userId: req.user._id.toString(),
organizationId: membershipOrg.organization.toString(),
acceptedRoles
});
req.targetMembership = targetMembership;
return next();
} catch (err) {
return next(UnauthorizedRequestError({
message: 'Unable to validate organization membership'
}));
}
const { membershipId } = req[locationMembershipOrgId];
req.membershipOrg = await validateClientForMembershipOrg({
authData: req.authData,
membershipOrgId: new Types.ObjectId(membershipId),
acceptedRoles,
acceptedStatuses
});
return next();
}
}

@ -1,45 +1,46 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { IOrganization, MembershipOrg } from '../models';
import { UnauthorizedRequestError, ValidationError } from '../utils/errors';
import { validateMembershipOrg } from '../helpers/membershipOrg';
import { validateClientForOrganization } from '../helpers/organization';
type req = 'params' | 'body' | 'query';
/**
* Validate if user on request is a member with proper roles for organization
* on request params.
* @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted organization roles
* @param {String[]} obj.acceptedStatuses - accepted organization statuses
* @param {String[]} obj.accepteStatuses - accepted organization statuses
*/
const requireOrganizationAuth = ({
acceptedRoles,
acceptedStatuses
acceptedStatuses,
locationOrganizationId = 'params'
}: {
acceptedRoles: string[];
acceptedStatuses: string[];
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
locationOrganizationId?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// organization authorization middleware
// validate organization membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: req.params.organizationId
}).populate<{ organization: IOrganization }>('organization');
if (!membershipOrg) {
return next(UnauthorizedRequestError({message: "You're not a member of this Organization."}))
}
//TODO is this important to validate? I mean is it possible to save wrong role to database or get wrong role from databse? - Zamion101
if (!acceptedRoles.includes(membershipOrg.role)) {
return next(ValidationError({message: 'Failed to validate Organization Membership Role'}))
const { organizationId } = req[locationOrganizationId];
const { organization, membershipOrg } = await validateClientForOrganization({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId),
acceptedRoles,
acceptedStatuses
});
if (organization) {
req.organization = organization;
}
if (!acceptedStatuses.includes(membershipOrg.status)) {
return next(ValidationError({message: 'Failed to validate Organization Membership Status'}))
if (membershipOrg) {
req.membershipOrg = membershipOrg;
}
req.membershipOrg = membershipOrg;
return next();
};
};

@ -1,12 +1,17 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors';
import { Secret } from '../models';
import {
validateMembership
} from '../helpers/membership';
import {
validateClientForSecret
} from '../helpers/secrets';
// note: used for old /v1/secret and /v2/secret routes.
// newer /v2/secrets routes use [requireSecretsAuth] middleware
// newer /v2/secrets routes use [requireSecretsAuth] middleware with the exception
// of some /ee endpoints
/**
* Validate if user on request has proper membership to modify secret.
@ -15,34 +20,25 @@ import {
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
*/
const requireSecretAuth = ({
acceptedRoles
acceptedRoles,
requiredPermissions
}: {
acceptedRoles: string[];
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { secretId } = req.params;
const secret = await Secret.findById(secretId);
if (!secret) {
return next(SecretNotFoundError({
message: 'Failed to find secret'
}));
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: secret.workspace.toString(),
acceptedRoles
});
req._secret = secret;
const { secretId } = req.params;
const secret = await validateClientForSecret({
authData: req.authData,
secretId: new Types.ObjectId(secretId),
acceptedRoles,
requiredPermissions
});
req._secret = secret;
next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret' }));
}
next();
}
}

@ -1,48 +1,35 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { UnauthorizedRequestError } from '../utils/errors';
import { Secret, Membership } from '../models';
import { validateSecrets } from '../helpers/secret';
// TODO: make this work for delete route
import { validateClientForSecrets } from '../helpers/secrets';
const requireSecretsAuth = ({
acceptedRoles
acceptedRoles,
requiredPermissions = []
}: {
acceptedRoles: string[];
requiredPermissions?: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
let secrets;
try {
if (Array.isArray(req.body.secrets)) {
// case: validate multiple secrets
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: req.body.secrets.map((s: any) => s.id)
});
} else if (typeof req.body.secrets === 'object') { // change this to check for object
// case: validate 1 secret
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: [req.body.secrets.id]
});
} else if (Array.isArray(req.body.secretIds)) {
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: req.body.secretIds
});
} else if (typeof req.body.secretIds === 'string') {
// case: validate secretIds
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: [req.body.secretIds]
});
}
req.secrets = secrets;
return next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret(s)' }));
let secretIds = [];
if (Array.isArray(req.body.secrets)) {
secretIds = req.body.secrets.map((s: any) => s.id);
} else if (typeof req.body.secrets === 'object') {
secretIds = [req.body.secrets.id];
} else if (Array.isArray(req.body.secretIds)) {
secretIds = req.body.secretIds;
} else if (typeof req.body.secretIds === 'string') {
secretIds = [req.body.secretIds];
}
req.secrets = await validateClientForSecrets({
authData: req.authData,
secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
requiredPermissions
});
return next();
}
}

@ -0,0 +1,40 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { ServiceAccount } from '../models';
import {
ServiceAccountNotFoundError
} from '../utils/errors';
import {
validateMembershipOrg
} from '../helpers/membershipOrg';
import {
validateClientForServiceAccount
} from '../helpers/serviceAccount';
type req = 'params' | 'body' | 'query';
const requireServiceAccountAuth = ({
acceptedRoles,
acceptedStatuses,
locationServiceAccountId = 'params',
requiredPermissions = []
}: {
acceptedRoles: string[];
acceptedStatuses: string[];
locationServiceAccountId?: req;
requiredPermissions?: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const serviceAccountId = req[locationServiceAccountId].serviceAccountId;
req.serviceAccount = await validateClientForServiceAccount({
authData: req.authData,
serviceAccountId: new Types.ObjectId(serviceAccountId),
requiredPermissions
});
next();
}
}
export default requireServiceAccountAuth;

@ -0,0 +1,52 @@
import { Request, Response, NextFunction } from 'express';
import { ServiceAccount, ServiceAccountWorkspacePermission } from '../models';
import {
ServiceAccountNotFoundError
} from '../utils/errors';
import {
validateMembershipOrg
} from '../helpers/membershipOrg';
type req = 'params' | 'body' | 'query';
const requireServiceAccountWorkspacePermissionAuth = ({
acceptedRoles,
acceptedStatuses,
location = 'params'
}: {
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const serviceAccountWorkspacePermissionId = req[location].serviceAccountWorkspacePermissionId;
const serviceAccountWorkspacePermission = await ServiceAccountWorkspacePermission.findById(serviceAccountWorkspacePermissionId);
if (!serviceAccountWorkspacePermission) {
return next(ServiceAccountNotFoundError({ message: 'Failed to locate Service Account workspace permission' }));
}
const serviceAccount = await ServiceAccount.findById(serviceAccountWorkspacePermission.serviceAccount);
if (!serviceAccount) {
return next(ServiceAccountNotFoundError({ message: 'Failed to locate Service Account' }));
}
if (serviceAccount.user.toString() !== req.user.id.toString()) {
// case: creator of the service account is different from
// the user on the request -> apply middleware role/status validation
await validateMembershipOrg({
userId: req.user._id,
organizationId: serviceAccount.organization,
acceptedRoles,
acceptedStatuses
});
}
req.serviceAccount = serviceAccount;
next();
}
}
export default requireServiceAccountWorkspacePermissionAuth;

@ -1,5 +1,7 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { ServiceToken, ServiceTokenData } from '../models';
import { validateClientForServiceTokenData } from '../helpers/serviceTokenData';
import { validateMembership } from '../helpers/membership';
import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors';
@ -9,30 +11,17 @@ const requireServiceTokenDataAuth = ({
acceptedRoles,
location = 'params'
}: {
acceptedRoles: string[];
acceptedRoles: Array<'admin' | 'member'>;
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const { serviceTokenDataId } = req[location];
const serviceTokenData = await ServiceTokenData
.findById(req[location].serviceTokenDataId)
.select('+encryptedKey +iv +tag').populate('user');
if (!serviceTokenData) {
return next(AccountNotFoundError({ message: 'Failed to locate service token data' }));
}
if (req.user) {
// case: jwt auth
await validateMembership({
userId: req.user._id.toString(),
workspaceId: serviceTokenData.workspace.toString(),
acceptedRoles
});
}
req.serviceTokenData = serviceTokenData;
req.serviceTokenData = await validateClientForServiceTokenData({
authData: req.authData,
serviceTokenDataId: new Types.ObjectId(serviceTokenDataId),
acceptedRoles
});
next();
}

@ -1,5 +1,7 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { validateMembership } from '../helpers/membership';
import { validateClientForWorkspace } from '../helpers/workspace';
import { UnauthorizedRequestError } from '../utils/errors';
type req = 'params' | 'body' | 'query';
@ -13,38 +15,33 @@ type req = 'params' | 'body' | 'query';
*/
const requireWorkspaceAuth = ({
acceptedRoles,
location = 'params'
locationWorkspaceId,
locationEnvironment = undefined,
requiredPermissions = []
}: {
acceptedRoles: string[];
location?: req;
acceptedRoles: Array<'admin' | 'member'>;
locationWorkspaceId: req;
locationEnvironment?: req | undefined;
requiredPermissions?: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { workspaceId } = req[location];
if (req.user) {
// case: jwt auth
const membership = await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles
});
req.membership = membership;
}
if (
req.serviceTokenData
&& req.serviceTokenData.workspace.toString() !== workspaceId
&& req.serviceTokenData.environment !== req.body.environment
) {
next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
}
return next();
} catch (err) {
return next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
const workspaceId = req[locationWorkspaceId]?.workspaceId;
const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined;
// validate clients
const { membership } = await validateClientForWorkspace({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
acceptedRoles,
requiredPermissions
});
if (membership) {
req.membership = membership;
}
return next();
};
};

@ -3,6 +3,7 @@ import { Schema, model, Types } from 'mongoose';
export interface IAPIKeyData {
name: string;
user: Types.ObjectId;
lastUsed: Date;
expiresAt: Date;
secretHash: string;
}
@ -18,6 +19,9 @@ const apiKeyDataSchema = new Schema<IAPIKeyData>(
ref: 'User',
required: true
},
lastUsed: {
type: Date
},
expiresAt: {
type: Date
},

@ -10,6 +10,10 @@ import MembershipOrg, { IMembershipOrg } from './membershipOrg';
import Organization, { IOrganization } from './organization';
import Secret, { ISecret } from './secret';
import ServiceToken, { IServiceToken } from './serviceToken';
import ServiceAccount, { IServiceAccount } from './serviceAccount'; // new
import ServiceAccountKey, { IServiceAccountKey } from './serviceAccountKey'; // new
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from './serviceAccountOrganizationPermission'; // new
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from './serviceAccountWorkspacePermission'; // new
import TokenData, { ITokenData } from './tokenData';
import User, { IUser } from './user';
import UserAction, { IUserAction } from './userAction';
@ -43,6 +47,14 @@ export {
ISecret,
ServiceToken,
IServiceToken,
ServiceAccount,
IServiceAccount,
ServiceAccountKey,
IServiceAccountKey,
ServiceAccountOrganizationPermission,
IServiceAccountOrganizationPermission,
ServiceAccountWorkspacePermission,
IServiceAccountWorkspacePermission,
TokenData,
ITokenData,
User,

@ -9,9 +9,11 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE
} from "../variables";
export interface IIntegration {
@ -20,9 +22,12 @@ export interface IIntegration {
environment: string;
isActive: boolean;
app: string;
appId: string;
owner: string;
targetEnvironment: string;
appId: string;
targetEnvironmentId: string;
targetService: string;
targetServiceId: string;
path: string;
region: string;
integration:
@ -35,9 +40,11 @@ export interface IIntegration {
| 'github'
| 'gitlab'
| 'render'
| 'railway'
| 'flyio'
| 'circleci'
| 'travisci';
| 'travisci'
| 'supabase';
integrationAuth: Types.ObjectId;
}
@ -71,6 +78,20 @@ const integrationSchema = new Schema<IIntegration>(
type: String,
default: null,
},
targetEnvironmentId: {
type: String,
default: null
},
targetService: {
// railway-specific service
type: String,
default: null
},
targetServiceId: {
// railway-specific service
type: String,
default: null
},
owner: {
// github-specific repo owner-login
type: String,
@ -78,6 +99,7 @@ const integrationSchema = new Schema<IIntegration>(
},
path: {
// aws-parameter-store-specific path
// (also) vercel preview-branch
type: String,
default: null
},
@ -98,9 +120,11 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE
],
required: true,
},

@ -1,4 +1,4 @@
import { Schema, model, Types } from "mongoose";
import { Schema, model, Types, Document } from "mongoose";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@ -9,15 +9,17 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
} from "../variables";
export interface IIntegrationAuth {
export interface IIntegrationAuth extends Document {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'aws-parameter-store' | 'aws-secret-manager';
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager';
teamId: string;
accountId: string;
refreshCiphertext?: string;
@ -51,9 +53,11 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE
],
required: true,
},

@ -1,7 +1,7 @@
import { Schema, model, Types } from 'mongoose';
import { Schema, model, Types, Document } from 'mongoose';
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from '../variables';
export interface IMembershipOrg {
export interface IMembershipOrg extends Document {
_id: Types.ObjectId;
user: Types.ObjectId;
inviteEmail: string;

@ -0,0 +1,53 @@
import { Schema, model, Types, Document } from 'mongoose';
export interface IServiceAccount extends Document {
_id: Types.ObjectId;
name: string;
organization: Types.ObjectId;
user: Types.ObjectId;
publicKey: string;
lastUsed: Date;
expiresAt: Date;
secretHash: string;
}
const serviceAccountSchema = new Schema<IServiceAccount>(
{
name: {
type: String,
required: true
},
organization: {
type: Schema.Types.ObjectId,
ref: 'Organization',
required: true
},
user: { // user who created the service account
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
publicKey: {
type: String,
required: true
},
lastUsed: {
type: Date
},
expiresAt: {
type: Date
},
secretHash: {
type: String,
required: true,
select: false
}
},
{
timestamps: true
}
);
const ServiceAccount = model<IServiceAccount>('ServiceAccount', serviceAccountSchema);
export default ServiceAccount;

@ -0,0 +1,44 @@
import { Schema, model, Types } from 'mongoose';
export interface IServiceAccountKey {
_id: Types.ObjectId;
encryptedKey: string;
nonce: string;
sender: Types.ObjectId;
serviceAccount: Types.ObjectId;
workspace: Types.ObjectId;
}
const serviceAccountKeySchema = new Schema<IServiceAccountKey>(
{
encryptedKey: {
type: String,
required: true
},
nonce: {
type: String,
required: true
},
sender: {
type: Schema.Types.ObjectId,
required: true
},
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount',
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
}
},
{
timestamps: true
}
);
const ServiceAccountKey = model<IServiceAccountKey>('ServiceAccountKey', serviceAccountKeySchema);
export default ServiceAccountKey;

@ -0,0 +1,23 @@
import { Schema, model, Types, Document } from 'mongoose';
export interface IServiceAccountOrganizationPermission extends Document {
_id: Types.ObjectId;
serviceAccount: Types.ObjectId;
}
const serviceAccountOrganizationPermissionSchema = new Schema<IServiceAccountOrganizationPermission>(
{
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount',
required: true
}
},
{
timestamps: true
}
);
const ServiceAccountOrganizationPermission = model<IServiceAccountOrganizationPermission>('ServiceAccountOrganizationPermission', serviceAccountOrganizationPermissionSchema);
export default ServiceAccountOrganizationPermission;

@ -0,0 +1,44 @@
import { Schema, model, Types, Document } from 'mongoose';
export interface IServiceAccountWorkspacePermission extends Document {
_id: Types.ObjectId;
serviceAccount: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
read: boolean;
write: boolean;
}
const serviceAccountWorkspacePermissionSchema = new Schema<IServiceAccountWorkspacePermission>(
{
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount',
required: true
},
workspace:{
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
environment: {
type: String,
required: true
},
read: {
type: Boolean,
default: false
},
write: {
type: Boolean,
default: false
}
},
{
timestamps: true
}
);
const ServiceAccountWorkspacePermission = model<IServiceAccountWorkspacePermission>('ServiceAccountWorkspacePermission', serviceAccountWorkspacePermissionSchema);
export default ServiceAccountWorkspacePermission;

@ -1,10 +1,13 @@
import { Schema, model, Types } from 'mongoose';
import { Schema, model, Types, Document } from 'mongoose';
export interface IServiceTokenData {
export interface IServiceTokenData extends Document {
_id: Types.ObjectId;
name: string;
workspace: Types.ObjectId;
environment: string;
user: Types.ObjectId;
serviceAccount: Types.ObjectId;
lastUsed: Date;
expiresAt: Date;
secretHash: string;
encryptedKey: string;
@ -24,14 +27,20 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
ref: 'Workspace',
required: true
},
environment: { // TODO: adapt to upcoming environment id
environment: {
type: String,
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
ref: 'User'
},
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
lastUsed: {
type: Date
},
expiresAt: {
type: Date

@ -4,6 +4,7 @@ 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';
router.post('/token', validateRequest, authController.getNewToken);
@ -29,7 +30,7 @@ router.post(
'/logout',
authLimiter,
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
authController.logout
);
@ -37,7 +38,7 @@ router.post(
router.post(
'/checkAuth',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
authController.checkAuth
);

@ -8,15 +8,16 @@ import {
validateRequest
} from '../../middleware';
import { botController } from '../../controllers/v1';
import { ADMIN, MEMBER } from '../../variables';
import { ADMIN, MEMBER, AUTH_MODE_JWT } from '../../variables';
router.get(
'/:workspaceId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim().notEmpty(),
validateRequest,
@ -26,7 +27,7 @@ router.get(
router.patch(
'/:botId/active',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireBotAuth({
acceptedRoles: [ADMIN, MEMBER]

@ -6,14 +6,19 @@ import {
requireIntegrationAuthorizationAuth,
validateRequest
} from '../../middleware';
import { ADMIN, MEMBER } from '../../variables';
import {
ADMIN,
MEMBER,
AUTH_MODE_JWT,
AUTH_MODE_API_KEY
} from '../../variables';
import { body, param } from 'express-validator';
import { integrationController } from '../../controllers/v1';
router.post( // new: add new integration for integration auth
'/',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -25,6 +30,9 @@ router.post( // new: add new integration for integration auth
body('appId').trim(),
body('sourceEnvironment').trim(),
body('targetEnvironment').trim(),
body('targetEnvironmentId').trim(),
body('targetService').trim(),
body('targetServiceId').trim(),
body('owner').trim(),
body('path').trim(),
body('region').trim(),
@ -35,7 +43,7 @@ router.post( // new: add new integration for integration auth
router.patch(
'/:integrationId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER]
@ -54,7 +62,7 @@ router.patch(
router.delete(
'/:integrationId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER]

@ -7,13 +7,18 @@ import {
requireIntegrationAuthorizationAuth,
validateRequest
} from '../../middleware';
import { ADMIN, MEMBER } from '../../variables';
import {
ADMIN,
MEMBER,
AUTH_MODE_JWT,
AUTH_MODE_API_KEY
} from '../../variables';
import { integrationAuthController } from '../../controllers/v1';
router.get(
'/integration-options',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
integrationAuthController.getIntegrationOptions
);
@ -21,7 +26,7 @@ router.get(
router.get(
'/:integrationAuthId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
@ -34,11 +39,11 @@ router.get(
router.post(
'/oauth-token',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
locationWorkspaceId: 'body'
}),
body('workspaceId').exists().trim().notEmpty(),
body('code').exists().trim().notEmpty(),
@ -49,25 +54,25 @@ router.post(
router.post(
'/access-token',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
body('workspaceId').exists().trim().notEmpty(),
body('accessId').trim(),
body('accessToken').exists().trim().notEmpty(),
body('integration').exists().trim().notEmpty(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body'
}),
integrationAuthController.saveIntegrationAccessToken
);
router.get(
'/:integrationAuthId/apps',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
@ -81,7 +86,7 @@ router.get(
router.get(
'/:integrationAuthId/teams',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
@ -91,10 +96,53 @@ router.get(
integrationAuthController.getIntegrationAuthTeams
);
router.get(
'/:integrationAuthId/vercel/branches',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId').exists().isString(),
query('appId').exists().isString(),
query('teamId').optional().isString(),
validateRequest,
integrationAuthController.getIntegrationAuthVercelBranches
);
router.get(
'/:integrationAuthId/railway/environments',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId').exists().isString(),
query('appId').exists().isString(),
validateRequest,
integrationAuthController.getIntegrationAuthRailwayEnvironments
);
router.get(
'/:integrationAuthId/railway/services',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId').exists().isString(),
query('appId').exists().isString(),
validateRequest,
integrationAuthController.getIntegrationAuthRailwayServices
);
router.delete(
'/:integrationAuthId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],

@ -3,11 +3,12 @@ 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';
router.post(
'/signup',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
body('inviteeEmail').exists().trim().notEmpty().isEmail(),
body('organizationId').exists().trim().notEmpty(),

@ -6,16 +6,17 @@ import {
validateRequest
} from '../../middleware';
import { body, param } from 'express-validator';
import { ADMIN, MEMBER } from '../../variables';
import { ADMIN, MEMBER, AUTH_MODE_JWT } from '../../variables';
import { keyController } from '../../controllers/v1';
router.post(
'/:workspaceId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
body('key').exists(),
@ -26,10 +27,11 @@ router.post(
router.get(
'/:workspaceId/latest',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId'),
validateRequest,

@ -4,13 +4,14 @@ 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';
// note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId)
router.get( // used for old CLI (deprecate)
'/:workspaceId/connect',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param('workspaceId').exists().trim(),
validateRequest,
@ -20,7 +21,7 @@ router.get( // used for old CLI (deprecate)
router.delete(
'/:membershipId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param('membershipId').exists().trim(),
validateRequest,
@ -30,7 +31,7 @@ router.delete(
router.post(
'/:membershipId/change-role',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
body('role').exists().trim(),
validateRequest,
@ -40,7 +41,7 @@ router.post(
router.post(
'/:membershipId/deny-permissions',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param('membershipId').isMongoId().exists().trim(),
body('permissions').isArray().exists(),

@ -3,12 +3,13 @@ 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';
router.post(
// TODO
'/membershipOrg/:membershipOrgId/change-role',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param('membershipOrgId'),
validateRequest,
@ -18,7 +19,7 @@ router.post(
router.delete(
'/:membershipOrgId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param('membershipOrgId').exists().trim(),
validateRequest,

@ -6,13 +6,19 @@ import {
requireOrganizationAuth,
validateRequest
} from '../../middleware';
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables';
import {
OWNER,
ADMIN,
MEMBER,
ACCEPTED,
AUTH_MODE_JWT
} from '../../variables';
import { organizationController } from '../../controllers/v1';
router.get( // deprecated (moved to api/v2/users/me/organizations)
'/',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
organizationController.getOrganizations
);
@ -20,7 +26,7 @@ router.get( // deprecated (moved to api/v2/users/me/organizations)
router.post( // not used on frontend
'/',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
body('organizationName').exists().trim().notEmpty(),
validateRequest,
@ -30,7 +36,7 @@ router.post( // not used on frontend
router.get(
'/:organizationId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -44,7 +50,7 @@ router.get(
router.get( // deprecated (moved to api/v2/organizations/:organizationId/memberships)
'/:organizationId/users',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -58,7 +64,7 @@ router.get( // deprecated (moved to api/v2/organizations/:organizationId/members
router.get(
'/:organizationId/my-workspaces', // deprecated (moved to api/v2/organizations/:organizationId/workspaces)
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -72,7 +78,7 @@ router.get(
router.patch(
'/:organizationId/name',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -87,7 +93,7 @@ router.patch(
router.get(
'/:organizationId/incidentContactOrg',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -101,7 +107,7 @@ router.get(
router.post(
'/:organizationId/incidentContactOrg',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -116,7 +122,7 @@ router.post(
router.delete(
'/:organizationId/incidentContactOrg',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -131,7 +137,7 @@ router.delete(
router.post(
'/:organizationId/customer-portal-session',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -145,7 +151,7 @@ router.post(
router.get(
'/:organizationId/subscriptions',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -159,7 +165,7 @@ router.get(
router.get(
'/:organizationId/workspace-memberships',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],

@ -4,11 +4,14 @@ import { body } from 'express-validator';
import { requireAuth, requireSignupAuth, validateRequest } from '../../middleware';
import { passwordController } from '../../controllers/v1';
import { passwordLimiter } from '../../helpers/rateLimiter';
import {
AUTH_MODE_JWT
} from '../../variables';
router.post(
'/srp1',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
body('clientPublicKey').exists().isString().trim().notEmpty(),
validateRequest,
@ -19,7 +22,7 @@ router.post(
'/change-password',
passwordLimiter,
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
body('clientProof').exists().trim().notEmpty(),
body('protectedKey').exists().isString().trim().notEmpty(),
@ -62,7 +65,7 @@ router.post(
'/backup-private-key',
passwordLimiter,
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
body('clientProof').exists().isString().trim().notEmpty(),
body('encryptedPrivateKey').exists().isString().trim().notEmpty(), // (backup) private key encrypted under a strong key

@ -8,15 +8,22 @@ import {
} from '../../middleware';
import { body, query, param } from 'express-validator';
import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER } from '../../variables';
import {
ADMIN,
MEMBER,
AUTH_MODE_JWT
} from '../../variables';
// note to devs: these endpoints will be deprecated in favor of v2
router.post(
'/:workspaceId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
body('secrets').exists(),
body('keys').exists(),
@ -30,10 +37,11 @@ router.post(
router.get(
'/:workspaceId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
query('environment').exists().trim(),
query('channel'),

@ -7,7 +7,11 @@ import {
validateRequest
} from '../../middleware';
import { body } from 'express-validator';
import { ADMIN, MEMBER } from '../../variables';
import {
ADMIN,
MEMBER,
AUTH_MODE_JWT
} from '../../variables';
import { serviceTokenController } from '../../controllers/v1';
// note: deprecate service-token routes in favor of service-token data routes/structure
@ -21,11 +25,11 @@ router.get(
router.post(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
locationWorkspaceId: 'body'
}),
body('name').exists().trim().notEmpty(),
body('workspaceId').exists().trim().notEmpty(),

@ -2,11 +2,14 @@ import express from 'express';
const router = express.Router();
import { requireAuth } from '../../middleware';
import { userController } from '../../controllers/v1';
import {
AUTH_MODE_JWT
} from '../../variables';
router.get(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
userController.getUser
);

@ -3,12 +3,13 @@ const router = express.Router();
import { requireAuth, validateRequest } from '../../middleware';
import { body, query } from 'express-validator';
import { userActionController } from '../../controllers/v1';
import { AUTH_MODE_JWT } from '../../variables';
// note: [userAction] will be deprecated in /v2 in favor of [action]
router.post(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
body('action'),
validateRequest,
@ -18,7 +19,7 @@ router.post(
router.get(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
query('action'),
validateRequest,

@ -6,16 +6,21 @@ import {
requireWorkspaceAuth,
validateRequest
} from '../../middleware';
import { ADMIN, MEMBER } from '../../variables';
import {
ADMIN,
MEMBER,
AUTH_MODE_JWT
} from '../../variables';
import { workspaceController, membershipController } from '../../controllers/v1';
router.get(
'/:workspaceId/keys',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
validateRequest,
@ -25,10 +30,11 @@ router.get(
router.get(
'/:workspaceId/users',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
validateRequest,
@ -38,7 +44,7 @@ router.get(
router.get(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
workspaceController.getWorkspaces
);
@ -46,10 +52,11 @@ router.get(
router.get(
'/:workspaceId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
validateRequest,
@ -59,7 +66,7 @@ router.get(
router.post(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
body('workspaceName').exists().trim().notEmpty(),
body('organizationId').exists().trim().notEmpty(),
@ -70,10 +77,11 @@ router.post(
router.delete(
'/:workspaceId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN]
acceptedRoles: [ADMIN],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
validateRequest,
@ -83,10 +91,11 @@ router.delete(
router.post(
'/:workspaceId/name',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
body('name').exists().trim().notEmpty(),
@ -97,10 +106,11 @@ router.post(
router.post(
'/:workspaceId/invite-signup',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
body('email').exists().trim().notEmpty(),
@ -111,10 +121,11 @@ router.post(
router.get(
'/:workspaceId/integrations',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
validateRequest,
@ -124,10 +135,11 @@ router.get(
router.get(
'/:workspaceId/authorizations',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
validateRequest,
@ -137,10 +149,11 @@ router.get(
router.get(
'/:workspaceId/service-tokens', // deprecate
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
validateRequest,

@ -1,16 +1,19 @@
import express from 'express';
const router = express.Router();
import { param, body } from 'express-validator';
import {
requireAuth,
validateRequest
} from '../../middleware';
import { param, body } from 'express-validator';
import { apiKeyDataController } from '../../controllers/v2';
import {
AUTH_MODE_JWT
} from '../../variables';
router.get(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
apiKeyDataController.getAPIKeyData
);
@ -18,7 +21,7 @@ router.get(
router.post(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
body('name').exists().trim(),
body('expiresIn'), // measured in ms
@ -29,7 +32,7 @@ router.post(
router.delete(
'/:apiKeyDataId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param('apiKeyDataId').exists().trim(),
validateRequest,

@ -7,15 +7,20 @@ import {
requireWorkspaceAuth,
validateRequest,
} from '../../middleware';
import { ADMIN, MEMBER } from '../../variables';
import {
ADMIN,
MEMBER,
AUTH_MODE_JWT
} from '../../variables';
router.post(
'/:workspaceId/environments',
requireAuth({
acceptedAuthModes: ['jwt'],
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
@ -27,10 +32,11 @@ router.post(
router.put(
'/:workspaceId/environments',
requireAuth({
acceptedAuthModes: ['jwt'],
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
@ -43,10 +49,11 @@ router.put(
router.delete(
'/:workspaceId/environments',
requireAuth({
acceptedAuthModes: ['jwt'],
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
@ -57,14 +64,15 @@ router.delete(
router.get(
'/:workspaceId/environments',
requireAuth({
acceptedAuthModes: ['jwt'],
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [MEMBER, ADMIN],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
validateRequest,
environmentController.getAllAccessibleEnvironmentsOfWorkspace
);
export default router;
export default router;

@ -6,6 +6,7 @@ import workspace from './workspace';
import secret from './secret'; // deprecated
import secrets from './secrets';
import serviceTokenData from './serviceTokenData';
import serviceAccounts from './serviceAccounts';
import apiKeyData from './apiKeyData';
import environment from "./environment"
import tags from "./tags"
@ -19,6 +20,7 @@ export {
secret,
secrets,
serviceTokenData,
serviceAccounts,
apiKeyData,
environment,
tags

@ -6,8 +6,15 @@ import {
requireMembershipOrgAuth,
validateRequest
} from '../../middleware';
import { body, param, query } from 'express-validator';
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables';
import { body, param } from 'express-validator';
import {
OWNER,
ADMIN,
MEMBER,
ACCEPTED,
AUTH_MODE_JWT,
AUTH_MODE_API_KEY
} from '../../variables';
import { organizationsController } from '../../controllers/v2';
// TODO: /POST to create membership
@ -17,7 +24,7 @@ router.get(
param('organizationId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
@ -33,14 +40,15 @@ router.patch(
body('role').exists().isString().trim().isIn([OWNER, ADMIN, MEMBER]),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
requireMembershipOrgAuth({
acceptedRoles: [OWNER, ADMIN]
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
organizationsController.updateOrganizationMembership
);
@ -51,14 +59,15 @@ router.delete(
param('membershipId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
requireMembershipOrgAuth({
acceptedRoles: [OWNER, ADMIN]
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
organizationsController.deleteOrganizationMembership
);
@ -68,7 +77,7 @@ router.get(
param('organizationId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
@ -77,4 +86,18 @@ router.get(
organizationsController.getOrganizationWorkspaces
);
router.get(
'/:organizationId/service-accounts',
param('organizationId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
organizationsController.getOrganizationServiceAccounts
);
export default router;

@ -6,7 +6,14 @@ import {
validateRequest
} from '../../middleware';
import { body, param, query } from 'express-validator';
import { ADMIN, MEMBER } from '../../variables';
import {
ADMIN,
MEMBER,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_TOKEN,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../../variables';
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
import { secretController } from '../../controllers/v2';
@ -17,10 +24,11 @@ const router = express.Router();
router.post(
'/batch-create/workspace/:workspaceId/environment/:environment',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().isMongoId().trim(),
param('environment').exists().trim(),
@ -33,10 +41,11 @@ router.post(
router.post(
'/workspace/:workspaceId/environment/:environment',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().isMongoId().trim(),
param('environment').exists().trim(),
@ -51,10 +60,11 @@ router.get(
param('workspaceId').exists().trim(),
query("environment").exists(),
requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken']
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
query('channel'),
validateRequest,
@ -64,10 +74,11 @@ router.get(
router.get(
'/:secretId',
requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken']
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN]
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS]
}),
validateRequest,
secretController.getSecret
@ -76,13 +87,14 @@ router.get(
router.delete(
'/batch/workspace/:workspaceId/environment/:environmentName',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
body('secretIds').exists().isArray().custom(array => array.length > 0),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
validateRequest,
secretController.deleteSecrets
@ -91,10 +103,11 @@ router.delete(
router.delete(
'/:secretId',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS]
}),
param('secretId').isMongoId(),
validateRequest,
@ -104,29 +117,30 @@ router.delete(
router.patch(
'/batch-modify/workspace/:workspaceId/environment/:environmentName',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
body('secrets').exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
validateRequest,
secretController.updateSecrets
);
router.patch(
'/workspace/:workspaceId/environment/:environmentName',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: [AUTH_MODE_JWT]
}),
body('secret').isObject(),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
validateRequest,
secretController.updateSecret

@ -1,5 +1,6 @@
import express from 'express';
const router = express.Router();
import { Types } from 'mongoose';
import {
requireAuth,
requireWorkspaceAuth,
@ -8,12 +9,18 @@ import {
} from '../../middleware';
import { query, body } from 'express-validator';
import { secretsController } from '../../controllers/v2';
import { validateSecrets } from '../../helpers/secret';
import { validateClientForSecrets } from '../../helpers/secrets';
import {
ADMIN,
MEMBER,
SECRET_PERSONAL,
SECRET_SHARED
SECRET_SHARED,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../../variables';
import {
BatchSecretRequest
@ -22,12 +29,11 @@ import {
router.post(
'/batch',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['read', 'write']
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
locationWorkspaceId: 'body'
}),
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
@ -38,18 +44,17 @@ router.post(
const secretIds = requests
.map((request) => request.secret._id)
.filter((secretId) => secretId !== undefined)
if (secretIds.length > 0) {
const relevantSecrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds
req.secrets = await validateClientForSecrets({
authData: req.authData,
secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
requiredPermissions: []
});
req.secrets = relevantSecrets;
}
}
return true;
}),
return true;
}),
validateRequest,
secretsController.batchSecrets
);
@ -100,12 +105,13 @@ router.post(
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
locationWorkspaceId: 'body',
locationEnvironment: 'body',
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}),
secretsController.createSecrets
);
@ -117,12 +123,13 @@ router.get(
query('tagSlugs'),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['read']
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_ACCOUNT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'query'
locationWorkspaceId: 'query',
locationEnvironment: 'query',
requiredPermissions: [PERMISSION_READ_SECRETS]
}),
secretsController.getSecrets
);
@ -157,11 +164,11 @@ router.patch(
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}),
secretsController.updateSecrets
);
@ -186,14 +193,13 @@ router.delete(
.isEmpty(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}),
secretsController.deleteSecrets
);
export default router;
export default router;

@ -0,0 +1,159 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
requireOrganizationAuth,
requireWorkspaceAuth,
requireServiceAccountAuth,
requireServiceAccountWorkspacePermissionAuth,
validateRequest
} from '../../middleware';
import { param, query, body } from 'express-validator';
import {
OWNER,
ADMIN,
MEMBER,
ACCEPTED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT
} from '../../variables';
import { serviceAccountsController } from '../../controllers/v2';
router.get( // TODO: check
'/me',
requireAuth({
acceptedAuthModes: [AUTH_MODE_SERVICE_ACCOUNT]
}),
serviceAccountsController.getCurrentServiceAccount
);
router.get(
'/:serviceAccountId',
param('serviceAccountId').exists().isString().trim(),
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
serviceAccountsController.getServiceAccountById
);
router.post(
'/',
body('organizationId').exists().isString().trim(),
body('name').exists().isString().trim(),
body('publicKey').exists().isString().trim(),
body('expiresIn').isNumeric(), // measured in ms
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: 'body'
}),
serviceAccountsController.createServiceAccount
);
router.patch(
'/:serviceAccountId/name',
param('serviceAccountId').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
serviceAccountsController.changeServiceAccountName
);
router.delete(
'/:serviceAccountId',
param('serviceAccountId').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
serviceAccountsController.deleteServiceAccount
);
router.get(
'/:serviceAccountId/permissions/workspace',
param('serviceAccountId').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
serviceAccountsController.getServiceAccountWorkspacePermissions
);
router.post(
'/:serviceAccountId/permissions/workspace',
param('serviceAccountId').exists().isString().trim(),
body('workspaceId').exists().isString().notEmpty(),
body('environment').exists().isString().notEmpty(),
body('read').isBoolean().optional(),
body('write').isBoolean().optional(),
body('encryptedKey').exists().isString().notEmpty(),
body('nonce').exists().isString().notEmpty(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body'
}),
serviceAccountsController.addServiceAccountWorkspacePermission
);
router.delete(
'/:serviceAccountId/permissions/workspace/:serviceAccountWorkspacePermissionId',
param('serviceAccountId').exists().isString().trim(),
param('serviceAccountWorkspacePermissionId').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
requireServiceAccountWorkspacePermissionAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
serviceAccountsController.deleteServiceAccountWorkspacePermission
);
router.get(
'/:serviceAccountId/keys',
query('workspaceId').optional().isString(),
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT]
}),
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
serviceAccountsController.getServiceAccountKeys
);
export default router;

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