1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-21 20:49:07 +00:00

Compare commits

..

139 Commits

Author SHA1 Message Date
4bf2407d13 remove encryptionKey validation check 2023-06-06 09:43:11 -07:00
aaca66e5a4 Patch support for ENCRYPTION_KEY and ROOT_ENCRYPTION_KEY in generateSecretBlindIndexHelper 2023-06-06 14:24:06 +01:00
3a79a855cb Merge pull request from Infisical/folder-patch-v2
Patch backfill data
2023-06-05 23:14:14 -07:00
e28d0cbace bring back tags to secret version 2023-06-05 23:12:48 -07:00
c0fbe82ecb update populate number 2023-06-05 20:21:09 -07:00
b0e7304bff Patch backfill data 2023-06-05 20:19:15 -07:00
08868681d8 Merge pull request from akhilmhdh/fix/folder-breadcrumb
feat(folders): resolved auth issues and added the env dropdown change…
2023-06-05 09:51:59 -07:00
6dee858154 feat(folders): resolved auth issues and added the env dropdown change inside folders 2023-06-05 20:47:58 +05:30
b9dfff1cd8 add migration complete logs 2023-06-04 16:37:57 -07:00
44b9533636 Merge pull request from akhilmhdh/feat/folders
Feat/folders
2023-06-04 16:24:43 -07:00
599c8d94c9 Merge pull request from Infisical/single-rate-limit-store
use mongo rate limit store
2023-06-04 13:45:56 -07:00
77788e1524 use mongo rate limit store 2023-06-04 13:43:57 -07:00
3df62a6e0a patch dup email bug for login 2023-06-04 11:07:52 -07:00
e74cc471db fix(folders): changed to secret path in controllers for get by path op 2023-06-04 13:26:20 +05:30
58d3f3945a feat(folders): removed old comments 2023-06-04 13:22:29 +05:30
29fa618bff feat(folders): changed / to root in breadcrumbs for folders 2023-06-04 13:18:05 +05:30
668b5a9cfd feat(folders): adopted new strategy for rollback on folders 2023-06-04 13:18:05 +05:30
6ce0f48b2c fix(folders): fixed algorithm missing in rollback versions and resolved env change reset folderid 2023-06-04 13:18:05 +05:30
467e85b717 Minor style changes 2023-06-04 13:18:05 +05:30
579516bd38 feat(folders): implemented ui for folders in dashboard 2023-06-04 13:18:05 +05:30
deaa85cbe7 feat(folders): added support for snapshot by env and folder 2023-06-04 13:18:05 +05:30
08a4404fed fixed the email issue 2023-06-03 13:58:53 -07:00
73aa01c568 prevent passport init when envs are undefined 2023-06-03 12:20:04 -07:00
b926601a29 Merge pull request from sheensantoscapadngan/feature/google-signin-signup-integration
Feature/google signin signup integration
2023-06-02 23:44:45 -07:00
f619ee7297 revise self hosting order 2023-06-02 14:10:40 -07:00
bb825c3d68 add DO docs link 2023-06-02 14:06:04 -07:00
6bbd7f05a2 add digital ocean docs 2023-06-02 14:05:00 -07:00
4865b69e6d updated the slack link 2023-06-01 13:54:02 -07:00
6f3c7c0fbf fix ts issues 2023-05-31 11:29:08 -07:00
be80b5124f Final style changes to login/signup 2023-05-31 11:25:37 -07:00
4232776637 Merge pull request from Infisical/check-in-process
Update contributing docs to include expectations and procedures for submitting pull requests
2023-05-31 12:46:21 +03:00
2c12ede694 Add note for prioritization of first 3 PRs for contribution due to high volume of issues, PRs, and other initiatives in the pipeline 2023-05-31 12:44:30 +03:00
94141fedd6 Update contributing docs, add pull requests section 2023-05-31 12:25:08 +03:00
720ab446f9 Merge pull request from Infisical/migration-script
Add migration script for server key re-encryption
2023-05-30 21:03:14 +03:00
1a1693dbbf Add encryption key validation to validation script 2023-05-30 20:46:20 +03:00
9440afa386 Remove re-encryption from Infisical, move to migration script 2023-05-30 20:30:58 +03:00
8b1ec1424d patch quote type in docs 2023-05-30 11:19:15 -04:00
bd56f3b64c update docker run command for self host 2023-05-30 11:14:22 -04:00
f20ea723b7 Merge branch 'main' into feature/google-signin-signup-integration 2023-05-30 20:46:20 +08:00
86f76ebe70 no default user for selfhosting docs 2023-05-30 08:39:43 -04:00
821385c2f3 Revert "add prod img publish ste p"
This reverts commit f7dbd41431b4f3459161c16667951e6b2005daa4.
2023-05-30 07:29:45 -04:00
03c65c8635 Revert "add dummy step"
This reverts commit 893c4777fea475e905b97ef8bcfb2feb21a37154.
2023-05-30 07:29:31 -04:00
893c4777fe add dummy step 2023-05-29 18:49:14 -04:00
f7dbd41431 add prod img publish ste p 2023-05-29 18:46:04 -04:00
8d1f3e930a revert keychain name 2023-05-29 18:08:49 -04:00
f25715b3c4 update keychain name 2023-05-29 17:36:07 -04:00
37251ed607 Begin migration script for re-encryption 2023-05-29 23:46:16 +03:00
c078fb8bc1 allow user to create new keychain 2023-05-29 15:56:48 -04:00
2ae3c48b88 Merge branch 'main' of https://github.com/Infisical/infisical 2023-05-29 14:21:27 +03:00
ce28151952 Update posthog-js version 2023-05-29 14:21:16 +03:00
e6027b3c72 removed try catch from requireAuth middleware 2023-05-29 18:14:16 +08:00
7f4db518cc Merge branch 'main' into feature/google-signin-signup-integration 2023-05-29 18:00:43 +08:00
7562e7d667 Merge pull request from Infisical/changelog
Add preliminary changelog to docs
2023-05-29 12:18:27 +03:00
5c8f33a2d8 Add preliminary changelog to docs 2023-05-29 12:15:47 +03:00
d4f65e23c7 Merge branch 'feature/google-signin-signup-integration' of https://github.com/sheensantoscapadngan/infisical into feature/google-signin-signup-integration 2023-05-28 14:22:42 -07:00
50609d06f5 Final style changes to signup 2023-05-28 14:22:37 -07:00
3a5ad93450 Final style changes to signup 2023-05-28 14:22:17 -07:00
8493d51f5c Merge branch 'main' of https://github.com/Infisical/infisical 2023-05-28 22:23:51 +03:00
e90f63b375 Install and require express-async-errors earlier 2023-05-28 22:23:26 +03:00
af9ffdc51f delete pre commit (pre-commit.com) 2023-05-28 14:36:07 -04:00
3a76a82438 add dummy ENCRYPTION_KEY for testing backend docker img 2023-05-28 14:09:32 -04:00
8e972c704a resolved error handling issue with requireAuth middleware 2023-05-28 23:06:20 +08:00
b975115443 Merge branch 'main' into feature/google-signin-signup-integration 2023-05-28 22:12:02 +08:00
4a1821d537 Merge pull request from Infisical/gitlab-integration
Add pagination to retrieve envars for GitLab integration
2023-05-28 16:51:29 +03:00
01b87aeebf Add pagination to retrieve envars for GitLab integration 2023-05-28 16:46:05 +03:00
cea3b59053 Merge branch 'main' of https://github.com/Infisical/infisical 2023-05-27 19:12:47 -07:00
a6f6711c9a posthog attribution adjustment 2023-05-27 19:12:32 -07:00
3d3b416da2 Merge pull request from piyushchhabra/fix/project-list-scroll
fix(ui): fixed scroll on project list selection
2023-05-26 23:13:07 -07:00
bfbe2f2dcf brought the button back down and removed side bar for other browsers 2023-05-26 23:08:57 -07:00
8e5db3ee2f Merge pull request from Infisical/revert-601-add-refresh-token-cli
Revert "add refresh token to cli"
2023-05-26 16:56:13 -04:00
6b0e0f70d2 Revert "add refresh token to cli" 2023-05-26 16:56:02 -04:00
1fb9aad08a Revert "only re-store user creds when token expire"
This reverts commit df9efa65e7cc523723cd19902f4d183a464022bb.
2023-05-26 16:55:29 -04:00
61a09d817b Merge pull request from Infisical/revised-encryption-key
Update dummy variables in test
2023-05-26 17:31:59 +03:00
57b8ed4eef Merge remote-tracking branch 'origin' into revised-encryption-key 2023-05-26 17:29:54 +03:00
c3a1d03a9b Update test dummy variables 2023-05-26 17:29:23 +03:00
11afb6db51 Merge pull request from Infisical/revised-encryption-key
Add encryption metadata and upgrade ENCRYPTION_KEY to ROOT_ENCRYPTION_KEY
2023-05-26 17:01:00 +03:00
200d9de740 Fix merge conflicts 2023-05-26 16:41:17 +03:00
17060b22d7 Update README.md 2023-05-25 21:24:07 -07:00
c730280eff Update FeatureSet interface to include used counts 2023-05-26 00:26:16 +03:00
c45120e6e9 add shorter env name for file vault 2023-05-25 13:27:20 -04:00
c96fbd3724 fix(ui): fixing scroll on project list selection 2023-05-25 19:44:06 +05:30
e1e2eb7c3b Add SecretBlindIndexData for development user initialization 2023-05-25 16:07:08 +03:00
7812061e66 Update isPaid telemetry accounting to be tier-based instead of via slug 2023-05-25 12:59:18 +03:00
ca41c65fe0 small helm doc changes 2023-05-24 23:46:34 -04:00
d8c15a366d Merge pull request from piyushchhabra/fix/gui-tags-overflow
fix(ui): fixed tags overflow in delete card
2023-05-24 20:19:53 -07:00
df9efa65e7 only re-store user creds when token expire 2023-05-24 19:46:02 -04:00
1c5616e3b6 revise pre commit doc 2023-05-24 19:11:33 -04:00
27030138ec Merge pull request from Infisical/add-refresh-token-cli
add refresh token to cli
2023-05-24 18:53:52 -04:00
5aa367fe54 fix(ui): fixed tags overflow in card + port correction in README 2023-05-24 23:03:12 +05:30
fac4968193 moved oauth controller endpoints to auth 2023-05-24 23:44:30 +08:00
93cf7cde2d fixed login issue after mfa 2023-05-24 21:33:24 +08:00
422d04d7d7 migrated to standard request 2023-05-24 18:50:59 +08:00
4c41d279e9 Merge branch 'main' into feature/google-signin-signup-integration 2023-05-24 18:39:18 +08:00
51914c6a2e resolved package-lock conflicts 2023-05-22 22:07:30 +08:00
ad37a14f2e Merge branch 'main' into feature/google-signin-signup-integration 2023-05-22 21:54:51 +08:00
bc61de4a80 add provider auth secret to kubernetes and docker yaml 2023-05-20 23:15:36 +08:00
4367822777 re-added token caching and redirection 2023-05-18 23:04:55 +08:00
ca4a9b9937 resolved MFA not appearing 2023-05-18 02:07:07 +08:00
ec8d62d106 show toast when oauth login error 2023-05-18 01:58:35 +08:00
6ca3b8ba61 handled error cases for external auth login 2023-05-18 00:39:20 +08:00
4b2e91da74 added proper error handling for user creation 2023-05-18 00:12:28 +08:00
fac8affe78 added missing envs for documentation 2023-05-17 22:24:04 +08:00
1ccec486cc removed caching of providerAuthToken 2023-05-17 21:43:18 +08:00
6746f04f33 added self-hosting documentation for google 2023-05-15 23:39:19 +08:00
dba19b4a1d Merge branch 'main' into feature/google-signin-signup-integration 2023-05-15 20:41:08 +08:00
884aed74a5 made last name optional 2023-05-15 20:39:45 +08:00
9dc7cc58a7 uncommented code 2023-05-15 00:41:27 +08:00
6f66b56e7c updated package-lock 2023-05-12 22:24:38 +08:00
be2bac41bb Merge branch 'main' into feature/google-signin-signup-integration 2023-05-12 22:22:51 +08:00
0afa44a9f0 removed express-session types 2023-05-11 15:39:04 +08:00
5a99878d15 Final style edits to the login and signup flows 2023-05-10 16:12:57 -07:00
0d3e7f3c0c Merge branch 'feature/google-signin-signup-integration' of https://github.com/sheensantoscapadngan/infisical into feature/google-signin-signup-integration 2023-05-10 12:55:19 -07:00
967e520173 More designed changes to login flow 2023-05-10 12:55:08 -07:00
ccfe0b1eb9 reverted changes made to nginx config 2023-05-11 00:52:49 +08:00
0ef5779776 add providerAuthToken for MFA login 2023-05-11 00:47:16 +08:00
a194e90644 removed session references 2023-05-11 00:41:38 +08:00
addc849fa6 changed google-auth strategy and removed session use 2023-05-11 00:37:02 +08:00
074c0bdd77 utilized mongodb as persistent store for sessions 2023-05-10 23:01:44 +08:00
7ee33e9393 resolved merge conflict issues and updated use of translations 2023-05-10 21:39:11 +08:00
32cef27e8e Merge branch 'main' into feature/google-sign 2023-05-10 21:11:42 +08:00
1fce8cc769 More style changes to login 2023-05-09 23:24:37 -07:00
4e7145dfe5 Style changes to login 2023-05-09 20:45:59 -07:00
4c434555a4 finalized signup/signin ux regarding redirects 2023-05-07 20:54:30 +08:00
f011d61167 Merge remote-tracking branch 'origin' into revised-encryption-key 2023-05-06 22:22:03 +03:00
87e047a152 Checkpoint finish preliminary support for ROOT_ENCRYPTION_KEY 2023-05-06 22:07:59 +03:00
ea86e59d4f resolved component alignment of signup 2023-05-06 19:26:42 +08:00
3e19e6fd99 finalized login and signup ui 2023-05-06 18:52:01 +08:00
3d3d7c9821 Merge remote-tracking branch 'origin' into revised-encryption-key 2023-05-05 10:27:44 +03:00
5eeda6272c Checkpoint adding crypto metadata 2023-05-04 20:35:06 +03:00
b734b51954 developed new ui for new login and signup page 2023-05-05 00:39:28 +08:00
1172726e74 added signup v3 endpoints and developed initial new signup flow 2023-05-04 01:32:40 +08:00
c766686670 Fix merge conflicts for variable imports 2023-05-03 19:30:30 +03:00
099cee7f39 Begin refactoring backfilling and preparation operations into setup and start adding encryption metadata to models 2023-05-03 14:21:42 +03:00
f703ee29e5 implemented comments 2023-05-03 18:58:32 +08:00
dfb84e9932 developed initial version of new login page 2023-04-27 23:10:27 +08:00
2dd1570200 updated use of environment variables to utilize await 2023-04-27 02:00:16 +08:00
69472514af Merge branch 'main' into feature/google-signin-signup-integration 2023-04-27 01:46:26 +08:00
f956170820 added auth v3 endpoints for login1 and login2 2023-04-27 01:38:06 +08:00
007e8c4442 initial setup for google signin signup integration 2023-04-25 23:47:46 +08:00
223 changed files with 15784 additions and 12837 deletions
.env.example
.github
.infisicalignore.pre-commit-config.yaml.pre-commit-hooks.yamlREADME.md
backend
package-lock.jsonpackage.json
src
config
controllers
ee
helpers
index.ts
integrations
interfaces
services/SecretService
utils
middleware
models
routes
services
types
utils
validation
variables
test-resources
tests
helper
unit-tests/utils
cli/packages
docs
frontend
helm-charts/infisical
migration
render.yaml

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

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

Binary file not shown.

After

(image error) Size: 19 KiB

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

Binary file not shown.

After

(image error) Size: 28 KiB

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

1
.infisicalignore Normal file

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

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

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

@ -7,7 +7,7 @@
</p>
<h4 align="center">
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">Slack</a> |
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg">Slack</a> |
<a href="https://infisical.com/">Infisical Cloud</a> |
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
<a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> |
@ -25,9 +25,9 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a>
<a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-150.8k-orange" alt="Cloudsmith downloads" />
<img src="https://img.shields.io/badge/Downloads-240.2k-orange" alt="Cloudsmith downloads" />
</a>
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
</a>
<a href="https://twitter.com/infisical">
@ -55,7 +55,7 @@ We're on a mission to make secret management more accessible to everyone, not ju
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project
- **Role-based Access Controls** per environment
- [**Simple on-premise deployments** to AWS and Digital Ocean](https://infisical.com/docs/self-hosting/overview)
- [**2FA**](https://infisical.com/docs/documentation/platform/mfa) with more options coming soon
- [**Secret Scanning**](https://infisical.com/docs/cli/scanning-overview)
And much more.
@ -69,7 +69,9 @@ The fastest and most reliable way to get started with Infisical is signing up fo
### Deploy Infisical on premise
Deployment options: [AWS EC2](https://infisical.com/docs/self-hosting/overview), [Kubernetes](https://infisical.com/docs/self-hosting/overview), and [more](https://infisical.com/docs/self-hosting/overview).
<a href="https://infisical.com/docs/self-hosting/deployment-options/digital-ocean-marketplace"><img src=".github/images/do-k8-install-btn.png" width="200"/></a> <a href="https://infisical.com/docs/self-hosting/deployment-options/aws-ec2"><img src=".github/images/deploy-aws-button.png" width="150" width="300" /></a>
View all [deployment options](https://infisical.com/docs/self-hosting/overview)
### Run Infisical locally
@ -126,12 +128,12 @@ Whether it's big or small, we love contributions. Check out our guide to see how
Not sure where to get started? You can:
- [Book a free, non-pressure pairing sessions with one of our teammates](mailto:tony@infisical.com?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)!
- Join our <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">Slack</a>, and ask us any questions there.
- Join our <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg">Slack</a>, and ask us any questions there.
## Resources
- [Docs](https://infisical.com/docs/documentation/getting-started/introduction) for comprehensive documentation and guides
- [Slack](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g) for discussion with the community and Infisical team.
- [Slack](https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg) for discussion with the community and Infisical team.
- [GitHub](https://github.com/Infisical/infisical) for code, issues, and pull requests
- [Twitter](https://twitter.com/infisical) for fast news
- [YouTube](https://www.youtube.com/@infisical5306) for videos on secret management

7480
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -24,7 +24,7 @@
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"infisical-node": "^1.1.3",
"infisical-node": "^1.2.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
@ -32,9 +32,13 @@
"lodash": "^4.17.21",
"mongoose": "^6.10.5",
"node-cache": "^5.1.2",
"nanoid": "^3.3.6",
"nodemailer": "^6.8.0",
"passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0",
"query-string": "^7.1.3",
"rate-limit-mongo": "^2.3.2",
"request-ip": "^3.3.0",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
@ -87,6 +91,7 @@
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6",
"@types/passport": "^1.0.12",
"@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",

@ -1,12 +1,19 @@
import InfisicalClient from 'infisical-node';
const client = new InfisicalClient({
export const client = new InfisicalClient({
token: process.env.INFISICAL_TOKEN!
});
export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000;
export const getEncryptionKey = async () => {
const secretValue = (await client.getSecret('ENCRYPTION_KEY')).secretValue;
return secretValue === '' ? undefined : secretValue;
}
export const getRootEncryptionKey = async () => {
const secretValue = (await client.getSecret('ROOT_ENCRYPTION_KEY')).secretValue;
return secretValue === '' ? undefined : secretValue;
}
export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue === 'true'
export const getEncryptionKey = async () => (await client.getSecret('ENCRYPTION_KEY')).secretValue;
export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10;
export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d';
export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue;
@ -16,6 +23,8 @@ export const getJwtRefreshLifetime = async () => (await client.getSecret('JWT_RE
export const getJwtRefreshSecret = async () => (await client.getSecret('JWT_REFRESH_SECRET')).secretValue;
export const getJwtServiceSecret = async () => (await client.getSecret('JWT_SERVICE_SECRET')).secretValue;
export const getJwtSignupLifetime = async () => (await client.getSecret('JWT_SIGNUP_LIFETIME')).secretValue || '15m';
export const getJwtProviderAuthSecret = async () => (await client.getSecret('JWT_PROVIDER_AUTH_SECRET')).secretValue;
export const getJwtProviderAuthLifetime = async () => (await client.getSecret('JWT_PROVIDER_AUTH_LIFETIME')).secretValue || '15m';
export const getJwtSignupSecret = async () => (await client.getSecret('JWT_SIGNUP_SECRET')).secretValue;
export const getMongoURL = async () => (await client.getSecret('MONGO_URL')).secretValue;
export const getNodeEnv = async () => (await client.getSecret('NODE_ENV')).secretValue || 'production';
@ -27,12 +36,14 @@ export const getClientIdVercel = async () => (await client.getSecret('CLIENT_ID_
export const getClientIdNetlify = async () => (await client.getSecret('CLIENT_ID_NETLIFY')).secretValue;
export const getClientIdGitHub = async () => (await client.getSecret('CLIENT_ID_GITHUB')).secretValue;
export const getClientIdGitLab = async () => (await client.getSecret('CLIENT_ID_GITLAB')).secretValue;
export const getClientIdGoogle = async () => (await client.getSecret('CLIENT_ID_GOOGLE')).secretValue;
export const getClientSecretAzure = async () => (await client.getSecret('CLIENT_SECRET_AZURE')).secretValue;
export const getClientSecretHeroku = async () => (await client.getSecret('CLIENT_SECRET_HEROKU')).secretValue;
export const getClientSecretVercel = async () => (await client.getSecret('CLIENT_SECRET_VERCEL')).secretValue;
export const getClientSecretNetlify = async () => (await client.getSecret('CLIENT_SECRET_NETLIFY')).secretValue;
export const getClientSecretGitHub = async () => (await client.getSecret('CLIENT_SECRET_GITHUB')).secretValue;
export const getClientSecretGitLab = async () => (await client.getSecret('CLIENT_SECRET_GITLAB')).secretValue;
export const getClientSecretGoogle = async () => (await client.getSecret('CLIENT_SECRET_GOOGLE')).secretValue;
export const getClientSlugVercel = async () => (await client.getSecret('CLIENT_SLUG_VERCEL')).secretValue;
export const getPostHogHost = async () => (await client.getSecret('POSTHOG_HOST')).secretValue || 'https://app.posthog.com';
export const getPostHogProjectApiKey = async () => (await client.getSecret('POSTHOG_PROJECT_API_KEY')).secretValue || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
@ -46,8 +57,14 @@ export const getSmtpPassword = async () => (await client.getSecret('SMTP_PASSWOR
export const getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue;
export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical';
export const getLicenseKey = async () => (await client.getSecret('LICENSE_KEY')).secretValue;
export const getLicenseServerKey = async () => (await client.getSecret('LICENSE_SERVER_KEY')).secretValue;
export const getLicenseKey = async () => {
const secretValue = (await client.getSecret('LICENSE_KEY')).secretValue;
return secretValue === '' ? undefined : secretValue;
}
export const getLicenseServerKey = async () => {
const secretValue = (await client.getSecret('LICENSE_SERVER_KEY')).secretValue;
return secretValue === '' ? undefined : secretValue;
}
export const getLicenseServerUrl = async () => (await client.getSecret('LICENSE_SERVER_URL')).secretValue || 'https://portal.infisical.com';
// TODO: deprecate from here

@ -267,3 +267,7 @@ export const getNewToken = async (req: Request, res: Response) => {
});
}
};
export const handleAuthProviderCallback = (req: Request, res: Response) => {
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
}

@ -5,7 +5,7 @@ import {
IntegrationAuth,
Bot
} from '../../models';
import { INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables';
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables';
import { IntegrationService } from '../../services';
import {
getApps,
@ -129,7 +129,9 @@ export const saveIntegrationAccessToken = async (
integration
}, {
workspace: new Types.ObjectId(workspaceId),
integration
integration,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true,
upsert: true

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

@ -39,7 +39,7 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
error: 'Failed to send email verification code'
});
}
return res.status(200).send({
message: `Sent an email verification code to ${email}`
});

@ -288,6 +288,8 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
if (!user) throw new Error('Failed to find user');
await LoginSRPDetail.deleteOne({ userId: user.id })
await checkUserDevice({
user,
ip: req.ip,

@ -6,7 +6,7 @@ import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCre
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 { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { TelemetryService } from '../../services';
import { User } from "../../models";
import { AccountNotFoundError } from '../../utils/errors';
@ -36,7 +36,9 @@ export const createSecret = async (req: Request, res: Response) => {
workspace: new Types.ObjectId(workspaceId),
environment,
type: secretToCreate.type,
user: new Types.ObjectId(req.user._id)
user: new Types.ObjectId(req.user._id),
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}
@ -92,7 +94,9 @@ export const createSecrets = async (req: Request, res: Response) => {
workspace: new Types.ObjectId(workspaceId),
environment,
type: rawSecret.type,
user: new Types.ObjectId(req.user._id)
user: new Types.ObjectId(req.user._id),
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}
sanitizedSecretesToCreate.push(safeUpdateFields)

File diff suppressed because it is too large Load Diff

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

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

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

@ -7,13 +7,12 @@ import { EELicenseService } from '../../services';
* Return the organization's current plan and allowed feature set
*/
export const getOrganizationPlan = async (req: Request, res: Response) => {
const plan = await EELicenseService.getOrganizationPlan(req.organization._id.toString());
const { organizationId } = req.params;
// cache fetched plan for organization
EELicenseService.localFeatureSet.set(req.organization._id.toString(), plan);
const plan = await EELicenseService.getOrganizationPlan(organizationId);
return res.status(200).send({
plan
plan,
});
}

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

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

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

@ -1,6 +1,11 @@
import { Types } from "mongoose";
import { Secret, ISecret } from "../../models";
import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models";
import {
SecretSnapshot,
SecretVersion,
ISecretVersion,
FolderVersion,
} from "../models";
/**
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
@ -12,22 +17,31 @@ import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models";
*/
const takeSecretSnapshotHelper = async ({
workspaceId,
environment,
folderId = "root",
}: {
workspaceId: Types.ObjectId;
environment: string;
folderId?: string;
}) => {
// get all folder ids
const secretIds = (
await Secret.find(
{
workspace: workspaceId,
environment,
folder: folderId,
},
"_id"
)
).lean()
).map((s) => s._id);
const latestSecretVersions = (
await SecretVersion.aggregate([
{
$match: {
environment,
workspace: new Types.ObjectId(workspaceId),
secret: {
$in: secretIds,
},
@ -45,6 +59,11 @@ const takeSecretSnapshotHelper = async ({
},
]).exec()
).map((s) => s.versionId);
const latestFolderVersion = await FolderVersion.findOne({
environment,
workspace: workspaceId,
"nodes.id": folderId,
}).sort({ "nodes.version": -1 });
const latestSecretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId,
@ -52,8 +71,11 @@ const takeSecretSnapshotHelper = async ({
const secretSnapshot = await new SecretSnapshot({
workspace: workspaceId,
environment,
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
secretVersions: latestSecretVersions,
folderId,
folderVersion: latestFolderVersion,
}).save();
return secretSnapshot;
@ -93,52 +115,8 @@ const markDeletedSecretVersionsHelper = async ({
);
};
/**
* Initialize secret versioning by setting previously unversioned
* secrets to version 1 and begin populating secret versions.
*/
const initSecretVersioningHelper = async () => {
await Secret.updateMany(
{ version: { $exists: false } },
{ $set: { version: 1 } }
);
const unversionedSecrets: ISecret[] = await Secret.aggregate([
{
$lookup: {
from: "secretversions",
localField: "_id",
foreignField: "secret",
as: "versions",
},
},
{
$match: {
versions: { $size: 0 },
},
},
]);
if (unversionedSecrets.length > 0) {
await addSecretVersionsHelper({
secretVersions: unversionedSecrets.map(
(s, idx) =>
new SecretVersion({
...s,
secret: s._id,
version: s.version ? s.version : 1,
isDeleted: false,
workspace: s.workspace,
environment: s.environment,
})
),
});
}
};
export {
takeSecretSnapshotHelper,
addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
initSecretVersioningHelper,
};

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

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

@ -1,15 +1,18 @@
import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot';
import SecretVersion, { ISecretVersion } from './secretVersion';
import Log, { ILog } from './log';
import Action, { IAction } from './action';
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
import SecretVersion, { ISecretVersion } from "./secretVersion";
import FolderVersion, { TFolderRootVersionSchema } from "./folderVersion";
import Log, { ILog } from "./log";
import Action, { IAction } from "./action";
export {
SecretSnapshot,
ISecretSnapshot,
SecretVersion,
ISecretVersion,
Log,
ILog,
Action,
IAction
}
SecretSnapshot,
ISecretSnapshot,
SecretVersion,
ISecretVersion,
FolderVersion,
TFolderRootVersionSchema,
Log,
ILog,
Action,
IAction,
};

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

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

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

@ -17,9 +17,11 @@ import { OrganizationNotFoundError } from '../../utils/errors';
interface FeatureSet {
_id: string | null;
slug: 'starter' | 'team' | 'pro' | 'enterprise' | null;
tier: number | null;
projectLimit: number | null;
tier: number;
workspaceLimit: number | null;
workspacesUsed: number;
memberLimit: number | null;
membersUsed: number;
secretVersioning: boolean;
pitRecovery: boolean;
rbac: boolean;
@ -44,8 +46,10 @@ class EELicenseService {
_id: null,
slug: null,
tier: -1,
projectLimit: null,
workspaceLimit: null,
workspacesUsed: 0,
memberLimit: null,
membersUsed: 0,
secretVersioning: true,
pitRecovery: true,
rbac: true,
@ -63,11 +67,13 @@ class EELicenseService {
});
}
public async getOrganizationPlan(organizationId: string) {
public async getOrganizationPlan(organizationId: string): Promise<FeatureSet> {
try {
if (this.instanceType === 'cloud') {
const cachedPlan = this.localFeatureSet.get(organizationId);
if (cachedPlan) return cachedPlan;
const cachedPlan = this.localFeatureSet.get<FeatureSet>(organizationId);
if (cachedPlan) {
return cachedPlan;
}
const organization = await Organization.findById(organizationId);
if (!organization) throw OrganizationNotFoundError();
@ -76,6 +82,9 @@ class EELicenseService {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`
);
// cache fetched plan for organization
this.localFeatureSet.set(organizationId, currentPlan);
return currentPlan;
}
} catch (err) {

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

@ -19,6 +19,7 @@ import {
import {
getJwtAuthLifetime,
getJwtAuthSecret,
getJwtProviderAuthSecret,
getJwtRefreshLifetime,
getJwtRefreshSecret
} from '../config';
@ -41,7 +42,6 @@ const validateAuthMode = ({
headers: { [key: string]: string | string[] | undefined },
acceptedAuthModes: string[]
}) => {
// TODO: refactor middleware
const apiKey = headers['x-api-key'];
const authHeader = headers['authorization'];
@ -318,8 +318,34 @@ const createToken = ({
});
};
const validateProviderAuthToken = async ({
email,
user,
providerAuthToken,
}: {
email: string;
user: IUser,
providerAuthToken?: string;
}) => {
if (!providerAuthToken) {
throw new Error('Invalid authentication request.');
}
const decodedToken = <jwt.ProviderAuthJwtPayload>(
jwt.verify(providerAuthToken, await getJwtProviderAuthSecret())
);
if (
decodedToken.authProvider !== user.authProvider ||
decodedToken.email !== email
) {
throw new Error('Invalid authentication credentials.')
}
}
export {
validateAuthMode,
validateProviderAuthToken,
getAuthUserPayload,
getAuthSTDPayload,
getAuthSAAKPayload,

@ -4,107 +4,26 @@ import {
BotKey,
Secret,
ISecret,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData,
IUser
} from "../models";
import {
generateKeyPair,
encryptSymmetric,
decryptSymmetric,
decryptAsymmetric,
} from "../utils/crypto";
encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8,
decryptAsymmetric
} from '../utils/crypto';
import {
SECRET_SHARED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} 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",
});
};
import {
getEncryptionKey,
getRootEncryptionKey,
client
} from "../config";
import { InternalServerError } from "../utils/errors";
/**
* Create an inactive bot with name [name] for workspace with id [workspaceId]
@ -119,23 +38,52 @@ const createBot = async ({
name: string;
workspaceId: Types.ObjectId;
}) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const { publicKey, privateKey } = generateKeyPair();
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: privateKey,
key: await getEncryptionKey(),
if (rootEncryptionKey) {
const {
ciphertext,
iv,
tag
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
return await new Bot({
name,
workspace: workspaceId,
isActive: false,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64
}).save();
} else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey,
key: await getEncryptionKey(),
});
return await new Bot({
name,
workspace: workspaceId,
isActive: false,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}).save();
}
throw InternalServerError({
message: 'Failed to create new bot due to missing encryption key'
});
const bot = await new Bot({
name,
workspace: workspaceId,
isActive: false,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
}).save();
return bot;
};
/**
@ -161,14 +109,14 @@ const getSecretsHelper = async ({
});
secrets.forEach((secret: ISecret) => {
const secretKey = decryptSymmetric({
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key,
});
const secretValue = decryptSymmetric({
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
@ -189,34 +137,54 @@ const getSecretsHelper = async ({
* @returns {String} key - decrypted workspace key
*/
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const botKey = await BotKey.findOne({
workspace: workspaceId,
}).populate<{ sender: IUser }>("sender", "publicKey");
})
.populate<{ sender: IUser }>("sender", "publicKey");
if (!botKey) throw new Error("Failed to find bot key");
const bot = await Bot.findOne({
workspace: workspaceId,
}).select("+encryptedPrivateKey +iv +tag");
}).select("+encryptedPrivateKey +iv +tag +algorithm +keyEncoding");
if (!bot) throw new Error("Failed to find bot");
if (!bot.isActive) throw new Error("Bot is not active");
const privateKeyBot = decryptSymmetric({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: await getEncryptionKey(),
});
if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) {
// case: encoding scheme is base64
const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag);
const key = decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
});
return decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
});
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
// case: encoding scheme is utf8
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: encryptionKey
});
return decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
});
}
return key;
throw InternalServerError({
message: "Failed to obtain bot's copy of workspace key needed for bot operations"
});
};
/**
@ -234,7 +202,7 @@ const encryptSymmetricHelper = async ({
plaintext: string;
}) => {
const key = await getKey({ workspaceId: workspaceId.toString() });
const { ciphertext, iv, tag } = encryptSymmetric({
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext,
key,
});
@ -266,7 +234,7 @@ const decryptSymmetricHelper = async ({
tag: string;
}) => {
const key = await getKey({ workspaceId: workspaceId.toString() });
const plaintext = decryptSymmetric({
const plaintext = decryptSymmetric128BitHexKeyUTF8({
ciphertext,
iv,
tag,
@ -277,9 +245,8 @@ const decryptSymmetricHelper = async ({
};
export {
validateClientForBot,
createBot,
getSecretsHelper,
encryptSymmetricHelper,
decryptSymmetricHelper,
decryptSymmetricHelper
};

@ -1,6 +1,4 @@
import mongoose from 'mongoose';
import { EESecretService } from '../ee/services';
import { SecretService } from '../services';
import { getLogger } from '../utils/logger';
/**
@ -21,9 +19,7 @@ const initDatabaseHelper = async ({
mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string');
(await getLogger("database")).info("Database connection established");
await EESecretService.initSecretVersioning();
await SecretService.initSecretBlindIndexDataHelper();
} catch (err) {
(await getLogger("database")).error(`Unable to establish Database connection due to the error.\n${err}`);
}

@ -3,40 +3,20 @@ import { Types } from 'mongoose';
import {
Bot,
Integration,
IntegrationAuth,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
IntegrationAuth
} 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
INTEGRATION_NETLIFY,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8
} from '../variables';
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;
@ -45,84 +25,6 @@ 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]
@ -400,7 +302,9 @@ const setIntegrationAuthRefreshHelper = async ({
}, {
refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv,
refreshTag: obj.tag
refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
@ -461,7 +365,9 @@ const setIntegrationAuthAccessHelper = async ({
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
@ -475,7 +381,6 @@ const setIntegrationAuthAccessHelper = async ({
}
export {
validateClientForIntegration,
handleOAuthExchangeHelper,
syncIntegrationsHelper,
getIntegrationAuthRefreshHelper,

@ -1,106 +1,7 @@
import * as Sentry from '@sentry/node';
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'
});
}
import { Membership, Key } from '../models';
import { MembershipNotFoundError, BadRequestError } from '../utils/errors';
/**
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
@ -111,32 +12,35 @@ const validateClientForMembership = async ({
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
*/
const validateMembership = async ({
userId,
workspaceId,
acceptedRoles,
userId,
workspaceId,
acceptedRoles,
}: {
userId: Types.ObjectId;
workspaceId: Types.ObjectId;
acceptedRoles?: Array<'admin' | 'member'>;
userId: Types.ObjectId | string;
workspaceId: Types.ObjectId | string;
acceptedRoles?: Array<'admin' | 'member'>;
}) => {
const membership = await Membership.findOne({
user: userId,
workspace: workspaceId
}).populate("workspace");
if (!membership) {
throw MembershipNotFoundError({ message: 'Failed to find workspace membership' });
}
if (acceptedRoles) {
if (!acceptedRoles.includes(membership.role)) {
throw BadRequestError({ message: 'Failed authorization for membership role' });
}
}
return membership;
}
const membership = await Membership.findOne({
user: userId,
workspace: workspaceId,
}).populate('workspace');
if (!membership) {
throw MembershipNotFoundError({
message: 'Failed to find workspace membership',
});
}
if (acceptedRoles) {
if (!acceptedRoles.includes(membership.role)) {
throw BadRequestError({
message: 'Failed authorization for membership role',
});
}
}
return membership;
};
/**
* Return membership matching criteria specified in query [queryObj]
@ -144,16 +48,16 @@ const validateMembership = async ({
* @return {Object} membership - membership
*/
const findMembership = async (queryObj: any) => {
let membership;
try {
membership = await Membership.findOne(queryObj);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to find membership');
}
let membership;
try {
membership = await Membership.findOne(queryObj);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to find membership');
}
return membership;
return membership;
};
/**
@ -165,39 +69,39 @@ const findMembership = async (queryObj: any) => {
* @param {String[]} obj.roles - roles of users.
*/
const addMemberships = async ({
userIds,
workspaceId,
roles
userIds,
workspaceId,
roles,
}: {
userIds: string[];
workspaceId: string;
roles: string[];
userIds: string[];
workspaceId: string;
roles: string[];
}): Promise<void> => {
try {
const operations = userIds.map((userId, idx) => {
return {
updateOne: {
filter: {
user: userId,
workspace: workspaceId,
role: roles[idx]
},
update: {
user: userId,
workspace: workspaceId,
role: roles[idx]
},
upsert: true
}
};
});
try {
const operations = userIds.map((userId, idx) => {
return {
updateOne: {
filter: {
user: userId,
workspace: workspaceId,
role: roles[idx],
},
update: {
user: userId,
workspace: workspaceId,
role: roles[idx],
},
upsert: true,
},
};
});
await Membership.bulkWrite(operations as any);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to add users to workspace');
}
await Membership.bulkWrite(operations as any);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to add users to workspace');
}
};
/**
@ -206,33 +110,27 @@ const addMemberships = async ({
* @param {String} obj.membershipId - id of membership to delete
*/
const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
let deletedMembership;
try {
deletedMembership = await Membership.findOneAndDelete({
_id: membershipId
});
let deletedMembership;
try {
deletedMembership = await Membership.findOneAndDelete({
_id: membershipId,
});
// delete keys associated with the membership
if (deletedMembership?.user) {
// case: membership had a registered user
await Key.deleteMany({
receiver: deletedMembership.user,
workspace: deletedMembership.workspace
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete membership');
}
// delete keys associated with the membership
if (deletedMembership?.user) {
// case: membership had a registered user
await Key.deleteMany({
receiver: deletedMembership.user,
workspace: deletedMembership.workspace,
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete membership');
}
return deletedMembership;
return deletedMembership;
};
export {
validateClientForMembership,
validateMembership,
addMemberships,
findMembership,
deleteMembership
};
export { validateMembership, addMemberships, findMembership, deleteMembership };

@ -3,95 +3,12 @@ import {
MembershipOrg,
Workspace,
Membership,
Key,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
Key
} 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]
@ -234,7 +151,6 @@ const deleteMembershipOrg = async ({
};
export {
validateClientForMembershipOrg,
validateMembershipOrg,
findMembershipOrg,
addMembershipsOrg,

@ -1,21 +1,8 @@
import Stripe from "stripe";
import { Types } from "mongoose";
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,
ACCEPTED
} from "../variables";
import {
getStripeSecretKey,
@ -23,12 +10,6 @@ import {
getStripeProductTeam,
getStripeProductStarter,
} from "../config";
import {
UnauthorizedRequestError,
OrganizationNotFoundError,
} from "../utils/errors";
import { validateUserClientForOrganization } from "../helpers/user";
import { validateServiceAccountClientForOrganization } from "../helpers/serviceAccount";
import {
EELicenseService
} from '../ee/services';
@ -40,88 +21,6 @@ import {
licenseKeyRequest
} from '../config/request';
/**
* 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]
* @param {Object} obj
@ -251,6 +150,8 @@ const updateSubscriptionOrgQuantity = async ({
quantity
}
);
EELicenseService.localFeatureSet.del(organizationId);
}
if (EELicenseService.instanceType === 'enterprise-self-hosted') {
@ -273,8 +174,7 @@ const updateSubscriptionOrgQuantity = async ({
};
export {
validateClientForOrganization,
createOrganization,
initSubscriptionOrg,
updateSubscriptionOrgQuantity,
updateSubscriptionOrgQuantity
};

@ -1,9 +1,16 @@
import rateLimit from 'express-rate-limit';
const MongoStore = require('rate-limit-mongo');
// 120 requests per minute
// 200 per minute
const apiLimiter = rateLimit({
windowMs: 60 * 1000,
max: 240,
store: new MongoStore({
uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60,
collectionName: "expressRateRecords-apiLimiter",
errorHandler: console.error.bind(null, 'rate-limit-mongo')
}),
windowMs: 1000 * 60,
max: 200,
standardHeaders: true,
legacyHeaders: false,
skip: (request) => {
@ -14,10 +21,16 @@ const apiLimiter = rateLimit({
}
});
// 10 requests per minute
// 50 requests per 1 hours
const authLimit = rateLimit({
windowMs: 60 * 1000,
max: 10,
store: new MongoStore({
uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60 * 60,
errorHandler: console.error.bind(null, 'rate-limit-mongo'),
collectionName: "expressRateRecords-authLimit",
}),
windowMs: 1000 * 60 * 60,
max: 50,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => {
@ -25,10 +38,16 @@ const authLimit = rateLimit({
}
});
// 10 requests per hour
// 50 requests per 1 hour
const passwordLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 10,
store: new MongoStore({
uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60 * 60,
errorHandler: console.error.bind(null, 'rate-limit-mongo'),
collectionName: "expressRateRecords-passwordLimiter",
}),
windowMs: 1000 * 60 * 60,
max: 50,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => {

@ -1,5 +1,5 @@
import { Types } from "mongoose";
import { Secret, ISecret, Membership } from "../models";
import { Secret, ISecret } from "../models";
import { EESecretService, EELogService } from "../ee/services";
import { IAction, SecretVersion } from "../ee/models";
import {
@ -9,9 +9,9 @@ import {
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
} from "../variables";
import _ from "lodash";
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
interface V1PushSecret {
ciphertextKey: string;
@ -194,6 +194,8 @@ const v1PushSecrets = async ({
secretValueIV: newSecret.ivValue,
secretValueTag: newSecret.tagValue,
secretValueHash: newSecret.hashValue,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
});
}),
});
@ -225,6 +227,8 @@ const v1PushSecrets = async ({
secretCommentIV: s.ivComment,
secretCommentTag: s.tagComment,
secretCommentHash: s.hashComment,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
};
if (toAdd[idx].type === "personal") {
@ -254,6 +258,8 @@ const v1PushSecrets = async ({
secretValueIV,
secretValueTag,
secretValueHash,
algorithm,
keyEncoding,
}) =>
new SecretVersion({
secret: _id,
@ -271,6 +277,8 @@ const v1PushSecrets = async ({
secretValueIV,
secretValueTag,
secretValueHash,
algorithm,
keyEncoding,
})
),
});
@ -279,6 +287,7 @@ const v1PushSecrets = async ({
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
});
};
@ -467,6 +476,8 @@ const v2PushSecrets = async ({
workspace: workspaceId,
type: toAdd[idx].type,
environment,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
...(toAdd[idx].type === "personal" ? { user: userId } : {}),
}))
);
@ -478,6 +489,8 @@ const v2PushSecrets = async ({
...secretDocument,
secret: secretDocument._id,
isDeleted: false,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
});
}),
});
@ -494,6 +507,7 @@ const v2PushSecrets = async ({
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
});
// (EE) create (audit) log

File diff suppressed because it is too large Load Diff

@ -1,24 +1,8 @@
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]
@ -26,7 +10,7 @@ import {
* @param {String} obj.email - email of user to initialize
* @returns {Object} user - the initialized user
*/
const setupAccount = async ({ email }: { email: string }) => {
export const setupAccount = async ({ email }: { email: string }) => {
const user = await new User({
email
}).save();
@ -52,7 +36,7 @@ const setupAccount = async ({ email }: { email: string }) => {
* @param {String} obj.verifier - verifier for auth SRP
* @returns {Object} user - the completed user
*/
const completeAccount = async ({
export const completeAccount = async ({
userId,
firstName,
lastName,
@ -113,7 +97,7 @@ const completeAccount = async ({
* @param {String} obj.ip - login ip address
* @param {String} obj.userAgent - login user-agent
*/
const checkUserDevice = async ({
export const checkUserDevice = async ({
user,
ip,
userAgent
@ -148,206 +132,4 @@ const checkUserDevice = async ({
}
});
}
}
/**
* 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,136 +1,14 @@
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import { Types } from 'mongoose';
import {
Workspace,
Bot,
Membership,
Key,
Secret,
User,
IUser,
ServiceAccountWorkspacePermission,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
SecretBlindIndexData
Secret
} 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';
import { encryptSymmetric } from '../utils/crypto';
import { SecretService } from '../services';
/**
* 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,
requireBlindIndicesEnabled
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions?: string[];
requireBlindIndicesEnabled: boolean;
}) => {
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw WorkspaceNotFoundError({
message: 'Failed to find workspace'
});
if (requireBlindIndicesEnabled) {
// case: blind indices are not enabled for secrets in this workspace
// (i.e. workspace was created before blind indices were introduced
// and no admin has enabled it)
const secretBlindIndexData = await SecretBlindIndexData.exists({
workspace: new Types.ObjectId(workspaceId)
});
if (!secretBlindIndexData) throw UnauthorizedRequestError({
message: 'Failed workspace authorization due to blind indices not being enabled'
});
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
return ({ membership, workspace });
}
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, workspace });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for workspace'
});
}
/**
* Create a workspace with name [name] in organization with id [organizationId]
* and a bot for it.
@ -203,7 +81,6 @@ const deleteWorkspace = async ({ id }: { id: string }) => {
};
export {
validateClientForWorkspace,
createWorkspace,
deleteWorkspace
};

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

@ -1559,20 +1559,35 @@ const syncSecretsGitLab = async ({
environment_scope: string;
}
// get secrets from gitlab
const getSecretsRes: GitLabSecret[] = (
await standardRequest.get(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`,
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => {
const gitLabApiUrl = `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integrationAppId}/variables`;
const headers = {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
};
let allEnvVariables: GitLabSecret[] = [];
let url: string | null = `${gitLabApiUrl}?per_page=100`;
while (url) {
const response: any = await standardRequest.get(url, { headers });
allEnvVariables = [...allEnvVariables, ...response.data];
const linkHeader = response.headers.link;
const nextLink = linkHeader?.split(',').find((part: string) => part.includes('rel="next"'));
if (nextLink) {
url = nextLink.trim().split(';')[0].slice(1, -1);
} else {
url = null;
}
)
)
.data
.filter((secret: GitLabSecret) =>
}
return allEnvVariables;
};
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) =>
secret.environment_scope === integration.targetEnvironment
);
@ -1631,6 +1646,7 @@ const syncSecretsGitLab = async ({
);
}
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);

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

@ -0,0 +1,41 @@
export interface IGenerateKeyPairOutput {
publicKey: string;
privateKey: string
}
export interface IEncryptAsymmetricInput {
plaintext: string;
publicKey: string;
privateKey: string;
}
export interface IEncryptAsymmetricOutput {
ciphertext: string;
nonce: string;
}
export interface IDecryptAsymmetricInput {
ciphertext: string;
nonce: string;
publicKey: string;
privateKey: string;
}
export interface IEncryptSymmetricInput {
plaintext: string;
key: string;
}
export interface IEncryptSymmetricOutput {
ciphertext: string;
iv: string;
tag: string;
}
export interface IDecryptSymmetricInput {
ciphertext: string;
iv: string;
tag: string;
key: string;
}

@ -0,0 +1 @@
export * from './crypto';

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

@ -7,9 +7,6 @@ import {
getAuthAPIKeyPayload,
getAuthSAAKPayload
} from '../helpers/auth';
import {
UnauthorizedRequestError
} from '../utils/errors';
import {
IUser,
IServiceAccount,
@ -45,7 +42,7 @@ const requireAuth = ({
acceptedAuthModes: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// validate auth token against accepted auth modes [acceptedAuthModes]
// and return token type [authTokenType] and value [authTokenValue]
const { authMode, authTokenValue } = validateAuthMode({
@ -80,13 +77,13 @@ const requireAuth = ({
req.user = authPayload;
break;
}
req.requestData = {
...req.params,
...req.query,
...req.body,
}
req.authData = {
authMode,
authPayload, // User, ServiceAccount, ServiceTokenData
@ -94,7 +91,7 @@ const requireAuth = ({
authIP: req.ip,
authUserAgent: req.headers['user-agent'] ?? 'other'
}
return next();
}
}

@ -1,9 +1,6 @@
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';
import { validateClientForBot } from '../validation';
type req = 'params' | 'body' | 'query';

@ -1,10 +1,6 @@
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';
import { validateClientForIntegration } from '../validation';
/**
* Validate if user on request is a member of workspace with proper roles associated

@ -1,10 +1,6 @@
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';
import { validateClientForIntegrationAuth } from '../validation';
type req = 'params' | 'body' | 'query';

@ -1,13 +1,6 @@
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import {
Membership,
} from '../models';
import {
validateClientForMembership,
validateMembership
} from '../helpers/membership';
import { validateClientForMembership } from '../validation';
type req = 'params' | 'body' | 'query';

@ -1,16 +1,6 @@
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import {
MembershipOrg
} from '../models';
import {
validateClientForMembershipOrg,
validateMembershipOrg
} from '../helpers/membershipOrg';
// TODO: transform
import { validateClientForMembershipOrg } from '../validation';
type req = 'params' | 'body' | 'query';

@ -1,9 +1,6 @@
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';
import { validateClientForOrganization } from '../validation';
type req = 'params' | 'body' | 'query';

@ -1,13 +1,6 @@
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';
import { validateClientForSecret } from '../validation';
// note: used for old /v1/secret and /v2/secret routes.
// newer /v2/secrets routes use [requireSecretsAuth] middleware with the exception

@ -1,8 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { UnauthorizedRequestError } from '../utils/errors';
import { Secret, Membership } from '../models';
import { validateClientForSecrets } from '../helpers/secrets';
import { validateClientForSecrets } from '../validation';
const requireSecretsAuth = ({
acceptedRoles,

@ -1,15 +1,6 @@
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';
import { validateClientForServiceAccount } from '../validation';
type req = 'params' | 'body' | 'query';

@ -1,9 +1,6 @@
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';
import { validateClientForServiceTokenData } from '../validation';
type req = 'params' | 'body' | 'query';

@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { validateClientForWorkspace } from '../helpers/workspace';
import { validateClientForWorkspace } from '../validation';
type req = 'params' | 'body' | 'query';

@ -1,4 +1,9 @@
import { Schema, model, Types } from 'mongoose';
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../variables';
export interface IBackupPrivateKey {
_id: Types.ObjectId;
@ -7,6 +12,8 @@ export interface IBackupPrivateKey {
iv: string;
tag: string;
salt: string;
algorithm: string;
keyEncoding: 'base64' | 'utf8';
verifier: string;
}
@ -32,6 +39,19 @@ const backupPrivateKeySchema = new Schema<IBackupPrivateKey>(
select: false,
required: true
},
algorithm: { // the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
],
required: true
},
salt: {
type: String,
select: false,

@ -1,4 +1,10 @@
import { Schema, model, Types } from 'mongoose';
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_HEX,
ENCODING_SCHEME_BASE64
} from '../variables';
export interface IBot {
_id: Types.ObjectId;
@ -9,6 +15,8 @@ export interface IBot {
encryptedPrivateKey: string;
iv: string;
tag: string;
algorithm: 'aes-256-gcm';
keyEncoding: 'base64' | 'utf8';
}
const botSchema = new Schema<IBot>(
@ -45,6 +53,21 @@ const botSchema = new Schema<IBot>(
type: String,
required: true,
select: false
},
algorithm: { // the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
select: false
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
],
required: true,
select: false
}
},
{

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

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

@ -14,6 +14,9 @@ import {
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from "../variables";
export interface IIntegrationAuth extends Document {
@ -31,6 +34,8 @@ export interface IIntegrationAuth extends Document {
accessCiphertext?: string;
accessIV?: string;
accessTag?: string;
algorithm?: 'aes-256-gcm';
keyEncoding?: 'utf8' | 'base64';
accessExpiresAt?: Date;
}
@ -109,6 +114,19 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
type: Date,
select: false,
},
algorithm: { // the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
],
required: true
}
},
{
timestamps: true,

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

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

@ -1,4 +1,9 @@
import { Schema, model, Types, Document } from 'mongoose';
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../variables';
export interface ISecretBlindIndexData extends Document {
_id: Types.ObjectId;
@ -6,6 +11,8 @@ export interface ISecretBlindIndexData extends Document {
encryptedSaltCiphertext: string;
saltIV: string;
saltTag: string;
algorithm: 'aes-256-gcm';
keyEncoding: 'base64' | 'utf8'
}
const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
@ -15,7 +22,7 @@ const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
ref: 'Workspace',
required: true
},
encryptedSaltCiphertext: {
encryptedSaltCiphertext: { // TODO: make these select: false
type: String,
required: true
},
@ -26,7 +33,23 @@ const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
saltTag: {
type: String,
required: true
},
algorithm: {
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
select: false
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
],
required: true,
select: false
}
}
);

@ -1,7 +1,13 @@
import { Schema, model, Types, Document } from 'mongoose';
export enum AuthProvider {
GOOGLE = 'google',
}
export interface IUser extends Document {
_id: Types.ObjectId;
authId?: string;
authProvider?: AuthProvider;
email: string;
firstName?: string;
lastName?: string;
@ -26,9 +32,17 @@ export interface IUser extends Document {
const userSchema = new Schema<IUser>(
{
authId: {
type: String,
},
authProvider: {
type: String,
enum: AuthProvider,
},
email: {
type: String,
required: true
required: true,
unique: true,
},
firstName: {
type: String

@ -1,6 +1,7 @@
import express from 'express';
const router = express.Router();
import { body } from 'express-validator';
import passport from 'passport';
import { requireAuth, validateRequest } from '../../middleware';
import { authController } from '../../controllers/v1';
import { authLimiter } from '../../helpers/rateLimiter';
@ -27,20 +28,37 @@ router.post( // deprecated (moved to api/v2/auth/login2)
);
router.post(
'/logout',
'/logout',
authLimiter,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
}),
authController.logout
);
router.post(
'/checkAuth',
'/checkAuth',
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
}),
authController.checkAuth
);
router.get(
'/redirect/google',
authLimiter,
passport.authenticate('google', {
scope: ['profile', 'email'],
session: false,
}),
)
router.get(
'/callback/google',
passport.authenticate('google', { failureRedirect: '/login/provider/error', session: false }),
authController.handleAuthProviderCallback,
)
export default router;

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

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

@ -2,204 +2,233 @@ import express from 'express';
const router = express.Router();
import { Types } from 'mongoose';
import {
requireAuth,
requireWorkspaceAuth,
requireSecretsAuth,
validateRequest
requireAuth,
requireWorkspaceAuth,
requireSecretsAuth,
validateRequest,
} from '../../middleware';
import { validateClientForSecrets } from '../../validation';
import { query, body } from 'express-validator';
import { secretsController } from '../../controllers/v2';
import { validateClientForSecrets } from '../../helpers/secrets';
import {
ADMIN,
MEMBER,
SECRET_PERSONAL,
SECRET_SHARED,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
ADMIN,
MEMBER,
SECRET_PERSONAL,
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
} from '../../types/secret';
import { BatchSecretRequest } from '../../types/secret';
router.post(
'/batch',
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body'
}),
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('requests')
.exists()
.custom(async (requests: BatchSecretRequest[], { req }) => {
if (Array.isArray(requests)) {
const secretIds = requests
.map((request) => request.secret._id)
.filter((secretId) => secretId !== undefined)
'/batch',
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body',
}),
body('workspaceId').exists().isString().trim(),
body('folderId').default('root').isString().trim(),
body('environment').exists().isString().trim(),
body('requests')
.exists()
.custom(async (requests: BatchSecretRequest[], { req }) => {
if (Array.isArray(requests)) {
const secretIds = requests
.map((request) => request.secret._id)
.filter((secretId) => secretId !== undefined);
if (secretIds.length > 0) {
req.secrets = await validateClientForSecrets({
authData: req.authData,
secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
requiredPermissions: []
});
}
}
return true;
}),
validateRequest,
secretsController.batchSecrets
if (secretIds.length > 0) {
req.secrets = await validateClientForSecrets({
authData: req.authData,
secretIds: secretIds.map(
(secretId: string) => new Types.ObjectId(secretId)
),
requiredPermissions: [],
});
}
}
return true;
}),
validateRequest,
secretsController.batchSecrets
);
router.post(
'/',
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('secrets')
.exists()
.custom((value) => {
if (Array.isArray(value)) {
// case: create multiple secrets
if (value.length === 0) throw new Error('secrets cannot be an empty array')
for (const secret of value) {
if (
!secret.type ||
!(secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED) ||
!secret.secretKeyCiphertext ||
!secret.secretKeyIV ||
!secret.secretKeyTag ||
(typeof secret.secretValueCiphertext !== 'string') ||
!secret.secretValueIV ||
!secret.secretValueTag
) {
throw new Error('secrets array must contain objects that have required secret properties');
}
}
} else if (typeof value === 'object') {
// case: update 1 secret
if (
!value.type ||
!(value.type === SECRET_PERSONAL || value.type === SECRET_SHARED) ||
!value.secretKeyCiphertext ||
!value.secretKeyIV ||
!value.secretKeyTag ||
!value.secretValueCiphertext ||
!value.secretValueIV ||
!value.secretValueTag
) {
throw new Error('secrets object is missing required secret properties');
}
} else {
throw new Error('secrets must be an object or an array of objects')
}
'/',
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('folderId').default('root').isString().trim(),
body('secrets')
.exists()
.custom((value) => {
if (Array.isArray(value)) {
// case: create multiple secrets
if (value.length === 0)
throw new Error('secrets cannot be an empty array');
for (const secret of value) {
if (
!secret.type ||
!(
secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED
) ||
!secret.secretKeyCiphertext ||
!secret.secretKeyIV ||
!secret.secretKeyTag ||
typeof secret.secretValueCiphertext !== 'string' ||
!secret.secretValueIV ||
!secret.secretValueTag
) {
throw new Error(
'secrets array must contain objects that have required secret properties'
);
}
}
} else if (typeof value === 'object') {
// case: update 1 secret
if (
!value.type ||
!(value.type === SECRET_PERSONAL || value.type === SECRET_SHARED) ||
!value.secretKeyCiphertext ||
!value.secretKeyIV ||
!value.secretKeyTag ||
!value.secretValueCiphertext ||
!value.secretValueIV ||
!value.secretValueTag
) {
throw new Error(
'secrets object is missing required secret properties'
);
}
} else {
throw new Error('secrets must be an object or an array of objects');
}
return true;
}),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
return true;
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body',
locationEnvironment: 'body',
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}),
secretsController.createSecrets
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body',
locationEnvironment: 'body',
requiredPermissions: [PERMISSION_WRITE_SECRETS],
}),
secretsController.createSecrets
);
router.get(
'/',
query('workspaceId').exists().trim(),
query('environment').exists().trim(),
query('tagSlugs'),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_ACCOUNT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'query',
locationEnvironment: 'query',
requiredPermissions: [PERMISSION_READ_SECRETS]
}),
secretsController.getSecrets
'/',
query('workspaceId').exists().trim(),
query('environment').exists().trim(),
query('tagSlugs'),
query('folderId').default('root').isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'query',
locationEnvironment: 'query',
requiredPermissions: [PERMISSION_READ_SECRETS],
}),
secretsController.getSecrets
);
router.patch(
'/',
body('secrets')
.exists()
.custom((value) => {
if (Array.isArray(value)) {
// case: update multiple secrets
if (value.length === 0) throw new Error('secrets cannot be an empty array')
for (const secret of value) {
if (
!secret.id
) {
throw new Error('Each secret must contain a ID property');
}
}
} else if (typeof value === 'object') {
// case: update 1 secret
if (
!value.id
) {
throw new Error('secret must contain a ID property');
}
} else {
throw new Error('secrets must be an object or an array of objects')
}
'/',
body('secrets')
.exists()
.custom((value) => {
if (Array.isArray(value)) {
// case: update multiple secrets
if (value.length === 0)
throw new Error('secrets cannot be an empty array');
for (const secret of value) {
if (!secret.id) {
throw new Error('Each secret must contain a ID property');
}
}
} else if (typeof value === 'object') {
// case: update 1 secret
if (!value.id) {
throw new Error('secret must contain a ID property');
}
} else {
throw new Error('secrets must be an object or an array of objects');
}
return true;
}),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
return true;
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}),
secretsController.updateSecrets
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
],
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS],
}),
secretsController.updateSecrets
);
router.delete(
'/',
body('secretIds')
.exists()
.custom((value) => {
// case: delete 1 secret
if (typeof value === 'string') return true;
'/',
body('secretIds')
.exists()
.custom((value) => {
// case: delete 1 secret
if (typeof value === 'string') return true;
if (Array.isArray(value)) {
// case: delete multiple secrets
if (value.length === 0) throw new Error('secrets cannot be an empty array');
return value.every((id: string) => typeof id === 'string')
}
if (Array.isArray(value)) {
// case: delete multiple secrets
if (value.length === 0)
throw new Error('secrets cannot be an empty array');
return value.every((id: string) => typeof id === 'string');
}
throw new Error('secretIds must be a string or an array of strings');
})
.not()
.isEmpty(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}),
secretsController.deleteSecrets
throw new Error('secretIds must be a string or an array of strings');
})
.not()
.isEmpty(),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
],
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS],
}),
secretsController.deleteSecrets
);
export default router;
export default router;

@ -0,0 +1,29 @@
import express from 'express';
import { body } from 'express-validator';
import { validateRequest } from '../../middleware';
import { authController } from '../../controllers/v3';
import { authLimiter } from '../../helpers/rateLimiter';
const router = express.Router();
router.post(
'/login1',
authLimiter,
body('email').isString().trim(),
body('providerAuthToken').isString().trim().optional({nullable: true}),
body('clientPublicKey').isString().trim().notEmpty(),
validateRequest,
authController.login1
);
router.post(
'/login2',
authLimiter,
body('email').isString().trim(),
body('providerAuthToken').isString().trim().optional({nullable: true}),
body('clientProof').isString().trim().notEmpty(),
validateRequest,
authController.login2
);
export default router;

@ -1,7 +1,11 @@
import auth from './auth';
import secrets from './secrets';
import workspaces from './workspaces';
import signup from './signup';
export {
auth,
secrets,
workspaces
}
signup,
workspaces,
}

@ -0,0 +1,29 @@
import express from 'express';
const router = express.Router();
import { body } from 'express-validator';
import { signupController } from '../../controllers/v3';
import { authLimiter } from '../../helpers/rateLimiter';
import { validateRequest } from '../../middleware';
router.post(
'/complete-account/signup',
authLimiter,
body('email').exists().isString().trim().notEmpty().isEmail(),
body('firstName').exists().isString().trim().notEmpty(),
body('lastName').exists().isString().trim().optional({nullable: true}),
body('protectedKey').exists().isString().trim().notEmpty(),
body('protectedKeyIV').exists().isString().trim().notEmpty(),
body('protectedKeyTag').exists().isString().trim().notEmpty(),
body('publicKey').exists().isString().trim().notEmpty(),
body('encryptedPrivateKey').exists().isString().trim().notEmpty(),
body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(),
body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(),
body('salt').exists().isString().trim().notEmpty(),
body('verifier').exists().isString().trim().notEmpty(),
body('organizationName').exists().isString().trim().notEmpty(),
body('providerAuthToken').isString().trim().optional({nullable: true}),
validateRequest,
signupController.completeAccountSignup,
);
export default router;

@ -0,0 +1,189 @@
import { nanoid } from "nanoid";
import { TFolderSchema } from "../models/folder";
type TAppendFolderDTO = {
folderName: string;
parentFolderId?: string;
};
type TRenameFolderDTO = {
folderName: string;
folderId: string;
};
export const validateFolderName = (folderName: string) => {
const validNameRegex = /^[a-zA-Z0-9-_]+$/;
return validNameRegex.test(folderName);
};
export const generateFolderId = (): string => nanoid(12);
// simple bfs search
export const searchByFolderId = (
root: TFolderSchema,
folderId: string
): TFolderSchema | undefined => {
const queue = [root];
while (queue.length) {
const folder = queue.pop() as TFolderSchema;
if (folder.id === folderId) {
return folder;
}
queue.push(...folder.children);
}
};
export const folderBfsTraversal = async (
root: TFolderSchema,
callback: (data: TFolderSchema) => void | Promise<void>
) => {
const queue = [root];
while (queue.length) {
const folder = queue.pop() as TFolderSchema;
await callback(folder);
queue.push(...folder.children);
}
};
// bfs and then append to the folder
const appendChild = (folders: TFolderSchema, folderName: string) => {
const folder = folders.children.find(({ name }) => name === folderName);
if (folder) {
throw new Error("Folder already exists");
}
const id = generateFolderId();
folders.version += 1;
folders.children.push({
id,
name: folderName,
children: [],
version: 1,
});
return { id, name: folderName };
};
// root of append child wrapper
export const appendFolder = (
folders: TFolderSchema,
{ folderName, parentFolderId }: TAppendFolderDTO
) => {
const isRoot = !parentFolderId;
if (isRoot) {
return appendChild(folders, folderName);
}
const folder = searchByFolderId(folders, parentFolderId);
if (!folder) {
throw new Error("Parent Folder not found");
}
return appendChild(folder, folderName);
};
export const renameFolder = (
folders: TFolderSchema,
{ folderName, folderId }: TRenameFolderDTO
) => {
const folder = searchByFolderId(folders, folderId);
if (!folder) {
throw new Error("Folder doesn't exist");
}
folder.name = folderName;
};
// bfs but stops on parent folder
// Then unmount the required child and then return both
export const deleteFolderById = (folders: TFolderSchema, folderId: string) => {
const queue = [folders];
while (queue.length) {
const folder = queue.pop() as TFolderSchema;
const index = folder.children.findIndex(({ id }) => folderId === id);
if (index !== -1) {
const deletedFolder = folder.children.splice(index, 1);
return { deletedNode: deletedFolder[0], parent: folder };
}
queue.push(...folder.children);
}
};
// bfs but return parent of the folderID
export const getParentFromFolderId = (
folders: TFolderSchema,
folderId: string
) => {
const queue = [folders];
while (queue.length) {
const folder = queue.pop() as TFolderSchema;
const index = folder.children.findIndex(({ id }) => folderId === id);
if (index !== -1) return folder;
queue.push(...folder.children);
}
};
// to get all folders ids from everything from below nodes
export const getAllFolderIds = (folders: TFolderSchema) => {
const folderIds: Array<{ id: string; name: string }> = [];
const queue = [folders];
while (queue.length) {
const folder = queue.pop() as TFolderSchema;
folderIds.push({ id: folder.id, name: folder.name });
queue.push(...folder.children);
}
return folderIds;
};
// To get the path of a folder from the root. Used for breadcrumbs
// LOGIC: We do dfs instead if bfs
// Each time we go down we record the current node
// We then record the number of childs of each root node
// When we reach leaf node or when all childs of a root node are visited
// We remove it from path recorded by using the total child record
export const searchByFolderIdWithDir = (
folders: TFolderSchema,
folderId: string
) => {
const stack = [folders];
const dir: Array<{ id: string; name: string }> = [];
const hits: Record<string, number> = {};
while (stack.length) {
const folder = stack.shift() as TFolderSchema;
// score the hit
hits[folder.id] = folder.children.length;
const parent = dir[dir.length - 1];
if (parent) hits[parent.id] -= 1;
if (folder.id === folderId) {
dir.push({ name: folder.name, id: folder.id });
return { folder, dir };
}
if (folder.children.length) {
dir.push({ name: folder.name, id: folder.id });
stack.unshift(...folder.children);
} else {
if (!hits[parent.id]) {
dir.pop();
}
}
}
return;
};
// to get folder of a path given
// Like /frontend/folder#1
export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => {
const path = searchPath.split("/").filter(Boolean);
const queue = [folders];
let segment: TFolderSchema | undefined;
while (queue.length && path.length) {
const folder = queue.pop();
const segmentPath = path.shift();
segment = folder?.children.find(({ name }) => name === segmentPath);
if (!segment) return;
queue.push(segment);
}
return segment;
};

@ -1,4 +1,3 @@
// WIP
import { Types } from 'mongoose';
import {
ISecret
@ -11,7 +10,6 @@ import {
DeleteSecretParams
} from '../interfaces/services/SecretService';
import {
initSecretBlindIndexDataHelper,
createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper,
@ -24,16 +22,6 @@ import {
} from '../helpers/secrets';
class SecretService {
/**
*
* @param param0 h
* @returns
*/
static async initSecretBlindIndexDataHelper() {
return await initSecretBlindIndexDataHelper();
}
/**
* Create secret blind index data containing encrypted blind index salt

@ -1,6 +1,6 @@
import nodemailer from 'nodemailer';
import {
SMTP_HOST_SENDGRID,
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL,
@ -35,29 +35,29 @@ export const initSmtp = async () => {
mailOpts.requireTLS = true;
break;
case SMTP_HOST_MAILGUN:
mailOpts.requireTLS = true;
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
case SMTP_HOST_SOCKETLABS:
mailOpts.requireTLS = true;
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
case SMTP_HOST_ZOHOMAIL:
mailOpts.requireTLS = true;
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
case SMTP_HOST_GMAIL:
mailOpts.requireTLS = true;
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
break;
default:
if ((await getSmtpHost()).includes('amazonaws.com')) {
mailOpts.tls = {
@ -76,6 +76,7 @@ export const initSmtp = async () => {
.then((err) => {
Sentry.setUser(null);
Sentry.captureMessage('SMTP - Successfully connected');
console.log("SMTP - Successfully connected")
})
.catch(async (err) => {
Sentry.setUser(null);

@ -9,6 +9,12 @@ import {
AuthData
} from '../../interfaces/middleware';
declare module 'express' {
interface Request {
user?: any;
}
}
// TODO: fix (any) types
declare global {
namespace Express {
@ -18,6 +24,7 @@ declare global {
workspace: any;
membership: any;
targetMembership: any;
providerAuthToken: any;
organization: any;
membershipOrg: any;
integration: any;

@ -1,53 +1,46 @@
import { Types } from 'mongoose';
import { Assign, Omit } from 'utility-types';
import { ISecret } from '../../models';
import { mongo } from 'mongoose';
import { Types } from "mongoose";
import { Assign, Omit } from "utility-types";
import { ISecret } from "../../models";
import { mongo } from "mongoose";
// Everything is required, except the omitted types
export type CreateSecretRequestBody = Omit<ISecret, "user" | "version" | "environment" | "workspace">;
export type CreateSecretRequestBody = Omit<
ISecret,
"user" | "version" | "environment" | "workspace"
>;
// Omit the listed properties, then make everything optional and then make _id required
export type ModifySecretRequestBody = Assign<Partial<Omit<ISecret, "user" | "version" | "environment" | "workspace">>, { _id: string }>;
// Omit the listed properties, then make everything optional and then make _id required
export type ModifySecretRequestBody = Assign<
Partial<Omit<ISecret, "user" | "version" | "environment" | "workspace">>,
{ _id: string }
>;
// Used for modeling sanitized secrets before uplaod. To be used for converting user input for uploading
export type SanitizedSecretModify = Partial<Omit<ISecret, "user" | "version" | "environment" | "workspace">>;
export type SanitizedSecretModify = Partial<
Omit<ISecret, "user" | "version" | "environment" | "workspace">
>;
// Everything is required, except the omitted types
export type SanitizedSecretForCreate = Omit<ISecret, "version" | "_id">;
export interface BatchSecretRequest {
id: string;
method: 'POST' | 'PATCH' | 'DELETE';
secret: Secret;
id: string;
method: "POST" | "PATCH" | "DELETE";
secret: Secret;
}
export interface BatchSecret {
_id: string;
type: 'shared' | 'personal',
secretBlindIndex: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: string[];
_id: string;
type: "shared" | "personal";
secretBlindIndex: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: string[];
}
export interface BatchSecret {
_id: string;
type: 'shared' | 'personal',
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: string[];
}

@ -5,6 +5,7 @@
************************************************************************************************/
import { Key, Membership, MembershipOrg, Organization, User, Workspace } from "../models";
import { SecretService } from "../services";
import { Types } from 'mongoose';
import { getNodeEnv } from '../config';
@ -119,7 +120,12 @@ export const createTestUserForDevelopment = async () => {
// create workspace if not exist
const workspaceInDB = await Workspace.findById(testWorkspaceId)
if (!workspaceInDB) {
await Workspace.create(testWorkspace)
const workspace = await Workspace.create(testWorkspace)
// initialize blind index salt for workspace
await SecretService.createSecretBlindIndexData({
workspaceId: workspace._id
});
}
// create workspace key if not exist

@ -1,10 +1,22 @@
import express from 'express';
import passport from 'passport';
import { AuthData } from '../interfaces/middleware';
import {
User,
ServiceAccount,
ServiceTokenData,
ServiceToken
AuthProvider,
User,
ServiceAccount,
ServiceTokenData,
} from '../models';
import { createToken } from '../helpers/auth';
import {
getClientIdGoogle,
getClientSecretGoogle,
getJwtProviderAuthLifetime,
getJwtProviderAuthSecret
} from '../config';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const GoogleStrategy = require('passport-google-oauth20').Strategy;
// TODO: find a more optimal folder structure to store these types of functions
@ -14,17 +26,17 @@ import {
* @returns
*/
const getAuthDataPayloadIdObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { userId: authData.authPayload._id };
}
if (authData.authPayload instanceof User) {
return { userId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceAccount) {
return { serviceAccountId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceAccount) {
return { serviceAccountId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { serviceTokenDataId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { serviceTokenDataId: authData.authPayload._id };
}
};
@ -35,20 +47,72 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
*/
const getAuthDataPayloadUserObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { user: authData.authPayload._id };
}
if (authData.authPayload instanceof User) {
return { user: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceAccount) {
return { user: authData.authPayload.user };
}
if (authData.authPayload instanceof ServiceAccount) {
return { user: authData.authPayload.user };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { user: authData.authPayload.user };
if (authData.authPayload instanceof ServiceTokenData) {
return { user: authData.authPayload.user };
}
}
const initializePassport = async () => {
const googleClientSecret = await getClientSecretGoogle();
const googleClientId = await getClientIdGoogle();
passport.use(new GoogleStrategy({
passReqToCallback: true,
clientID: googleClientId,
clientSecret: googleClientSecret,
callbackURL: '/api/v1/auth/callback/google',
scope: ['profile', ' email'],
}, async (
req: express.Request,
accessToken: string,
refreshToken: string,
profile: any,
cb: any
) => {
try {
const email = profile.emails[0].value;
let user = await User.findOne({
authProvider: AuthProvider.GOOGLE,
authId: profile.id,
}).select('+publicKey')
if (!user) {
user = await new User({
email,
authProvider: AuthProvider.GOOGLE,
authId: profile.id,
}).save();
}
const providerAuthToken = createToken({
payload: {
userId: user._id.toString(),
email: user.email,
authProvider: user.authProvider,
isUserCompleted: !!user.publicKey
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(),
});
req.providerAuthToken = providerAuthToken;
cb(null, profile);
} catch (err) {
cb(null, false);
}
}));
}
export {
getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj
}
getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj,
initializePassport,
}

@ -1,139 +0,0 @@
import nacl from 'tweetnacl';
import util from 'tweetnacl-util';
import AesGCM from './aes-gcm';
/**
* Return new base64, NaCl, public-private key pair.
* @returns {Object} obj
* @returns {String} obj.publicKey - base64, NaCl, public key
* @returns {String} obj.privateKey - base64, NaCl, private key
*/
const generateKeyPair = () => {
const pair = nacl.box.keyPair();
return ({
publicKey: util.encodeBase64(pair.publicKey),
privateKey: util.encodeBase64(pair.secretKey)
});
}
/**
* Return assymmetrically encrypted [plaintext] using [publicKey] where
* [publicKey] likely belongs to the recipient.
* @param {Object} obj
* @param {String} obj.plaintext - plaintext to encrypt
* @param {String} obj.publicKey - public key of the recipient
* @param {String} obj.privateKey - private key of the sender (current user)
* @returns {Object} obj
* @returns {String} ciphertext - base64-encoded ciphertext
* @returns {String} nonce - base64-encoded nonce
*/
const encryptAsymmetric = ({
plaintext,
publicKey,
privateKey
}: {
plaintext: string;
publicKey: string;
privateKey: string;
}) => {
const nonce = nacl.randomBytes(24);
const ciphertext = nacl.box(
util.decodeUTF8(plaintext),
nonce,
util.decodeBase64(publicKey),
util.decodeBase64(privateKey)
);
return {
ciphertext: util.encodeBase64(ciphertext),
nonce: util.encodeBase64(nonce)
};
};
/**
* Return assymmetrically decrypted [ciphertext] using [privateKey] where
* [privateKey] likely belongs to the recipient.
* @param {Object} obj
* @param {String} obj.ciphertext - ciphertext to decrypt
* @param {String} obj.nonce - nonce
* @param {String} obj.publicKey - public key of the sender
* @param {String} obj.privateKey - private key of the receiver (current user)
* @param {String} plaintext - UTF8 plaintext
*/
const decryptAsymmetric = ({
ciphertext,
nonce,
publicKey,
privateKey
}: {
ciphertext: string;
nonce: string;
publicKey: string;
privateKey: string;
}): string => {
const plaintext: any = nacl.box.open(
util.decodeBase64(ciphertext),
util.decodeBase64(nonce),
util.decodeBase64(publicKey),
util.decodeBase64(privateKey)
);
return util.encodeUTF8(plaintext);
};
/**
* Return symmetrically encrypted [plaintext] using [key].
* @param {Object} obj
* @param {String} obj.plaintext - plaintext to encrypt
* @param {String} obj.key - hex key
*/
const encryptSymmetric = ({
plaintext,
key
}: {
plaintext: string;
key: string;
}) => {
const obj = AesGCM.encrypt(plaintext, key);
const { ciphertext, iv, tag } = obj;
return {
ciphertext,
iv,
tag
};
};
/**
* Return symmetrically decypted [ciphertext] using [iv], [tag],
* and [key].
* @param {Object} obj
* @param {String} obj.ciphertext - ciphertext to decrypt
* @param {String} obj.iv - iv
* @param {String} obj.tag - tag
* @param {String} obj.key - hex key
*
*/
const decryptSymmetric = ({
ciphertext,
iv,
tag,
key
}: {
ciphertext: string;
iv: string;
tag: string;
key: string;
}): string => {
const plaintext = AesGCM.decrypt(ciphertext, iv, tag, key);
return plaintext;
};
export {
generateKeyPair,
encryptAsymmetric,
decryptAsymmetric,
encryptSymmetric,
decryptSymmetric
};

@ -0,0 +1,166 @@
import crypto from 'crypto';
import nacl from 'tweetnacl';
import util from 'tweetnacl-util';
import {
IGenerateKeyPairOutput,
IEncryptAsymmetricInput,
IEncryptAsymmetricOutput,
IDecryptAsymmetricInput,
IEncryptSymmetricInput,
IDecryptSymmetricInput
} from '../../interfaces/utils';
import { BadRequestError } from '../errors';
import {
ALGORITHM_AES_256_GCM,
NONCE_BYTES_SIZE,
BLOCK_SIZE_BYTES_16
} from '../../variables';
/**
* Return new base64, NaCl, public-private key pair.
* @returns {Object} obj
* @returns {String} obj.publicKey - (base64) NaCl, public key
* @returns {String} obj.privateKey - (base64), NaCl, private key
*/
const generateKeyPair = (): IGenerateKeyPairOutput => {
const pair = nacl.box.keyPair();
return ({
publicKey: util.encodeBase64(pair.publicKey),
privateKey: util.encodeBase64(pair.secretKey)
});
}
/**
* Return assymmetrically encrypted [plaintext] using [publicKey] where
* [publicKey] likely belongs to the recipient.
* @param {Object} obj
* @param {String} obj.plaintext - plaintext to encrypt
* @param {String} obj.publicKey - (base64) Nacl public key of the recipient
* @param {String} obj.privateKey - (base64) Nacl private key of the sender (current user)
* @returns {Object} obj
* @returns {String} obj.ciphertext - (base64) ciphertext
* @returns {String} obj.nonce - (base64) nonce
*/
const encryptAsymmetric = ({
plaintext,
publicKey,
privateKey
}: IEncryptAsymmetricInput): IEncryptAsymmetricOutput => {
const nonce = nacl.randomBytes(24);
const ciphertext = nacl.box(
util.decodeUTF8(plaintext),
nonce,
util.decodeBase64(publicKey),
util.decodeBase64(privateKey)
);
return {
ciphertext: util.encodeBase64(ciphertext),
nonce: util.encodeBase64(nonce)
};
};
/**
* Return assymmetrically decrypted [ciphertext] using [privateKey] where
* [privateKey] likely belongs to the recipient.
* @param {Object} obj
* @param {String} obj.ciphertext - ciphertext to decrypt
* @param {String} obj.nonce - (base64) nonce
* @param {String} obj.publicKey - (base64) public key of the sender
* @param {String} obj.privateKey - (base64) private key of the receiver (current user)
* @returns {String} plaintext - (utf8) plaintext
*/
const decryptAsymmetric = ({
ciphertext,
nonce,
publicKey,
privateKey
}: IDecryptAsymmetricInput): string => {
const plaintext: Uint8Array | null = nacl.box.open(
util.decodeBase64(ciphertext),
util.decodeBase64(nonce),
util.decodeBase64(publicKey),
util.decodeBase64(privateKey)
);
if (plaintext == null) throw BadRequestError({
message: 'Invalid ciphertext or keys'
});
return util.encodeUTF8(plaintext);
};
/**
* Return symmetrically encrypted [plaintext] using [key].
*
* NOTE: THIS FUNCTION SHOULD NOT BE USED FOR ALL FUTURE
* ENCRYPTION OPERATIONS UNLESS IT TOUCHES OLD FUNCTIONALITY
* THAT USES IT. USE encryptSymmetric() instead
*
* @param {Object} obj
* @param {String} obj.plaintext - (utf8) plaintext to encrypt
* @param {String} obj.key - (hex) 128-bit key
* @returns {Object} obj
* @returns {String} obj.ciphertext (base64) ciphertext
* @returns {String} obj.iv (base64) iv
* @returns {String} obj.tag (base64) tag
*/
const encryptSymmetric128BitHexKeyUTF8 = ({
plaintext,
key
}: IEncryptSymmetricInput) => {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16);
const cipher = crypto.createCipheriv(ALGORITHM_AES_256_GCM, key, iv);
let ciphertext = cipher.update(plaintext, 'utf8', 'base64');
ciphertext += cipher.final('base64');
return {
ciphertext,
iv: iv.toString('base64'),
tag: cipher.getAuthTag().toString('base64')
};
}
/**
* Return symmetrically decrypted [ciphertext] using [iv], [tag],
* and [key].
*
* NOTE: THIS FUNCTION SHOULD NOT BE USED FOR ALL FUTURE
* DECRYPTION OPERATIONS UNLESS IT TOUCHES OLD FUNCTIONALITY
* THAT USES IT. USE decryptSymmetric() instead
*
* @param {Object} obj
* @param {String} obj.ciphertext - ciphertext to decrypt
* @param {String} obj.iv - (base64) 256-bit iv
* @param {String} obj.tag - (base64) tag
* @param {String} obj.key - (hex) 128-bit key
* @returns {String} cleartext - the deciphered ciphertext
*/
const decryptSymmetric128BitHexKeyUTF8 = ({
ciphertext,
iv,
tag,
key
}: IDecryptSymmetricInput) => {
const decipher = crypto.createDecipheriv(
ALGORITHM_AES_256_GCM,
key,
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(tag, 'base64'));
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
cleartext += decipher.final('utf8');
return cleartext;
}
export {
generateKeyPair,
encryptAsymmetric,
decryptAsymmetric,
encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8
};

@ -1,87 +1,87 @@
import Folder from "../models/folder";
// import Folder from "../models/folder";
export const ROOT_FOLDER_PATH = "/"
// export const ROOT_FOLDER_PATH = "/"
export const getFolderPath = async (folderId: string) => {
let currentFolder = await Folder.findById(folderId);
const pathSegments = [];
// export const getFolderPath = async (folderId: string) => {
// let currentFolder = await Folder.findById(folderId);
// const pathSegments = [];
while (currentFolder) {
pathSegments.unshift(currentFolder.name);
currentFolder = currentFolder.parent ? await Folder.findById(currentFolder.parent) : null;
}
// while (currentFolder) {
// pathSegments.unshift(currentFolder.name);
// currentFolder = currentFolder.parent ? await Folder.findById(currentFolder.parent) : null;
// }
return '/' + pathSegments.join('/');
};
// return '/' + pathSegments.join('/');
// };
/**
Returns the folder ID associated with the specified secret path in the given workspace and environment.
@param workspaceId - The ID of the workspace to search in.
@param environment - The environment to search in.
@param secretPath - The secret path to search for.
@returns The folder ID associated with the specified secret path, or undefined if the path is at the root folder level.
@throws Error if the specified secret path is not found.
*/
export const getFolderIdFromPath = async (workspaceId: string, environment: string, secretPath: string) => {
const secretPathParts = secretPath.split("/").filter(path => path != "")
if (secretPathParts.length <= 1) {
return undefined // root folder, so no folder id
}
// /**
// Returns the folder ID associated with the specified secret path in the given workspace and environment.
// @param workspaceId - The ID of the workspace to search in.
// @param environment - The environment to search in.
// @param secretPath - The secret path to search for.
// @returns The folder ID associated with the specified secret path, or undefined if the path is at the root folder level.
// @throws Error if the specified secret path is not found.
// */
// export const getFolderIdFromPath = async (workspaceId: string, environment: string, secretPath: string) => {
// const secretPathParts = secretPath.split("/").filter(path => path != "")
// if (secretPathParts.length <= 1) {
// return undefined // root folder, so no folder id
// }
const folderId = await Folder.find({ path: secretPath, workspace: workspaceId, environment: environment })
if (!folderId) {
throw Error("Secret path not found")
}
// const folderId = await Folder.find({ path: secretPath, workspace: workspaceId, environment: environment })
// if (!folderId) {
// throw Error("Secret path not found")
// }
return folderId
}
// return folderId
// }
/**
* Cleans up a path by removing empty parts, duplicate slashes,
* and ensuring it starts with ROOT_FOLDER_PATH.
* @param path - The input path to clean up.
* @returns The cleaned-up path string.
*/
export const normalizePath = (path: string) => {
if (path == undefined || path == "" || path == ROOT_FOLDER_PATH) {
return ROOT_FOLDER_PATH
}
// /**
// * Cleans up a path by removing empty parts, duplicate slashes,
// * and ensuring it starts with ROOT_FOLDER_PATH.
// * @param path - The input path to clean up.
// * @returns The cleaned-up path string.
// */
// export const normalizePath = (path: string) => {
// if (path == undefined || path == "" || path == ROOT_FOLDER_PATH) {
// return ROOT_FOLDER_PATH
// }
const pathParts = path.split("/").filter(part => part != "")
const cleanPathString = ROOT_FOLDER_PATH + pathParts.join("/")
// const pathParts = path.split("/").filter(part => part != "")
// const cleanPathString = ROOT_FOLDER_PATH + pathParts.join("/")
return cleanPathString
}
// return cleanPathString
// }
export const getFoldersInDirectory = async (workspaceId: string, environment: string, pathString: string) => {
const normalizedPath = normalizePath(pathString)
const foldersInDirectory = await Folder.find({
workspace: workspaceId,
environment: environment,
parentPath: normalizedPath,
});
// export const getFoldersInDirectory = async (workspaceId: string, environment: string, pathString: string) => {
// const normalizedPath = normalizePath(pathString)
// const foldersInDirectory = await Folder.find({
// workspace: workspaceId,
// environment: environment,
// parentPath: normalizedPath,
// });
return foldersInDirectory;
}
// return foldersInDirectory;
// }
/**
* Returns the parent path of the given path.
* @param path - The input path.
* @returns The parent path string.
*/
export const getParentPath = (path: string) => {
const normalizedPath = normalizePath(path);
const folderParts = normalizedPath.split('/').filter(part => part !== '');
// /**
// * Returns the parent path of the given path.
// * @param path - The input path.
// * @returns The parent path string.
// */
// export const getParentPath = (path: string) => {
// const normalizedPath = normalizePath(path);
// const folderParts = normalizedPath.split('/').filter(part => part !== '');
let folderParent = ROOT_FOLDER_PATH;
if (folderParts.length > 1) {
folderParent = ROOT_FOLDER_PATH + folderParts.slice(0, folderParts.length - 1).join('/');
}
// let folderParent = ROOT_FOLDER_PATH;
// if (folderParts.length > 1) {
// folderParent = ROOT_FOLDER_PATH + folderParts.slice(0, folderParts.length - 1).join('/');
// }
return folderParent;
}
// return folderParent;
// }
export const validateFolderName = (folderName: string) => {
const validNameRegex = /^[a-zA-Z0-9-_]+$/;
return validNameRegex.test(folderName);
}
// export const validateFolderName = (folderName: string) => {
// const validNameRegex = /^[a-zA-Z0-9-_]+$/;
// return validNameRegex.test(folderName);
// }

@ -0,0 +1,406 @@
import crypto from "crypto";
import { Types } from "mongoose";
import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto";
import { EESecretService } from "../../ee/services";
import { ISecretVersion, SecretSnapshot, SecretVersion } from "../../ee/models";
import {
Secret,
ISecret,
SecretBlindIndexData,
Workspace,
Bot,
BackupPrivateKey,
IntegrationAuth,
} from "../../models";
import { generateKeyPair } from "../../utils/crypto";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64,
} from "../../variables";
import { InternalServerError } from "../errors";
/**
* Backfill secrets to ensure that they're all versioned and have
* corresponding secret versions
*/
export const backfillSecretVersions = async () => {
await Secret.updateMany(
{ version: { $exists: false } },
{ $set: { version: 1 } }
);
const unversionedSecrets: ISecret[] = await Secret.aggregate([
{
$lookup: {
from: "secretversions",
localField: "_id",
foreignField: "secret",
as: "versions",
},
},
{
$match: {
versions: { $size: 0 },
},
},
]);
if (unversionedSecrets.length > 0) {
await EESecretService.addSecretVersions({
secretVersions: unversionedSecrets.map(
(s, idx) =>
new SecretVersion({
...s,
secret: s._id,
version: s.version ? s.version : 1,
isDeleted: false,
workspace: s.workspace,
environment: s.environment,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
})
),
});
}
console.log("Migration: Secret version migration v1 complete")
};
/**
* Backfill workspace bots to ensure that every workspace has a bot
*/
export const backfillBots = async () => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const workspaceIdsWithBot = await Bot.distinct("workspace");
const workspaceIdsToAddBot = await Workspace.distinct("_id", {
_id: {
$nin: workspaceIdsWithBot,
},
});
if (workspaceIdsToAddBot.length === 0) return;
const botsToInsert = await Promise.all(
workspaceIdsToAddBot.map(async (workspaceToAddBot) => {
const { publicKey, privateKey } = generateKeyPair();
if (rootEncryptionKey) {
const {
ciphertext: encryptedPrivateKey,
iv,
tag,
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
return new Bot({
name: "Infisical Bot",
workspace: workspaceToAddBot,
isActive: false,
publicKey,
encryptedPrivateKey,
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64,
});
} else if (encryptionKey) {
const {
ciphertext: encryptedPrivateKey,
iv,
tag,
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey,
key: encryptionKey,
});
return new Bot({
name: "Infisical Bot",
workspace: workspaceToAddBot,
isActive: false,
publicKey,
encryptedPrivateKey,
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
});
}
throw InternalServerError({
message:
"Failed to backfill workspace bots due to missing encryption key",
});
})
);
await Bot.insertMany(botsToInsert);
};
/**
* Backfill secret blind index data to ensure that every workspace
* has a secret blind index data
*/
export const backfillSecretBlindIndexData = async () => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct(
"workspace"
);
const workspaceIdsToBlindIndex = await Workspace.distinct("_id", {
_id: {
$nin: workspaceIdsBlindIndexed,
},
});
if (workspaceIdsToBlindIndex.length === 0) return;
const secretBlindIndexDataToInsert = await Promise.all(
workspaceIdsToBlindIndex.map(async (workspaceToBlindIndex) => {
const salt = crypto.randomBytes(16).toString("base64");
if (rootEncryptionKey) {
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag,
} = client.encryptSymmetric(salt, rootEncryptionKey);
return new SecretBlindIndexData({
workspace: workspaceToBlindIndex,
encryptedSaltCiphertext,
saltIV,
saltTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64,
});
} else if (encryptionKey) {
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag,
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: salt,
key: encryptionKey,
});
return new SecretBlindIndexData({
workspace: workspaceToBlindIndex,
encryptedSaltCiphertext,
saltIV,
saltTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
});
}
throw InternalServerError({
message:
"Failed to backfill secret blind index data due to missing encryption key",
});
})
);
SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert);
};
/**
* Backfill Secret, SecretVersion, SecretBlindIndexData, Bot,
* BackupPrivateKey, IntegrationAuth collections to ensure that
* they all have encryption metadata documented
*/
export const backfillEncryptionMetadata = async () => {
// backfill secret encryption metadata
await Secret.updateMany(
{
algorithm: {
$exists: false,
},
keyEncoding: {
$exists: false,
},
},
{
$set: {
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
},
}
);
// backfill secret version encryption metadata
await SecretVersion.updateMany(
{
algorithm: {
$exists: false,
},
keyEncoding: {
$exists: false,
},
},
{
$set: {
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
},
}
);
// backfill secret blind index encryption metadata
await SecretBlindIndexData.updateMany(
{
algorithm: {
$exists: false,
},
keyEncoding: {
$exists: false,
},
},
{
$set: {
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
},
}
);
// backfill bot encryption metadata
await Bot.updateMany(
{
algorithm: {
$exists: false,
},
keyEncoding: {
$exists: false,
},
},
{
$set: {
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
},
}
);
// backfill backup private key encryption metadata
await BackupPrivateKey.updateMany(
{
algorithm: {
$exists: false,
},
keyEncoding: {
$exists: false,
},
},
{
$set: {
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
},
}
);
// backfill integration auth encryption metadata
await IntegrationAuth.updateMany(
{
algorithm: {
$exists: false,
},
keyEncoding: {
$exists: false,
},
},
{
$set: {
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
},
}
);
};
export const backfillSecretFolders = async () => {
await Secret.updateMany(
{
folder: {
$exists: false,
},
},
{
$set: {
folder: "root",
},
}
);
await SecretVersion.updateMany(
{
folder: {
$exists: false,
},
},
{
$set: {
folder: "root",
},
}
);
// Back fill because tags were missing in secret versions
await SecretVersion.updateMany(
{
tags: {
$exists: false,
},
},
{
$set: {
tags: [],
},
}
);
let secretSnapshots = await SecretSnapshot.find({
environment: {
$exists: false,
},
})
.populate<{ secretVersions: ISecretVersion[] }>("secretVersions")
.limit(50);
while (secretSnapshots.length > 0) {
for (const secSnapshot of secretSnapshots) {
const groupSnapByEnv: Record<string, Array<ISecretVersion>> = {};
secSnapshot.secretVersions.forEach((secVer) => {
if (!groupSnapByEnv?.[secVer.environment])
groupSnapByEnv[secVer.environment] = [];
groupSnapByEnv[secVer.environment].push(secVer);
});
const newSnapshots = Object.keys(groupSnapByEnv).map((snapEnv) => {
const secretIdsOfEnvGroup = groupSnapByEnv[snapEnv] ? groupSnapByEnv[snapEnv].map(secretVersion => secretVersion._id) : []
return {
...secSnapshot.toObject({ virtuals: false }),
_id: new Types.ObjectId(),
environment: snapEnv,
secretVersions: secretIdsOfEnvGroup,
}
});
await SecretSnapshot.insertMany(newSnapshots);
await secSnapshot.delete();
}
secretSnapshots = await SecretSnapshot.find({
environment: {
$exists: false,
},
})
.populate<{ secretVersions: ISecretVersion[] }>("secretVersions")
.limit(50);
}
console.log("Migration: Folder migration v1 complete")
};

@ -0,0 +1,95 @@
import * as Sentry from '@sentry/node';
import { DatabaseService, TelemetryService } from '../../services';
import { setTransporter } from '../../helpers/nodemailer';
import { EELicenseService } from '../../ee/services';
import { initSmtp } from '../../services/smtp';
import { createTestUserForDevelopment } from '../addDevelopmentUser';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require('../patchAsyncRoutes');
import { validateEncryptionKeysConfig } from './validateConfig';
import {
backfillSecretVersions,
backfillBots,
backfillSecretBlindIndexData,
backfillEncryptionMetadata,
backfillSecretFolders,
} from './backfillData';
import {
reencryptBotPrivateKeys,
reencryptSecretBlindIndexDataSalts,
} from './reencryptData';
import {
getNodeEnv,
getMongoURL,
getSentryDSN,
getClientSecretGoogle,
getClientIdGoogle,
} from '../../config';
import { initializePassport } from '../auth';
/**
* Prepare Infisical upon startup. This includes tasks like:
* - Log initial telemetry message
* - Initializing SMTP configuration
* - Initializing the instance global feature set (if applicable)
* - Initializing the database connection
* - Initializing Sentry
* - Backfilling data
* - Re-encrypting data
*/
export const setup = async () => {
patchRouterParam();
await validateEncryptionKeysConfig();
await TelemetryService.logTelemetryMessage();
// initializing SMTP configuration
setTransporter(await initSmtp());
// initializing global feature set
await EELicenseService.initGlobalFeatureSet();
// initializing the database connection
await DatabaseService.initDatabase(await getMongoURL());
const googleClientSecret: string = await getClientSecretGoogle();
const googleClientId: string = await getClientIdGoogle();
if (googleClientId && googleClientSecret) {
await initializePassport();
}
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY
// await reencryptBotPrivateKeys();
// await reencryptSecretBlindIndexDataSalts();
// initializing the database connection
await DatabaseService.initDatabase(await getMongoURL());
/**
* NOTE: the order in this setup function is critical.
* It is important to backfill data before performing any re-encryption functionality.
*/
// backfilling data to catch up with new collections and updated fields
await backfillSecretVersions();
await backfillBots();
await backfillSecretBlindIndexData();
await backfillEncryptionMetadata();
await backfillSecretFolders();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY
await reencryptBotPrivateKeys();
await reencryptSecretBlindIndexDataSalts();
// initializing Sentry
Sentry.init({
dsn: await getSentryDSN(),
tracesSampleRate: 1.0,
debug: (await getNodeEnv()) === 'production' ? false : true,
environment: await getNodeEnv(),
});
await createTestUserForDevelopment();
};

@ -0,0 +1,124 @@
import {
Bot,
IBot,
ISecretBlindIndexData,
SecretBlindIndexData
} from '../../models';
import { decryptSymmetric128BitHexKeyUTF8 } from '../../utils/crypto';
import {
client,
getEncryptionKey,
getRootEncryptionKey
} from '../../config';
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../../variables';
/**
* Re-encrypt bot private keys from hex 128-bit ENCRYPTION_KEY
* to base64 256-bit ROOT_ENCRYPTION_KEY
*/
export const reencryptBotPrivateKeys = async () => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
if (encryptionKey && rootEncryptionKey) {
// 1: re-encrypt bot private keys under ROOT_ENCRYPTION_KEY
const bots = await Bot.find({
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}).select('+encryptedPrivateKey iv tag algorithm keyEncoding');
if (bots.length === 0) return;
const operationsBot = await Promise.all(
bots.map(async (bot: IBot) => {
const privateKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: encryptionKey
});
const {
ciphertext: encryptedPrivateKey,
iv,
tag
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
return ({
updateOne: {
filter: {
_id: bot._id
},
update: {
encryptedPrivateKey,
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64
}
}
})
})
);
await Bot.bulkWrite(operationsBot);
}
}
/**
* Re-encrypt secret blind index data salts from hex 128-bit ENCRYPTION_KEY
* to base64 256-bit ROOT_ENCRYPTION_KEY
*/
export const reencryptSecretBlindIndexDataSalts = async () => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
if (encryptionKey && rootEncryptionKey) {
const secretBlindIndexData = await SecretBlindIndexData.find({
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}).select('+encryptedSaltCiphertext +saltIV +saltTag +algorithm +keyEncoding');
if (secretBlindIndexData.length == 0) return;
const operationsSecretBlindIndexData = await Promise.all(
secretBlindIndexData.map(async (secretBlindIndexDatum: ISecretBlindIndexData) => {
const salt = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secretBlindIndexDatum.encryptedSaltCiphertext,
iv: secretBlindIndexDatum.saltIV,
tag: secretBlindIndexDatum.saltTag,
key: encryptionKey
});
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = client.encryptSymmetric(salt, rootEncryptionKey);
return ({
updateOne: {
filter: {
_id: secretBlindIndexDatum._id
},
update: {
encryptedSaltCiphertext,
saltIV,
saltTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64
}
}
})
})
);
await SecretBlindIndexData.bulkWrite(operationsSecretBlindIndexData);
}
}

@ -0,0 +1,61 @@
import {
getEncryptionKey,
getRootEncryptionKey
} from '../../config';
import {
InternalServerError
} from '../../utils/errors';
/**
* Validate ENCRYPTION_KEY and ROOT_ENCRYPTION_KEY. Specifically:
* - ENCRYPTION_KEY is a hex, 128-bit string
* - ROOT_ENCRYPTION_KEY is a base64, 128-bit string
* - Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY are present
*
* - Encrypted data is consistent with the passed in encryption keys
*
* NOTE 1: ENCRYPTION_KEY is being transitioned to ROOT_ENCRYPTION_KEY
* NOTE 2: In the future, we will have a superior validation function
* built into the SDK.
*/
export const validateEncryptionKeysConfig = async () => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
if (
(encryptionKey === undefined || encryptionKey === "") &&
(rootEncryptionKey === undefined || rootEncryptionKey === "")
) throw InternalServerError({
message: "Failed to find required root encryption key environment variable. Please make sure that you're passing in a ROOT_ENCRYPTION_KEY environment variable."
});
// if (encryptionKey && encryptionKey !== '') {
// // validate [encryptionKey]
// const keyBuffer = Buffer.from(encryptionKey, 'hex');
// const decoded = keyBuffer.toString('hex');
// if (decoded !== encryptionKey) throw InternalServerError({
// message: 'Failed to validate that the encryption key is correctly encoded in hex.'
// });
// if (keyBuffer.length !== 16) throw InternalServerError({
// message: 'Failed to validate that the encryption key is a 128-bit hex string.'
// });
// }
if (rootEncryptionKey && rootEncryptionKey !== '') {
// validate [rootEncryptionKey]
const keyBuffer = Buffer.from(rootEncryptionKey, 'base64')
const decoded = keyBuffer.toString('base64');
if (decoded !== rootEncryptionKey) throw InternalServerError({
message: 'Failed to validate that the root encryption key is correctly encoded in base64'
});
if (keyBuffer.length !== 32) throw InternalServerError({
message: 'Failed to validate that the encryption key is a 256-bit base64 string'
});
}
}

@ -0,0 +1,98 @@
import { Types } from 'mongoose';
import {
IUser,
IServiceAccount,
IServiceTokenData,
Bot,
User,
ServiceAccount,
ServiceTokenData
} from '../models';
import { validateServiceAccountClientForWorkspace } from './serviceAccount';
import { validateUserClientForWorkspace } from './user';
import {
UnauthorizedRequestError,
BotNotFoundError
} 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 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
*/
export 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",
});
};

@ -0,0 +1,10 @@
export * from './workspace';
export * from './bot';
export * from './integration';
export * from './integrationAuth';
export * from './membership';
export * from './membershipOrg';
export * from './organization';
export * from './secrets';
export * from './serviceAccount';
export * from './serviceTokenData';

@ -0,0 +1,103 @@
import { Types } from 'mongoose';
import {
IUser,
IServiceAccount,
IServiceTokenData,
Integration,
IntegrationAuth,
User,
ServiceAccount,
ServiceTokenData
} from '../models';
import { validateServiceAccountClientForWorkspace } from './serviceAccount';
import { validateUserClientForWorkspace } from './user';
import { IntegrationService } from '../services';
import {
IntegrationNotFoundError,
IntegrationAuthNotFoundError,
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 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
*/
export 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'
});
}

@ -20,8 +20,8 @@ import {
UnauthorizedRequestError
} from '../utils/errors';
import { IntegrationService } from '../services';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
import { validateUserClientForWorkspace } from './user';
import { validateServiceAccountClientForWorkspace } from './serviceAccount';
/**
* Validate authenticated clients for integration authorization with id [integrationAuthId] based

@ -0,0 +1,94 @@
import { Types } from 'mongoose';
import {
IUser,
IServiceAccount,
IServiceTokenData,
Membership,
User,
ServiceAccount,
ServiceTokenData
} from '../models';
import { validateServiceAccountClientForWorkspace } from './serviceAccount';
import { validateUserClientForWorkspace } from './user';
import { validateServiceTokenDataClientForWorkspace } from './serviceTokenData';
import {
MembershipNotFoundError,
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 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
*/
export 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'
});
}

@ -0,0 +1,93 @@
import { Types } from 'mongoose';
import {
IUser,
IServiceAccount,
IServiceTokenData,
MembershipOrg,
User,
ServiceAccount,
ServiceTokenData
} from '../models';
import {
validateMembershipOrg
} from '../helpers/membershipOrg';
import {
MembershipOrgNotFoundError,
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
*/
export 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'
});
}

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