mirror of
https://github.com/Infisical/infisical.git
synced 2025-04-13 16:52:12 +00:00
Compare commits
368 Commits
v0.4.2
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
77d47e071b | |||
4bf2407d13 | |||
aaca66e5a4 | |||
3a79a855cb | |||
e28d0cbace | |||
c0fbe82ecb | |||
b0e7304bff | |||
08868681d8 | |||
6dee858154 | |||
b9dfff1cd8 | |||
44b9533636 | |||
599c8d94c9 | |||
77788e1524 | |||
3df62a6e0a | |||
e74cc471db | |||
58d3f3945a | |||
29fa618bff | |||
668b5a9cfd | |||
6ce0f48b2c | |||
467e85b717 | |||
579516bd38 | |||
deaa85cbe7 | |||
08a4404fed | |||
73aa01c568 | |||
b926601a29 | |||
f619ee7297 | |||
bb825c3d68 | |||
6bbd7f05a2 | |||
4865b69e6d | |||
6f3c7c0fbf | |||
be80b5124f | |||
4232776637 | |||
2c12ede694 | |||
94141fedd6 | |||
720ab446f9 | |||
1a1693dbbf | |||
9440afa386 | |||
8b1ec1424d | |||
bd56f3b64c | |||
f20ea723b7 | |||
86f76ebe70 | |||
821385c2f3 | |||
03c65c8635 | |||
893c4777fe | |||
f7dbd41431 | |||
8d1f3e930a | |||
f25715b3c4 | |||
37251ed607 | |||
c078fb8bc1 | |||
2ae3c48b88 | |||
ce28151952 | |||
e6027b3c72 | |||
7f4db518cc | |||
7562e7d667 | |||
5c8f33a2d8 | |||
d4f65e23c7 | |||
50609d06f5 | |||
3a5ad93450 | |||
8493d51f5c | |||
e90f63b375 | |||
af9ffdc51f | |||
3a76a82438 | |||
8e972c704a | |||
b975115443 | |||
4a1821d537 | |||
01b87aeebf | |||
cea3b59053 | |||
a6f6711c9a | |||
3d3b416da2 | |||
bfbe2f2dcf | |||
8e5db3ee2f | |||
6b0e0f70d2 | |||
1fb9aad08a | |||
61a09d817b | |||
57b8ed4eef | |||
c3a1d03a9b | |||
11afb6db51 | |||
200d9de740 | |||
17060b22d7 | |||
c730280eff | |||
c45120e6e9 | |||
c96fbd3724 | |||
e1e2eb7c3b | |||
7812061e66 | |||
ca41c65fe0 | |||
d8c15a366d | |||
df9efa65e7 | |||
1c5616e3b6 | |||
27030138ec | |||
c37ce4eaea | |||
5aa367fe54 | |||
fac4968193 | |||
17647587f9 | |||
f3dc7fcf7b | |||
93cf7cde2d | |||
422d04d7d7 | |||
4c41d279e9 | |||
e65c6568e1 | |||
9d40a96633 | |||
859fe09ac6 | |||
d0d6419d4d | |||
8b05ce11f7 | |||
a7fb0786f9 | |||
f2de1778cb | |||
952cf47b9a | |||
1d17596af1 | |||
01385687e0 | |||
d2e3aa15b0 | |||
96607153dc | |||
a8502377c7 | |||
5aa99001cc | |||
83dd35299c | |||
b5b2f402ad | |||
ec34572087 | |||
7f7d120c2f | |||
899d46514c | |||
658df21189 | |||
51914c6a2e | |||
ad37a14f2e | |||
8341faddc5 | |||
8e3a23e6d8 | |||
bc61de4a80 | |||
1c89474159 | |||
2f765600b1 | |||
d9057216b5 | |||
6aab90590f | |||
f7466d4855 | |||
ea2565ed35 | |||
4586656b85 | |||
e4953398df | |||
7722231656 | |||
845a476974 | |||
fc19a17f4b | |||
0890b1912f | |||
82ecc2d7dc | |||
460bdbb91c | |||
446a63a917 | |||
d67cb7b507 | |||
353ff63298 | |||
4367822777 | |||
9f40266f5c | |||
ca4a9b9937 | |||
ec8d62d106 | |||
8af8a1d3d5 | |||
631423fbc8 | |||
4383779377 | |||
8249043826 | |||
6ca3b8ba61 | |||
20294ee233 | |||
4b2e91da74 | |||
fac8affe78 | |||
1ccec486cc | |||
c5a924e935 | |||
429bfd27b2 | |||
c99c873d78 | |||
092a6911ce | |||
a9b642e618 | |||
919ddf5de2 | |||
89a89af4e6 | |||
b3e68cf3fb | |||
960063e61a | |||
abf4eaf6db | |||
739f97f5c9 | |||
faed5c1821 | |||
c95598aaa6 | |||
e791684f4d | |||
6746f04f33 | |||
d32c5fb869 | |||
dba19b4a1d | |||
884aed74a5 | |||
abbf1918dc | |||
9dc7cc58a7 | |||
876d0119d3 | |||
6d70dc437e | |||
174e22a2bc | |||
6f66b56e7c | |||
be2bac41bb | |||
f4815641d8 | |||
5b95c255ec | |||
3123f6fc1f | |||
a913cd97a4 | |||
0afa44a9f0 | |||
781e0b24c8 | |||
5a99878d15 | |||
0d3e7f3c0c | |||
967e520173 | |||
ccfe0b1eb9 | |||
0ef5779776 | |||
a194e90644 | |||
addc849fa6 | |||
074c0bdd77 | |||
28de8cddd7 | |||
ed3e53f9a3 | |||
7ee33e9393 | |||
32cef27e8e | |||
1fce8cc769 | |||
4e7145dfe5 | |||
9cb4d5abb7 | |||
efdd1e64c4 | |||
5b3be6063f | |||
12c399d4a9 | |||
ecd17e1d6d | |||
fb4c811414 | |||
3561c589b1 | |||
420d71d923 | |||
3db5c040c3 | |||
b4f336a5bb | |||
43e61c94f0 | |||
69fa4a80c5 | |||
cf9e8b8a6b | |||
c6d5498a42 | |||
7aa5ef844c | |||
ad7972e7e1 | |||
c6d8f24968 | |||
d8ff0bef0d | |||
29b96246b9 | |||
8503c9355b | |||
ddf0a272f6 | |||
e3980f8666 | |||
d52534b185 | |||
4c434555a4 | |||
f011d61167 | |||
db07a033e1 | |||
87e047a152 | |||
ea86e59d4f | |||
3e19e6fd99 | |||
3c71bcaa8d | |||
476d0be101 | |||
2eff7b6128 | |||
d8a781af1f | |||
8b42f4f998 | |||
da127a3c0a | |||
d4aa75a182 | |||
d097003e9b | |||
b615a5084e | |||
379f086828 | |||
f11a7d0f87 | |||
f5aeb85c62 | |||
3d3d7c9821 | |||
2966aa6eda | |||
b1f2515731 | |||
c5094ec37d | |||
6c745f617d | |||
5eeda6272c | |||
b734b51954 | |||
82995fbd02 | |||
8d09a45454 | |||
38f578c4ae | |||
65b12eee5e | |||
9043db4727 | |||
0eceeb6aa9 | |||
2d2bbbd0ad | |||
c9b4e11539 | |||
fd4ea97e18 | |||
49d2ecc460 | |||
1172726e74 | |||
c766686670 | |||
ca31a70032 | |||
3334338eaa | |||
099cee7f39 | |||
f703ee29e5 | |||
6d5e281811 | |||
87d36ac47a | |||
b72e1198df | |||
837ea2ef40 | |||
b462ca3e89 | |||
f639f682c9 | |||
365fcb3044 | |||
01d9695153 | |||
21eb1815c4 | |||
85f3ae95b6 | |||
e888eed1bf | |||
addac63700 | |||
efd13e6b19 | |||
4ac74e6e9a | |||
1d422fa82c | |||
8ba3f8d1f7 | |||
6b83393952 | |||
da07d71e15 | |||
82d3971d9e | |||
3dd21374e7 | |||
c5fe41ae57 | |||
9f0313f50b | |||
a6e670e93a | |||
ec97e1a930 | |||
55ca6938db | |||
1401c7f6bc | |||
bb6d0fd7c6 | |||
689a20dca2 | |||
e4b4126971 | |||
04b04cba5c | |||
89e5f644a4 | |||
c5619d27d7 | |||
12a1d8e822 | |||
a85a7d1b00 | |||
fc2846534f | |||
2b605856a3 | |||
191582ef26 | |||
213b5d465b | |||
75f550caf2 | |||
daabf5ab70 | |||
7b11976a60 | |||
39be52c6b2 | |||
bced5d0151 | |||
939d7eb433 | |||
6de25174aa | |||
fd9387a25e | |||
b17a40d83e | |||
2aa79d4ad6 | |||
44b4de754a | |||
db0f0d0d9c | |||
3471e387ae | |||
aadd964409 | |||
102e45891c | |||
b9ae224aef | |||
e5cb0cbca3 | |||
330968c7af | |||
68e8e727cd | |||
3b94ee42e9 | |||
09286b4421 | |||
04a9604ba9 | |||
dfb84e9932 | |||
d86f88db92 | |||
fc53c094b7 | |||
6726ca1882 | |||
ddbe4d7040 | |||
3f6b0a9e66 | |||
c3a47597b6 | |||
a696a99232 | |||
8b1e64f75e | |||
f137087ef1 | |||
2157fab181 | |||
d2acab57e0 | |||
811929987b | |||
4ac13f61e0 | |||
3d2b0fa3fc | |||
2dd1570200 | |||
69472514af | |||
f956170820 | |||
242809ce26 | |||
492bf39243 | |||
dbfa4f5277 | |||
3fd2e22cbd | |||
150eb1f5ee | |||
6314a949f8 | |||
660c5806e3 | |||
c6d2828262 | |||
8dedfad22d | |||
7a3456ca1d | |||
a946031d6f | |||
f0075e8d09 | |||
007e8c4442 | |||
3b00df6662 | |||
a263d7481b | |||
6f91331549 | |||
13ecc22159 | |||
a5c5ec1f4d | |||
cbb28dc373 | |||
e00aad4159 | |||
fb8aaa9d9f | |||
e5c5e4cca2 | |||
803a97fdfc | |||
9e42a7a33e | |||
34c79b08bc | |||
aacdaf4556 | |||
a7484f8be5 | |||
e1bf31b371 | |||
3817831577 |
.dockerignore.env.example.goreleaser.yamlecosystem.config.js
.github
images
resources
workflows
.husky
.infisicalignoreDockerfile.standalone-infisicalREADME.mdSECURITY.mdbackend
package-lock.jsonpackage.json
src
config
controllers
v1
authController.tsintegrationAuthController.tsmembershipController.tsmembershipOrgController.tsorganizationController.tspasswordController.tssecretController.tssecretsFolderController.tsserviceTokenController.tssignupController.tsstripeController.ts
v2
apiKeyDataController.tsauthController.tssecretController.tssecretsController.tsserviceAccountsController.tsserviceTokenDataController.tssignupController.tsworkspaceController.ts
v3
ee
controllers/v1
cloudProductsController.tsindex.tsorganizationsController.tssecretController.tssecretSnapshotController.tsstripeController.tsworkspaceController.ts
helpers
models
routes/v1
services
helpers
auth.tsbot.tsdatabase.tsevent.tsintegration.tskey.tsmembership.tsmembershipOrg.tsnodemailer.tsorganization.tsrateLimiter.tssecret.tssecrets.tstoken.tsuser.tsworkspace.ts
index.tsintegrations
interfaces
middleware
requestErrorHandler.tsrequireAuth.tsrequireBotAuth.tsrequireIntegrationAuth.tsrequireIntegrationAuthorizationAuth.tsrequireMembershipAuth.tsrequireMembershipOrgAuth.tsrequireMfaAuth.tsrequireOrganizationAuth.tsrequireSecretAuth.tsrequireSecretsAuth.tsrequireServiceAccountAuth.tsrequireServiceTokenAuth.tsrequireServiceTokenDataAuth.tsrequireSignupAuth.tsrequireWorkspaceAuth.ts
models
backupPrivateKey.tsbot.tsfolder.tsindex.tsintegration.tsintegrationAuth.tsloginSRPDetail.tssecret.tssecretBlindIndexData.tsuser.ts
routes
services
templates
types
utils
validation
bot.tsindex.tsintegration.tsintegrationAuth.tsmembership.tsmembershipOrg.tsorganization.tssecrets.tsserviceAccount.tsserviceTokenData.tsuser.tsworkspace.ts
variables
test-resources
tests
cli
.infisicalignore
docker-compose.dev.ymlconfig
detect
go.modgo.summain.gopackages
api
cmd
export.goinit.gologging.gologin.go
pre-commit-script
reset.goroot.gorun.goscan.gosecrets.gouser.govault.gomodels
telemetry
util
visualize
report
constants.gocsv.gocsv_test.gofinding.gofinding_test.gojson.gojson_test.goreport.goreport_test.gosarif.gosarif_test.go
testdata
baseline
config
allow_aws_re.tomlallow_commit.tomlallow_global_aws_re.tomlallow_path.tomlbad_entropy_group.tomlbase.tomlentropy_group.tomlescaped_character_group.tomlextend_1.tomlextend_2.tomlextend_3.tomlgeneric.tomlgeneric_with_py_path.tomlpath_only.tomlsimple.toml
expected
git
report
repos
nogit
small
README.md
api
dotGit
COMMIT_EDITMSGFETCH_HEADHEADORIG_HEADconfigdescriptionindexpacked-refs
main.goinfo
logs
objects
02
15
2e
49
5c
78
90
9a
a1
a5
a9
bc
d8
da
e5
f1
pack
refs
staged
.gitleaksignoreREADME.md
api
dotGit
COMMIT_EDITMSGFETCH_HEADHEADORIG_HEADconfigdescriptionindexpacked-refs
main.goinfo
logs
objects
02
15
2e
46
49
5c
65
66
78
90
9a
a1
a5
a9
b1
bc
bf
d8
da
e5
f1
pack
refs
symlinks
tmp
docs
api-reference/overview
authentication.mdx
examples
changelog
cli
contributing
documentation
getting-started
guides
platform
getting-started
images
authentication-google-redirect.pngdashboard-secrets-overview.pngdo-k8-install-btn.pngemail-gmail-app-access.pngproject-members.pngspring-maven-debug-1.pngspring-maven-debug-2.pngspring-maven-debug-3.pngspring-maven-debug-4.pngspring-maven-debug-5.png
integrations
mint.jsonsdks
self-hosting
authentication
configuration
deployment-options
aws-ec2.mdxdigital-ocean-marketplace.mdxdocker-compose.mdxfly.io.mdxkubernetes-helm.mdxrender.mdxstandalone-infisical.mdx
overview.mdxfrontend
README.mdnext-i18next.config.jsnext.config.jspackage-lock.jsonpackage.json
public
images
locales
en
activity.jsonbilling.jsoncommon.jsondashboard.jsonintegrations.jsonlogin.jsonmfa.jsonnav.jsonsection-api-key.jsonsection-incident.jsonsection-members.jsonsection-password.jsonsection-token.jsonsettings-members.jsonsettings-org.jsonsettings-personal.jsonsettings-project.jsonsignup.jsontranslations.json
es
activity.jsonbilling.jsoncommon.jsondashboard.jsonintegrations.jsonlogin.jsonmfa.jsonnav.jsonsection-api-key.jsonsection-incident.jsonsection-members.jsonsection-password.jsonsection-token.jsonsettings-members.jsonsettings-org.jsonsettings-personal.jsonsettings-project.jsonsignup.jsontranslations.json
fr
activity.jsonbilling.jsoncommon.jsondashboard.jsonintegrations.jsonlogin.jsonmfa.jsonnav.jsonsection-incident.jsonsection-members.jsonsection-password.jsonsection-token.jsonsettings-members.jsonsettings-org.jsonsettings-personal.jsonsettings-project.jsonsignup.jsontranslations.json
ko
billing.jsoncommon.jsondashboard.jsonintegrations.jsonlogin.jsonmfa.jsonnav.jsonsection-incident.jsonsection-members.jsonsection-password.jsonsection-token.jsonsettings-members.jsonsettings-org.jsonsettings-personal.jsonsettings-project.jsonsignup.jsontranslations.json
pt-BR
activity.jsonbilling.jsoncommon.jsondashboard.jsonintegrations.jsonlogin.jsonmfa.jsonnav.jsonsection-incident.jsonsection-members.jsonsection-password.jsonsection-token.jsonsettings-members.jsonsettings-org.jsonsettings-personal.jsonsettings-project.jsonsignup.jsontranslations.json
tr
activity.jsonbilling.jsoncommon.jsondashboard.jsonintegrations.jsonlogin.jsonmfa.jsonnav.jsonsection-api-key.jsonsection-incident.jsonsection-members.jsonsection-password.jsonsection-token.jsonsettings-members.jsonsettings-org.jsonsettings-personal.jsonsettings-project.jsonsignup.jsontranslations.json
lotties
src
components
basic
EventFilter.tsxInputField.tsxLayout.tsx
dialog
ActivateBotDialog.tsxAddApiKeyDialog.tsxAddIncidentContactDialog.tsxAddProjectMemberDialog.tsxAddServiceTokenDialog.tsx
table
billing
dashboard
integrations
login
navigation
signup
CodeInputStep.tsxDonwloadBackupPDFStep.tsxEnterEmailStep.tsxInitialSignupStep.tsxTeamInviteStep.tsxUserInfoStep.tsx
utilities
v2
Button
Card
DeleteActionModal
EmptyState
IconButton
Input
Menu
Popoverv2
Select
Tooltip
UpgradePlanModal
config
const.tsee/components
hooks
i18n.tslayouts/AppLayout
pages
_app.tsxsignup.tsxsignupinvite.tsx
reactQuery.tsactivity
api/auth
dashboard
home
integrations
[id].tsx
login.tsxaws-parameter-store
aws-secret-manager
azure-key-vault
circleci
flyio
github
gitlab
heroku
netlify
railway
render
supabase
travisci
vercel
login/provider
noprojects.tsxpassword-reset.tsxsaml-sso.tsxsettings
billing
org/[id]
personal
project
users
verify-email.tsxviews
DashboardPage
DashboardEnvOverview.tsxDashboardPage.tsxDashboardPage.utils.ts
components
EnvComparisonRow
FolderSection
SecretDetailDrawer
SecretDropzone
SecretInputRow
SecretTableHeader
Settings
OrgSettingsPage
ProjectSettingsPage
ProjectSettingsPage.tsx
components
AutoCapitalizationSection
CopyProjectIDSection
ProjectIndexSecretsSection
ProjectNameChangeSection
ServiceTokenSection
helm-charts/infisical
i18n
README.de.mdREADME.en.mdREADME.es.mdREADME.id.mdREADME.it.mdREADME.ja.mdREADME.ko.mdREADME.pt-br.mdREADME.tr.md
migration
nginx
render.yaml
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
backend/node_modules
|
||||
frontend/node_modules
|
20
.env.example
20
.env.example
@ -1,5 +1,6 @@
|
||||
# Keys
|
||||
# Required key for platform encryption/decryption ops
|
||||
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NOT BE USED FOR PRODUCTION
|
||||
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||
|
||||
# JWT
|
||||
@ -8,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
|
||||
@ -15,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
|
||||
@ -30,14 +33,12 @@ MONGO_PASSWORD=example
|
||||
# Required
|
||||
SITE_URL=http://localhost:8080
|
||||
|
||||
# Mail/SMTP
|
||||
SMTP_HOST=
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_FROM_ADDRESS=
|
||||
SMTP_FROM_NAME=Infisical
|
||||
# Mail/SMTP
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=
|
||||
SMTP_NAME=
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
|
||||
# Integration
|
||||
# Optional only if integration is used
|
||||
@ -67,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
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
BIN
.github/images/do-k8-install-btn.png
vendored
Normal file
Binary file not shown.
After ![]() (image error) Size: 28 KiB |
1
.github/resources/docker-compose.be-test.yml
vendored
1
.github/resources/docker-compose.be-test.yml
vendored
@ -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
|
||||
|
||||
|
23
.github/workflows/check-be-pull-request.yml
vendored
23
.github/workflows/check-be-pull-request.yml
vendored
@ -13,6 +13,7 @@ jobs:
|
||||
check-be-pr:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
@ -26,17 +27,17 @@ jobs:
|
||||
- name: 📦 Install dependencies
|
||||
run: npm ci --only-production
|
||||
working-directory: backend
|
||||
- name: 🧪 Run tests
|
||||
run: npm run test:ci
|
||||
working-directory: backend
|
||||
- name: 📁 Upload test results
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: be-test-results
|
||||
path: |
|
||||
./backend/reports
|
||||
./backend/coverage
|
||||
# - name: 🧪 Run tests
|
||||
# run: npm run test:ci
|
||||
# working-directory: backend
|
||||
# - name: 📁 Upload test results
|
||||
# uses: actions/upload-artifact@v3
|
||||
# if: always()
|
||||
# with:
|
||||
# name: be-test-results
|
||||
# path: |
|
||||
# ./backend/reports
|
||||
# ./backend/coverage
|
||||
- name: 🏗️ Run build
|
||||
run: npm run build
|
||||
working-directory: backend
|
||||
|
29
.github/workflows/check-fe-pull-request.yml
vendored
29
.github/workflows/check-fe-pull-request.yml
vendored
@ -2,40 +2,35 @@ name: Check Frontend Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ opened, synchronize ]
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- '!frontend/README.md'
|
||||
- '!frontend/.*'
|
||||
- 'frontend/.eslintrc.js'
|
||||
|
||||
- "frontend/**"
|
||||
- "!frontend/README.md"
|
||||
- "!frontend/.*"
|
||||
- "frontend/.eslintrc.js"
|
||||
|
||||
jobs:
|
||||
|
||||
check-fe-pr:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
-
|
||||
name: ☁️ Checkout source
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: 🔧 Setup Node 16
|
||||
- name: 🔧 Setup Node 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
node-version: "16"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
-
|
||||
name: 📦 Install dependencies
|
||||
- name: 📦 Install dependencies
|
||||
run: npm ci --only-production --ignore-scripts
|
||||
working-directory: frontend
|
||||
# -
|
||||
# name: 🧪 Run tests
|
||||
# run: npm run test:ci
|
||||
# working-directory: frontend
|
||||
-
|
||||
name: 🏗️ Run build
|
||||
- name: 🏗️ Run build
|
||||
run: npm run build
|
||||
working-directory: frontend
|
||||
|
21
.github/workflows/docker-image.yml
vendored
21
.github/workflows/docker-image.yml
vendored
@ -1,12 +1,17 @@
|
||||
name: Build, Publish and Deploy to Gamma
|
||||
on: [workflow_dispatch]
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "infisical/v*.*.*"
|
||||
|
||||
jobs:
|
||||
backend-image:
|
||||
name: Build backend image
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: extract_version
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: 📦 Install dependencies to test all dependencies
|
||||
@ -51,15 +56,19 @@ jobs:
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
push: true
|
||||
context: backend
|
||||
tags: infisical/backend:${{ steps.commit.outputs.short }},
|
||||
tags: |
|
||||
infisical/backend:${{ steps.commit.outputs.short }}
|
||||
infisical/backend:latest
|
||||
infisical/backend:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
frontend-image:
|
||||
name: Build frontend image
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: extract_version
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: Save commit hashes for tag
|
||||
@ -100,8 +109,10 @@ jobs:
|
||||
push: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
context: frontend
|
||||
tags: infisical/frontend:${{ steps.commit.outputs.short }},
|
||||
tags: |
|
||||
infisical/frontend:${{ steps.commit.outputs.short }}
|
||||
infisical/frontend:latest
|
||||
infisical/frontend:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
|
68
.github/workflows/release-standalone-docker-img.yml
vendored
Normal file
68
.github/workflows/release-standalone-docker-img.yml
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
name: Release standalone docker image
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
infisical-standalone:
|
||||
name: Build infisical standalone image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 📦 Install dependencies to test all dependencies
|
||||
run: npm ci --only-production
|
||||
working-directory: backend
|
||||
- uses: paulhatch/semantic-version@v5.0.2
|
||||
id: version
|
||||
with:
|
||||
# The prefix to use to identify tags
|
||||
tag_prefix: "infisical-standalone/v"
|
||||
# A string which, if present in a git commit, indicates that a change represents a
|
||||
# major (breaking) change, supports regular expressions wrapped with '/'
|
||||
major_pattern: "(MAJOR)"
|
||||
# Same as above except indicating a minor change, supports regular expressions wrapped with '/'
|
||||
minor_pattern: "(MINOR)"
|
||||
# A string to determine the format of the version output
|
||||
version_format: "${major}.${minor}.${patch}-prerelease${increment}"
|
||||
# Optional path to check for changes. If any changes are detected in the path the
|
||||
# 'changed' output will true. Enter multiple paths separated by spaces.
|
||||
change_path: "backend,frontend"
|
||||
# Prevents pre-v1.0.0 version from automatically incrementing the major version.
|
||||
# If enabled, when the major version is 0, major releases will be treated as minor and minor as patch. Note that the version_type output is unchanged.
|
||||
enable_prerelease_mode: true
|
||||
# - name: 🧪 Run tests
|
||||
# run: npm run test:ci
|
||||
# working-directory: backend
|
||||
- name: version output
|
||||
run: |
|
||||
echo "Output Value: ${{ steps.version.outputs.major }}"
|
||||
echo "Output Value: ${{ steps.version.outputs.minor }}"
|
||||
echo "Output Value: ${{ steps.version.outputs.patch }}"
|
||||
echo "Output Value: ${{ steps.version.outputs.version }}"
|
||||
echo "Output Value: ${{ steps.version.outputs.version_type }}"
|
||||
echo "Output Value: ${{ steps.version.outputs.increment }}"
|
||||
- name: Save commit hashes for tag
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
- name: 📦 Build backend and export to Docker
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
push: true
|
||||
context: .
|
||||
tags: |
|
||||
infisical/infisical:latest
|
||||
infisical/infisical:${{ steps.commit.outputs.short }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: Dockerfile.standalone-infisical
|
6
.github/workflows/release_build.yml
vendored
6
.github/workflows/release_build.yml
vendored
@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
# run only against tags
|
||||
tags:
|
||||
- "v*"
|
||||
- "infisical-cli/v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@ -41,13 +41,15 @@ jobs:
|
||||
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
|
||||
- uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
distribution: goreleaser
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
|
||||
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
- uses: actions/setup-python@v4
|
||||
- run: pip install --upgrade cloudsmith-cli
|
||||
- name: Publish to CloudSmith
|
||||
|
@ -11,10 +11,16 @@ before:
|
||||
- ./cli/scripts/completions.sh
|
||||
- ./cli/scripts/manpages.sh
|
||||
|
||||
monorepo:
|
||||
tag_prefix: infisical-cli/
|
||||
dir: cli
|
||||
|
||||
builds:
|
||||
- id: darwin-build
|
||||
binary: infisical
|
||||
ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
|
||||
ldflags:
|
||||
- -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
|
||||
- -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
|
||||
flags:
|
||||
- -trimpath
|
||||
env:
|
||||
@ -32,7 +38,9 @@ builds:
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
binary: infisical
|
||||
ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
|
||||
ldflags:
|
||||
- -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
|
||||
- -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
|
||||
flags:
|
||||
- -trimpath
|
||||
goos:
|
||||
@ -61,10 +69,10 @@ archives:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- README*
|
||||
- LICENSE*
|
||||
- manpages/*
|
||||
- completions/*
|
||||
- ../README*
|
||||
- ../LICENSE*
|
||||
- ../manpages/*
|
||||
- ../completions/*
|
||||
|
||||
release:
|
||||
replace_existing_draft: true
|
||||
@ -74,14 +82,7 @@ checksum:
|
||||
name_template: "checksums.txt"
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-devel"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
name_template: "{{ .Version }}-devel"
|
||||
|
||||
# publishers:
|
||||
# - name: fury.io
|
||||
@ -164,7 +165,7 @@ aurs:
|
||||
mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
|
||||
mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
|
||||
install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical"
|
||||
install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/infisical"
|
||||
install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/_infisical"
|
||||
install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish"
|
||||
# man pages
|
||||
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
|
||||
|
@ -3,3 +3,5 @@
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
|
||||
infisical scan git-changes --staged -v
|
||||
|
1
.infisicalignore
Normal file
1
.infisicalignore
Normal file
@ -0,0 +1 @@
|
||||
.github/resources/docker-compose.be-test.yml:generic-api-key:16
|
102
Dockerfile.standalone-infisical
Normal file
102
Dockerfile.standalone-infisical
Normal file
@ -0,0 +1,102 @@
|
||||
ARG POSTHOG_HOST=https://app.posthog.com
|
||||
ARG POSTHOG_API_KEY=posthog-api-key
|
||||
|
||||
FROM node:16-alpine AS frontend-dependencies
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci --only-production --ignore-scripts
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:16-alpine AS frontend-builder
|
||||
WORKDIR /app
|
||||
|
||||
# Copy dependencies
|
||||
COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
||||
# Copy all files
|
||||
COPY /frontend .
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_PUBLIC_ENV production
|
||||
ARG POSTHOG_HOST
|
||||
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
|
||||
ARG POSTHOG_API_KEY
|
||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
|
||||
|
||||
# Build
|
||||
RUN npm run build
|
||||
|
||||
# Production image
|
||||
FROM node:16-alpine AS frontend-runner
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images
|
||||
VOLUME /app/.next/cache/images
|
||||
|
||||
ARG POSTHOG_API_KEY
|
||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
||||
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
|
||||
|
||||
COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts
|
||||
COPY --from=frontend-builder /app/public ./public
|
||||
RUN chown nextjs:nodejs ./public/data
|
||||
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
##
|
||||
## BACKEND
|
||||
##
|
||||
FROM node:16-alpine AS backend-build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY backend/package*.json ./
|
||||
RUN npm ci --only-production
|
||||
|
||||
COPY /backend .
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:16-alpine AS backend-runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY backend/package*.json ./
|
||||
RUN npm ci --only-production
|
||||
|
||||
COPY --from=backend-build /app .
|
||||
|
||||
# Production stage
|
||||
FROM node:16-alpine AS production
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# Install PM2
|
||||
RUN npm install -g pm2
|
||||
# Copy ecosystem.config.js
|
||||
COPY ecosystem.config.js .
|
||||
|
||||
RUN apk add --no-cache nginx
|
||||
|
||||
COPY nginx/default-stand-alone-docker.conf /etc/nginx/nginx.conf
|
||||
|
||||
COPY --from=backend-runner /app /backend
|
||||
|
||||
COPY --from=frontend-runner /app/ /app/
|
||||
|
||||
EXPOSE 80
|
||||
ENV HTTPS_ENABLED false
|
||||
|
||||
CMD ["pm2-runtime", "start", "ecosystem.config.js"]
|
||||
|
||||
|
373
README.md
373
README.md
File diff suppressed because one or more lines are too long
10
SECURITY.md
10
SECURITY.md
@ -1,9 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
## Supported versions
|
||||
|
||||
We always recommend using the latest version of Infisical to ensure you get all security updates.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
## Reporting vulnerabilities
|
||||
|
||||
Please report security vulnerabilities or concerns to team@infisical.com.
|
||||
Please do not file GitHub issues or post on our public forum for security vulnerabilities, as they are public!
|
||||
|
||||
Infisical takes security issues very seriously. If you have any concerns about Infisical or believe you have uncovered a vulnerability, please get in touch via the e-mail address security@infisical.com. In the message, try to provide a description of the issue and ideally a way of reproducing it. The security team will get back to you as soon as possible.
|
||||
|
||||
Note that this security address should be used only for undisclosed vulnerabilities. Please report any security problems to us before disclosing it publicly.
|
7521
backend/package-lock.json
generated
7521
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,20 +1,19 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-secrets-manager": "^3.303.0",
|
||||
"@godaddy/terminus": "^4.11.2",
|
||||
"@aws-sdk/client-secrets-manager": "^3.319.0",
|
||||
"@godaddy/terminus": "^4.12.0",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"@sentry/node": "^7.45.0",
|
||||
"@sentry/tracing": "^7.46.0",
|
||||
"@sentry/node": "^7.41.0",
|
||||
"@sentry/node": "^7.49.0",
|
||||
"@sentry/tracing": "^7.48.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/libsodium-wrappers": "^0.7.10",
|
||||
"argon2": "^0.30.3",
|
||||
"await-to-js": "^3.0.0",
|
||||
"aws-sdk": "^2.1338.0",
|
||||
"axios": "^1.1.3",
|
||||
"aws-sdk": "^2.1364.0",
|
||||
"axios": "^1.3.5",
|
||||
"axios-retry": "^3.4.0",
|
||||
"bcrypt": "^5.1.0",
|
||||
"bigint-conversion": "^2.2.2",
|
||||
"bigint-conversion": "^2.4.0",
|
||||
"builder-pattern": "^2.2.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
@ -25,16 +24,21 @@
|
||||
"express-validator": "^6.14.2",
|
||||
"handlebars": "^4.7.7",
|
||||
"helmet": "^5.1.1",
|
||||
"infisical-node": "^1.0.37",
|
||||
"infisical-node": "^1.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jsrp": "^0.2.4",
|
||||
"libsodium-wrappers": "^0.7.10",
|
||||
"lodash": "^4.17.21",
|
||||
"mongoose": "^6.10.4",
|
||||
"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,64 +1,93 @@
|
||||
import infisical from 'infisical-node';
|
||||
export const getPort = () => infisical.get('PORT')! || 4000;
|
||||
export const getInviteOnlySignup = () => infisical.get('INVITE_ONLY_SIGNUP')! == undefined ? false : infisical.get('INVITE_ONLY_SIGNUP');
|
||||
export const getEncryptionKey = () => infisical.get('ENCRYPTION_KEY')!;
|
||||
export const getSaltRounds = () => parseInt(infisical.get('SALT_ROUNDS')!) || 10;
|
||||
export const getJwtAuthLifetime = () => infisical.get('JWT_AUTH_LIFETIME')! || '10d';
|
||||
export const getJwtAuthSecret = () => infisical.get('JWT_AUTH_SECRET')!;
|
||||
export const getJwtMfaLifetime = () => infisical.get('JWT_MFA_LIFETIME')! || '5m';
|
||||
export const getJwtMfaSecret = () => infisical.get('JWT_MFA_LIFETIME')! || '5m';
|
||||
export const getJwtRefreshLifetime = () => infisical.get('JWT_REFRESH_LIFETIME')! || '90d';
|
||||
export const getJwtRefreshSecret = () => infisical.get('JWT_REFRESH_SECRET')!;
|
||||
export const getJwtServiceSecret = () => infisical.get('JWT_SERVICE_SECRET')!;
|
||||
export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m';
|
||||
export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!;
|
||||
export const getMongoURL = () => infisical.get('MONGO_URL')!;
|
||||
export const getNodeEnv = () => infisical.get('NODE_ENV')! || 'production';
|
||||
export const getVerboseErrorOutput = () => infisical.get('VERBOSE_ERROR_OUTPUT')! === 'true' && true;
|
||||
export const getLokiHost = () => infisical.get('LOKI_HOST')!;
|
||||
export const getClientIdAzure = () => infisical.get('CLIENT_ID_AZURE')!;
|
||||
export const getClientIdHeroku = () => infisical.get('CLIENT_ID_HEROKU')!;
|
||||
export const getClientIdVercel = () => infisical.get('CLIENT_ID_VERCEL')!;
|
||||
export const getClientIdNetlify = () => infisical.get('CLIENT_ID_NETLIFY')!;
|
||||
export const getClientIdGitHub = () => infisical.get('CLIENT_ID_GITHUB')!;
|
||||
export const getClientIdGitLab = () => infisical.get('CLIENT_ID_GITLAB')!;
|
||||
export const getClientSecretAzure = () => infisical.get('CLIENT_SECRET_AZURE')!;
|
||||
export const getClientSecretHeroku = () => infisical.get('CLIENT_SECRET_HEROKU')!;
|
||||
export const getClientSecretVercel = () => infisical.get('CLIENT_SECRET_VERCEL')!;
|
||||
export const getClientSecretNetlify = () => infisical.get('CLIENT_SECRET_NETLIFY')!;
|
||||
export const getClientSecretGitHub = () => infisical.get('CLIENT_SECRET_GITHUB')!;
|
||||
export const getClientSecretGitLab = () => infisical.get('CLIENT_SECRET_GITLAB')!;
|
||||
export const getClientSlugVercel = () => infisical.get('CLIENT_SLUG_VERCEL')!;
|
||||
export const getPostHogHost = () => infisical.get('POSTHOG_HOST')! || 'https://app.posthog.com';
|
||||
export const getPostHogProjectApiKey = () => infisical.get('POSTHOG_PROJECT_API_KEY')! || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
|
||||
export const getSentryDSN = () => infisical.get('SENTRY_DSN')!;
|
||||
export const getSiteURL = () => infisical.get('SITE_URL')!;
|
||||
export const getSmtpHost = () => infisical.get('SMTP_HOST')!;
|
||||
export const getSmtpSecure = () => infisical.get('SMTP_SECURE')! === 'true' || false;
|
||||
export const getSmtpPort = () => parseInt(infisical.get('SMTP_PORT')!) || 587;
|
||||
export const getSmtpUsername = () => infisical.get('SMTP_USERNAME')!;
|
||||
export const getSmtpPassword = () => infisical.get('SMTP_PASSWORD')!;
|
||||
export const getSmtpFromAddress = () => infisical.get('SMTP_FROM_ADDRESS')!;
|
||||
export const getSmtpFromName = () => infisical.get('SMTP_FROM_NAME')! || 'Infisical';
|
||||
export const getStripeProductStarter = () => infisical.get('STRIPE_PRODUCT_STARTER')!;
|
||||
export const getStripeProductPro = () => infisical.get('STRIPE_PRODUCT_PRO')!;
|
||||
export const getStripeProductTeam = () => infisical.get('STRIPE_PRODUCT_TEAM')!;
|
||||
export const getStripePublishableKey = () => infisical.get('STRIPE_PUBLISHABLE_KEY')!;
|
||||
export const getStripeSecretKey = () => infisical.get('STRIPE_SECRET_KEY')!;
|
||||
export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!;
|
||||
export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true;
|
||||
export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!;
|
||||
export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true
|
||||
export const getHttpsEnabled = () => {
|
||||
if (getNodeEnv() != "production") {
|
||||
import InfisicalClient from 'infisical-node';
|
||||
|
||||
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 getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10;
|
||||
export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d';
|
||||
export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue;
|
||||
export const getJwtMfaLifetime = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m';
|
||||
export const getJwtMfaSecret = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m';
|
||||
export const getJwtRefreshLifetime = async () => (await client.getSecret('JWT_REFRESH_LIFETIME')).secretValue || '90d';
|
||||
export const getJwtRefreshSecret = async () => (await client.getSecret('JWT_REFRESH_SECRET')).secretValue;
|
||||
export const getJwtServiceSecret = async () => (await client.getSecret('JWT_SERVICE_SECRET')).secretValue;
|
||||
export const getJwtSignupLifetime = async () => (await client.getSecret('JWT_SIGNUP_LIFETIME')).secretValue || '15m';
|
||||
export const getJwtProviderAuthSecret = async () => (await client.getSecret('JWT_PROVIDER_AUTH_SECRET')).secretValue;
|
||||
export const getJwtProviderAuthLifetime = async () => (await client.getSecret('JWT_PROVIDER_AUTH_LIFETIME')).secretValue || '15m';
|
||||
export const getJwtSignupSecret = async () => (await client.getSecret('JWT_SIGNUP_SECRET')).secretValue;
|
||||
export const getMongoURL = async () => (await client.getSecret('MONGO_URL')).secretValue;
|
||||
export const getNodeEnv = async () => (await client.getSecret('NODE_ENV')).secretValue || 'production';
|
||||
export const getVerboseErrorOutput = async () => (await client.getSecret('VERBOSE_ERROR_OUTPUT')).secretValue === 'true' && true;
|
||||
export const getLokiHost = async () => (await client.getSecret('LOKI_HOST')).secretValue;
|
||||
export const getClientIdAzure = async () => (await client.getSecret('CLIENT_ID_AZURE')).secretValue;
|
||||
export const getClientIdHeroku = async () => (await client.getSecret('CLIENT_ID_HEROKU')).secretValue;
|
||||
export const getClientIdVercel = async () => (await client.getSecret('CLIENT_ID_VERCEL')).secretValue;
|
||||
export const getClientIdNetlify = async () => (await client.getSecret('CLIENT_ID_NETLIFY')).secretValue;
|
||||
export const getClientIdGitHub = async () => (await client.getSecret('CLIENT_ID_GITHUB')).secretValue;
|
||||
export const getClientIdGitLab = async () => (await client.getSecret('CLIENT_ID_GITLAB')).secretValue;
|
||||
export const 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';
|
||||
export const getSentryDSN = async () => (await client.getSecret('SENTRY_DSN')).secretValue;
|
||||
export const getSiteURL = async () => (await client.getSecret('SITE_URL')).secretValue;
|
||||
export const getSmtpHost = async () => (await client.getSecret('SMTP_HOST')).secretValue;
|
||||
export const getSmtpSecure = async () => (await client.getSecret('SMTP_SECURE')).secretValue === 'true' || false;
|
||||
export const getSmtpPort = async () => parseInt((await client.getSecret('SMTP_PORT')).secretValue) || 587;
|
||||
export const getSmtpUsername = async () => (await client.getSecret('SMTP_USERNAME')).secretValue;
|
||||
export const getSmtpPassword = async () => (await client.getSecret('SMTP_PASSWORD')).secretValue;
|
||||
export const getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue;
|
||||
export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical';
|
||||
|
||||
export const 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
|
||||
export const getStripeProductStarter = async () => (await client.getSecret('STRIPE_PRODUCT_STARTER')).secretValue;
|
||||
export const getStripeProductPro = async () => (await client.getSecret('STRIPE_PRODUCT_PRO')).secretValue;
|
||||
export const getStripeProductTeam = async () => (await client.getSecret('STRIPE_PRODUCT_TEAM')).secretValue;
|
||||
export const getStripePublishableKey = async () => (await client.getSecret('STRIPE_PUBLISHABLE_KEY')).secretValue;
|
||||
export const getStripeSecretKey = async () => (await client.getSecret('STRIPE_SECRET_KEY')).secretValue;
|
||||
export const getStripeWebhookSecret = async () => (await client.getSecret('STRIPE_WEBHOOK_SECRET')).secretValue;
|
||||
|
||||
export const getTelemetryEnabled = async () => (await client.getSecret('TELEMETRY_ENABLED')).secretValue !== 'false' && true;
|
||||
export const getLoopsApiKey = async () => (await client.getSecret('LOOPS_API_KEY')).secretValue;
|
||||
export const getSmtpConfigured = async () => (await client.getSecret('SMTP_HOST')).secretValue == '' || (await client.getSecret('SMTP_HOST')).secretValue == undefined ? false : true
|
||||
export const getHttpsEnabled = async () => {
|
||||
if ((await getNodeEnv()) != "production") {
|
||||
// no https for anything other than prod
|
||||
return false
|
||||
}
|
||||
|
||||
if (infisical.get('HTTPS_ENABLED') == undefined || infisical.get('HTTPS_ENABLED') == "") {
|
||||
if ((await client.getSecret('HTTPS_ENABLED')).secretValue == undefined || (await client.getSecret('HTTPS_ENABLED')).secretValue == "") {
|
||||
// default when no value present
|
||||
return true
|
||||
}
|
||||
|
||||
return infisical.get('HTTPS_ENABLED') === 'true' && true
|
||||
return (await client.getSecret('HTTPS_ENABLED')).secretValue === 'true' && true
|
||||
}
|
@ -1,10 +1,24 @@
|
||||
import axios from 'axios';
|
||||
import axiosRetry from 'axios-retry';
|
||||
import {
|
||||
getLicenseServerKeyAuthToken,
|
||||
setLicenseServerKeyAuthToken,
|
||||
getLicenseKeyAuthToken,
|
||||
setLicenseKeyAuthToken
|
||||
} from './storage';
|
||||
import {
|
||||
getLicenseKey,
|
||||
getLicenseServerKey,
|
||||
getLicenseServerUrl
|
||||
} from './index';
|
||||
|
||||
const axiosInstance = axios.create();
|
||||
// should have JWT to interact with the license server
|
||||
export const licenseServerKeyRequest = axios.create();
|
||||
export const licenseKeyRequest = axios.create();
|
||||
export const standardRequest = axios.create();
|
||||
|
||||
// add retry functionality to the axios instance
|
||||
axiosRetry(axiosInstance, {
|
||||
axiosRetry(standardRequest, {
|
||||
retries: 3,
|
||||
retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries
|
||||
retryCondition: (error) => {
|
||||
@ -13,4 +27,98 @@ axiosRetry(axiosInstance, {
|
||||
},
|
||||
});
|
||||
|
||||
export default axiosInstance;
|
||||
export const refreshLicenseServerKeyToken = async () => {
|
||||
const licenseServerKey = await getLicenseServerKey();
|
||||
const licenseServerUrl = await getLicenseServerUrl();
|
||||
|
||||
const { data: { token } } = await standardRequest.post(
|
||||
`${licenseServerUrl}/api/auth/v1/license-server-login`, {},
|
||||
{
|
||||
headers: {
|
||||
'X-API-KEY': licenseServerKey
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
setLicenseServerKeyAuthToken(token);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
export const refreshLicenseKeyToken = async () => {
|
||||
const licenseKey = await getLicenseKey();
|
||||
const licenseServerUrl = await getLicenseServerUrl();
|
||||
|
||||
const { data: { token } } = await standardRequest.post(
|
||||
`${licenseServerUrl}/api/auth/v1/license-login`, {},
|
||||
{
|
||||
headers: {
|
||||
'X-API-KEY': licenseKey
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
setLicenseKeyAuthToken(token);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
licenseServerKeyRequest.interceptors.request.use((config) => {
|
||||
const token = getLicenseServerKeyAuthToken();
|
||||
|
||||
if (token && config.headers) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
}, (err) => {
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
||||
licenseServerKeyRequest.interceptors.response.use((response) => {
|
||||
return response
|
||||
}, async function (err) {
|
||||
const originalRequest = err.config;
|
||||
|
||||
if (err.response.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
// refresh
|
||||
const token = await refreshLicenseServerKeyToken();
|
||||
|
||||
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
|
||||
return licenseServerKeyRequest(originalRequest);
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
||||
licenseKeyRequest.interceptors.request.use((config) => {
|
||||
const token = getLicenseKeyAuthToken();
|
||||
|
||||
if (token && config.headers) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
}, (err) => {
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
||||
licenseKeyRequest.interceptors.response.use((response) => {
|
||||
return response
|
||||
}, async function (err) {
|
||||
const originalRequest = err.config;
|
||||
|
||||
if (err.response.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
// refresh
|
||||
const token = await refreshLicenseKeyToken();
|
||||
|
||||
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
|
||||
return licenseKeyRequest(originalRequest);
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
});
|
30
backend/src/config/storage.ts
Normal file
30
backend/src/config/storage.ts
Normal file
@ -0,0 +1,30 @@
|
||||
const MemoryLicenseServerKeyTokenStorage = () => {
|
||||
let authToken: string;
|
||||
|
||||
return {
|
||||
setToken: (token: string) => {
|
||||
authToken = token;
|
||||
},
|
||||
getToken: () => authToken
|
||||
};
|
||||
};
|
||||
|
||||
const MemoryLicenseKeyTokenStorage = () => {
|
||||
let authToken: string;
|
||||
|
||||
return {
|
||||
setToken: (token: string) => {
|
||||
authToken = token;
|
||||
},
|
||||
getToken: () => authToken
|
||||
};
|
||||
};
|
||||
|
||||
const licenseServerTokenStorage = MemoryLicenseServerKeyTokenStorage();
|
||||
const licenseTokenStorage = MemoryLicenseKeyTokenStorage();
|
||||
|
||||
export const getLicenseServerKeyAuthToken = licenseServerTokenStorage.getToken;
|
||||
export const setLicenseServerKeyAuthToken = licenseServerTokenStorage.setToken;
|
||||
|
||||
export const getLicenseKeyAuthToken = licenseTokenStorage.getToken;
|
||||
export const setLicenseKeyAuthToken = licenseTokenStorage.setToken;
|
@ -126,7 +126,7 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: getHttpsEnabled()
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
|
||||
const loginAction = await EELogService.createAction({
|
||||
@ -182,7 +182,7 @@ export const logout = async (req: Request, res: Response) => {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: getHttpsEnabled() as boolean
|
||||
secure: (await getHttpsEnabled()) as boolean
|
||||
});
|
||||
|
||||
const logoutAction = await EELogService.createAction({
|
||||
@ -237,7 +237,7 @@ export const getNewToken = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(refreshToken, getJwtRefreshSecret())
|
||||
jwt.verify(refreshToken, await getJwtRefreshSecret())
|
||||
);
|
||||
|
||||
const user = await User.findOne({
|
||||
@ -252,8 +252,8 @@ export const getNewToken = async (req: Request, res: Response) => {
|
||||
payload: {
|
||||
userId: decodedToken.userId
|
||||
},
|
||||
expiresIn: getJwtAuthLifetime(),
|
||||
secret: getJwtAuthSecret()
|
||||
expiresIn: await getJwtAuthLifetime(),
|
||||
secret: await getJwtAuthSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
@ -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,
|
||||
@ -16,7 +16,7 @@ import {
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_RAILWAY_API_URL
|
||||
} from '../../variables';
|
||||
import request from '../../config/request';
|
||||
import { standardRequest } from '../../config/request';
|
||||
|
||||
/***
|
||||
* Return integration authorization with id [integrationAuthId]
|
||||
@ -44,7 +44,7 @@ export const getIntegrationAuth = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
export const getIntegrationOptions = async (req: Request, res: Response) => {
|
||||
const INTEGRATION_OPTIONS = getIntegrationOptionsFunc();
|
||||
const INTEGRATION_OPTIONS = await getIntegrationOptionsFunc();
|
||||
|
||||
return res.status(200).send({
|
||||
integrationOptions: INTEGRATION_OPTIONS,
|
||||
@ -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
|
||||
@ -229,7 +231,7 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon
|
||||
let branches: string[] = [];
|
||||
|
||||
if (appId && appId !== '') {
|
||||
const { data }: { data: VercelBranch[] } = await request.get(
|
||||
const { data }: { data: VercelBranch[] } = await standardRequest.get(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v1/integrations/git-branches`,
|
||||
{
|
||||
params,
|
||||
@ -292,7 +294,7 @@ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: R
|
||||
projectId: appId
|
||||
}
|
||||
|
||||
const { data: { data: { environments: { edges } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
|
||||
const { data: { data: { environments: { edges } } } } = await standardRequest.post(INTEGRATION_RAILWAY_API_URL, {
|
||||
query,
|
||||
variables,
|
||||
}, {
|
||||
@ -372,7 +374,7 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo
|
||||
id: appId
|
||||
}
|
||||
|
||||
const { data: { data: { project: { services: { edges } } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
|
||||
const { data: { data: { project: { services: { edges } } } } } = await standardRequest.post(INTEGRATION_RAILWAY_API_URL, {
|
||||
query,
|
||||
variables
|
||||
}, {
|
||||
|
@ -215,7 +215,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
|
||||
inviterFirstName: req.user.firstName,
|
||||
inviterEmail: req.user.email,
|
||||
workspaceName: req.membership.workspace.name,
|
||||
callback_url: getSiteURL() + '/login'
|
||||
callback_url: (await getSiteURL()) + '/login'
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Types } from 'mongoose';
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { MembershipOrg, Organization, User } from '../../models';
|
||||
@ -134,12 +135,13 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
if (!inviteeMembershipOrg) {
|
||||
|
||||
await new MembershipOrg({
|
||||
user: invitee,
|
||||
inviteEmail: inviteeEmail,
|
||||
organization: organizationId,
|
||||
role: MEMBER,
|
||||
status: invitee?.publicKey ? ACCEPTED : INVITED
|
||||
status: INVITED
|
||||
}).save();
|
||||
}
|
||||
} else {
|
||||
@ -164,6 +166,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||
const organization = await Organization.findOne({ _id: organizationId });
|
||||
|
||||
if (organization) {
|
||||
|
||||
const token = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||
email: inviteeEmail,
|
||||
@ -179,13 +182,14 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||
inviterEmail: req.user.email,
|
||||
organizationName: organization.name,
|
||||
email: inviteeEmail,
|
||||
organizationId: organization._id.toString(),
|
||||
token,
|
||||
callback_url: getSiteURL() + '/signupinvite'
|
||||
callback_url: (await getSiteURL()) + '/signupinvite'
|
||||
}
|
||||
});
|
||||
|
||||
if (!getSmtpConfigured()) {
|
||||
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}`
|
||||
if (!(await getSmtpConfigured())) {
|
||||
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,13 +218,18 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||
export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
let user, token;
|
||||
try {
|
||||
const { email, code } = req.body;
|
||||
const {
|
||||
email,
|
||||
organizationId,
|
||||
code
|
||||
} = req.body;
|
||||
|
||||
user = await User.findOne({ email }).select('+publicKey');
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: email,
|
||||
status: INVITED
|
||||
status: INVITED,
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
if (!membershipOrg)
|
||||
@ -238,6 +247,10 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
// membership can be approved and redirected to login/dashboard
|
||||
membershipOrg.status = ACCEPTED;
|
||||
await membershipOrg.save();
|
||||
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully verified email',
|
||||
@ -257,8 +270,8 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: getJwtSignupLifetime(),
|
||||
secret: getJwtSignupSecret()
|
||||
expiresIn: await getJwtSignupLifetime(),
|
||||
secret: await getJwtSignupSecret()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
|
@ -19,7 +19,8 @@ export const getOrganizations = async (req: Request, res: Response) => {
|
||||
try {
|
||||
organizations = (
|
||||
await MembershipOrg.find({
|
||||
user: req.user._id
|
||||
user: req.user._id,
|
||||
status: ACCEPTED
|
||||
}).populate('organization')
|
||||
).map((m) => m.organization);
|
||||
} catch (err) {
|
||||
@ -85,7 +86,7 @@ export const createOrganization = async (req: Request, res: Response) => {
|
||||
export const getOrganization = async (req: Request, res: Response) => {
|
||||
let organization;
|
||||
try {
|
||||
organization = req.membershipOrg.organization;
|
||||
organization = req.organization
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
@ -317,29 +318,29 @@ export const createOrganizationPortalSession = async (
|
||||
) => {
|
||||
let session;
|
||||
try {
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
|
||||
// check if there is a payment method on file
|
||||
const paymentMethods = await stripe.paymentMethods.list({
|
||||
customer: req.membershipOrg.organization.customerId,
|
||||
customer: req.organization.customerId,
|
||||
type: 'card'
|
||||
});
|
||||
|
||||
|
||||
if (paymentMethods.data.length < 1) {
|
||||
// case: no payment method on file
|
||||
session = await stripe.checkout.sessions.create({
|
||||
customer: req.membershipOrg.organization.customerId,
|
||||
customer: req.organization.customerId,
|
||||
mode: 'setup',
|
||||
payment_method_types: ['card'],
|
||||
success_url: getSiteURL() + '/dashboard',
|
||||
cancel_url: getSiteURL() + '/dashboard'
|
||||
success_url: (await getSiteURL()) + '/dashboard',
|
||||
cancel_url: (await getSiteURL()) + '/dashboard'
|
||||
});
|
||||
} else {
|
||||
session = await stripe.billingPortal.sessions.create({
|
||||
customer: req.membershipOrg.organization.customerId,
|
||||
return_url: getSiteURL() + '/dashboard'
|
||||
customer: req.organization.customerId,
|
||||
return_url: (await getSiteURL()) + '/dashboard'
|
||||
});
|
||||
}
|
||||
|
||||
@ -365,12 +366,12 @@ export const getOrganizationSubscriptions = async (
|
||||
) => {
|
||||
let subscriptions;
|
||||
try {
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
|
||||
subscriptions = await stripe.subscriptions.list({
|
||||
customer: req.membershipOrg.organization.customerId
|
||||
customer: req.organization.customerId
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
|
@ -44,7 +44,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
|
||||
substitutions: {
|
||||
email,
|
||||
token,
|
||||
callback_url: getSiteURL() + '/password-reset'
|
||||
callback_url: (await getSiteURL()) + '/password-reset'
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
@ -91,8 +91,8 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: getJwtSignupLifetime(),
|
||||
secret: getJwtSignupSecret()
|
||||
expiresIn: await getJwtSignupLifetime(),
|
||||
secret: await getJwtSignupSecret()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
|
@ -39,7 +39,7 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
|
||||
try {
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
let { secrets }: { secrets: PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
@ -114,7 +114,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
let key;
|
||||
try {
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
@ -183,7 +183,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
let key;
|
||||
try {
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
|
218
backend/src/controllers/v1/secretsFolderController.ts
Normal file
218
backend/src/controllers/v1/secretsFolderController.ts
Normal file
@ -0,0 +1,218 @@
|
||||
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;
|
||||
if (!validateFolderName(folderName)) {
|
||||
throw BadRequestError({
|
||||
message: "Folder name cannot contain spaces. Only underscore and dashes",
|
||||
});
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
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,
|
||||
});
|
||||
|
||||
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" });
|
||||
}
|
||||
|
||||
// check that user is a member of the workspace
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId,
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
});
|
||||
|
||||
const parentFolder = getParentFromFolderId(folders.nodes, folderId);
|
||||
if (!parentFolder) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
const folder = parentFolder.children.find(({ id }) => id === folderId);
|
||||
if (!folder) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
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 { workspaceId, environment } = req.body;
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
// check that user is a member of the workspace
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId,
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
});
|
||||
|
||||
const delOp = deleteFolderById(folders.nodes, folderId);
|
||||
if (!delOp) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
const { deletedNode: delFolder, parent: parentFolder } = delOp;
|
||||
|
||||
parentFolder.version += 1;
|
||||
const delFolderIds = getAllFolderIds(delFolder);
|
||||
|
||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||
const folderVersion = new FolderVersion({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: parentFolder,
|
||||
});
|
||||
await folderVersion.save();
|
||||
if (delFolderIds.length) {
|
||||
await Secret.deleteMany({
|
||||
folder: { $in: delFolderIds.map(({ id }) => id) },
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
});
|
||||
}
|
||||
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId: parentFolder.id,
|
||||
});
|
||||
|
||||
res.send({ message: "successfully deleted folders", folders: delFolderIds });
|
||||
};
|
||||
|
||||
// TODO: validate workspace
|
||||
export const getFolders = async (req: Request, res: Response) => {
|
||||
const { workspaceId, environment, parentFolderId } = req.query as {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
parentFolderId?: string;
|
||||
};
|
||||
|
||||
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,
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
@ -61,7 +61,7 @@ export const createServiceToken = async (req: Request, res: Response) => {
|
||||
workspaceId
|
||||
},
|
||||
expiresIn: expiresIn,
|
||||
secret: getJwtServiceSecret()
|
||||
secret: await getJwtServiceSecret()
|
||||
});
|
||||
} catch (err) {
|
||||
return res.status(400).send({
|
||||
|
@ -21,14 +21,6 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
|
||||
try {
|
||||
email = req.body.email;
|
||||
|
||||
if (getInviteOnlySignup()) {
|
||||
// Only one user can create an account without being invited. The rest need to be invited in order to make an account
|
||||
const userCount = await User.countDocuments({})
|
||||
if (userCount != 0) {
|
||||
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
|
||||
}
|
||||
}
|
||||
|
||||
const user = await User.findOne({ email }).select('+publicKey');
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
@ -47,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}`
|
||||
});
|
||||
@ -74,8 +66,16 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (await getInviteOnlySignup()) {
|
||||
// Only one user can create an account without being invited. The rest need to be invited in order to make an account
|
||||
const userCount = await User.countDocuments({})
|
||||
if (userCount != 0) {
|
||||
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
|
||||
}
|
||||
}
|
||||
|
||||
// verify email
|
||||
if (getSmtpConfigured()) {
|
||||
if (await getSmtpConfigured()) {
|
||||
await checkEmailVerification({
|
||||
email,
|
||||
code
|
||||
@ -93,8 +93,8 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: getJwtSignupLifetime(),
|
||||
secret: getJwtSignupSecret()
|
||||
expiresIn: await getJwtSignupLifetime(),
|
||||
secret: await getJwtSignupSecret()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
|
@ -13,7 +13,7 @@ export const handleWebhook = async (req: Request, res: Response) => {
|
||||
let event;
|
||||
try {
|
||||
// check request for valid stripe signature
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
|
||||
@ -21,7 +21,7 @@ export const handleWebhook = async (req: Request, res: Response) => {
|
||||
event = stripe.webhooks.constructEvent(
|
||||
req.body,
|
||||
sig,
|
||||
getStripeWebhookSecret()
|
||||
await getStripeWebhookSecret()
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
|
@ -43,7 +43,7 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
|
||||
const { name, expiresIn } = req.body;
|
||||
|
||||
const secret = crypto.randomBytes(16).toString('hex');
|
||||
const secretHash = await bcrypt.hash(secret, getSaltRounds());
|
||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
|
@ -124,8 +124,8 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: getJwtMfaLifetime(),
|
||||
secret: getJwtMfaSecret()
|
||||
expiresIn: await getJwtMfaLifetime(),
|
||||
secret: await getJwtMfaSecret()
|
||||
});
|
||||
|
||||
const code = await TokenService.createToken({
|
||||
@ -163,7 +163,7 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: getHttpsEnabled()
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
|
||||
// case: user does not have MFA enablgged
|
||||
@ -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,
|
||||
@ -302,7 +304,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: getHttpsEnabled()
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
|
||||
interface VerifyMfaTokenRes {
|
||||
|
@ -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';
|
||||
@ -17,7 +17,7 @@ import { AccountNotFoundError } from '../../utils/errors';
|
||||
* @param res
|
||||
*/
|
||||
export const createSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const secretToCreate: CreateSecretRequestBody = req.body.secret;
|
||||
const { workspaceId, environment } = req.params
|
||||
const sanitizedSecret: SanitizedSecretForCreate = {
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -70,7 +72,7 @@ export const createSecret = async (req: Request, res: Response) => {
|
||||
* @param res
|
||||
*/
|
||||
export const createSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
|
||||
const { workspaceId, environment } = req.params
|
||||
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
|
||||
@ -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)
|
||||
@ -132,7 +136,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const { workspaceId, environmentName } = req.params
|
||||
const secretIdsToDelete: string[] = req.body.secretIds
|
||||
|
||||
@ -186,7 +190,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
await Secret.findByIdAndDelete(req._secret._id)
|
||||
|
||||
if (postHogClient) {
|
||||
@ -215,7 +219,7 @@ export const deleteSecret = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const updateSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const { workspaceId, environmentName } = req.params
|
||||
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
|
||||
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
|
||||
@ -283,7 +287,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const updateSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const { workspaceId, environmentName } = req.params
|
||||
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
|
||||
|
||||
@ -337,7 +341,7 @@ export const updateSecret = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const getSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const { environment } = req.query;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -72,7 +72,7 @@ export const createServiceAccount = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
const secret = crypto.randomBytes(16).toString('base64');
|
||||
const secretHash = await bcrypt.hash(secret, getSaltRounds());
|
||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||
|
||||
// create service account
|
||||
const serviceAccount = await new ServiceAccount({
|
||||
|
@ -84,7 +84,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
} = req.body;
|
||||
|
||||
const secret = crypto.randomBytes(16).toString('hex');
|
||||
const secretHash = await bcrypt.hash(secret, getSaltRounds());
|
||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||
|
||||
let expiresAt;
|
||||
if (expiresIn) {
|
||||
|
@ -7,8 +7,9 @@ import {
|
||||
} from '../../helpers/signup';
|
||||
import { issueAuthTokens } from '../../helpers/auth';
|
||||
import { INVITED, ACCEPTED } from '../../variables';
|
||||
import request from '../../config/request';
|
||||
import { standardRequest } from '../../config/request';
|
||||
import { getLoopsApiKey, getHttpsEnabled } from '../../config';
|
||||
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
||||
|
||||
/**
|
||||
* Complete setting up user by adding their personal and auth information as part of the
|
||||
@ -87,6 +88,19 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
user
|
||||
});
|
||||
|
||||
// update organization membership statuses that are
|
||||
// invited to completed with user attached
|
||||
const membershipsToUpdate = await MembershipOrg.find({
|
||||
inviteEmail: email,
|
||||
status: INVITED
|
||||
});
|
||||
|
||||
membershipsToUpdate.forEach(async (membership) => {
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId: membership.organization.toString()
|
||||
});
|
||||
});
|
||||
|
||||
// update organization membership statuses that are
|
||||
// invited to completed with user attached
|
||||
await MembershipOrg.updateMany(
|
||||
@ -108,8 +122,8 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
token = tokens.token;
|
||||
|
||||
// sending a welcome email to new users
|
||||
if (getLoopsApiKey()) {
|
||||
await request.post("https://app.loops.so/api/v1/events/send", {
|
||||
if (await getLoopsApiKey()) {
|
||||
await standardRequest.post("https://app.loops.so/api/v1/events/send", {
|
||||
"email": email,
|
||||
"eventName": "Sign Up",
|
||||
"firstName": firstName,
|
||||
@ -117,7 +131,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
}, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Authorization": "Bearer " + getLoopsApiKey()
|
||||
"Authorization": "Bearer " + (await getLoopsApiKey())
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -127,7 +141,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: getHttpsEnabled()
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
@ -206,9 +220,20 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
|
||||
|
||||
if (!user)
|
||||
throw new Error('Failed to complete account for non-existent user');
|
||||
|
||||
|
||||
// update organization membership statuses that are
|
||||
// invited to completed with user attached
|
||||
const membershipsToUpdate = await MembershipOrg.find({
|
||||
inviteEmail: email,
|
||||
status: INVITED
|
||||
});
|
||||
|
||||
membershipsToUpdate.forEach(async (membership) => {
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId: membership.organization.toString()
|
||||
});
|
||||
});
|
||||
|
||||
await MembershipOrg.updateMany(
|
||||
{
|
||||
inviteEmail: email,
|
||||
@ -232,7 +257,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: getHttpsEnabled()
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
|
@ -48,7 +48,7 @@ interface V2PushSecret {
|
||||
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
try {
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
@ -123,7 +123,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
export const pullSecrets = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
try {
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
|
260
backend/src/controllers/v3/authController.ts
Normal file
260
backend/src/controllers/v3/authController.ts
Normal file
@ -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,
|
||||
}
|
||||
|
192
backend/src/controllers/v3/signupController.ts
Normal file
192
backend/src/controllers/v3/signupController.ts
Normal file
@ -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
|
||||
});
|
||||
};
|
34
backend/src/ee/controllers/v1/cloudProductsController.ts
Normal file
34
backend/src/ee/controllers/v1/cloudProductsController.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response } from 'express';
|
||||
import { EELicenseService } from '../../services';
|
||||
import { getLicenseServerUrl } from '../../../config';
|
||||
import { licenseServerKeyRequest } from '../../../config/request';
|
||||
|
||||
/**
|
||||
* Return available cloud product information.
|
||||
* Note: Nicely formatted to easily construct a table from
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getCloudProducts = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const billingCycle = req.query['billing-cycle'] as string;
|
||||
|
||||
if (EELicenseService.instanceType === 'cloud') {
|
||||
const { data } = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
head: [],
|
||||
rows: []
|
||||
});
|
||||
}
|
@ -1,15 +1,19 @@
|
||||
import * as stripeController from './stripeController';
|
||||
import * as secretController from './secretController';
|
||||
import * as secretSnapshotController from './secretSnapshotController';
|
||||
import * as organizationsController from './organizationsController';
|
||||
import * as workspaceController from './workspaceController';
|
||||
import * as actionController from './actionController';
|
||||
import * as membershipController from './membershipController';
|
||||
import * as cloudProductsController from './cloudProductsController';
|
||||
|
||||
export {
|
||||
stripeController,
|
||||
secretController,
|
||||
secretSnapshotController,
|
||||
organizationsController,
|
||||
workspaceController,
|
||||
actionController,
|
||||
membershipController
|
||||
membershipController,
|
||||
cloudProductsController
|
||||
}
|
83
backend/src/ee/controllers/v1/organizationsController.ts
Normal file
83
backend/src/ee/controllers/v1/organizationsController.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { getLicenseServerUrl } from '../../../config';
|
||||
import { licenseServerKeyRequest } from '../../../config/request';
|
||||
import { EELicenseService } from '../../services';
|
||||
|
||||
/**
|
||||
* Return the organization's current plan and allowed feature set
|
||||
*/
|
||||
export const getOrganizationPlan = async (req: Request, res: Response) => {
|
||||
const { organizationId } = req.params;
|
||||
|
||||
const plan = await EELicenseService.getOrganizationPlan(organizationId);
|
||||
|
||||
return res.status(200).send({
|
||||
plan,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the organization plan to product with id [productId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateOrganizationPlan = async (req: Request, res: Response) => {
|
||||
const {
|
||||
productId
|
||||
} = req.body;
|
||||
|
||||
const { data } = await licenseServerKeyRequest.patch(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan`,
|
||||
{
|
||||
productId
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the organization's payment methods on file
|
||||
*/
|
||||
export const getOrganizationPmtMethods = async (req: Request, res: Response) => {
|
||||
const { data: { pmtMethods } } = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
pmtMethods
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Stripe session URL to add payment method for organization
|
||||
*/
|
||||
export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
|
||||
const {
|
||||
success_url,
|
||||
cancel_url
|
||||
} = req.body;
|
||||
|
||||
const { data: { url } } = await licenseServerKeyRequest.post(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`,
|
||||
{
|
||||
success_url,
|
||||
cancel_url
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
url
|
||||
});
|
||||
}
|
||||
|
||||
export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => {
|
||||
const { pmtMethodId } = req.params;
|
||||
|
||||
const { data } = await licenseServerKeyRequest.delete(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods/${pmtMethodId}`,
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
}
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
|
||||
export const handleWebhook = async (req: Request, res: Response) => {
|
||||
let event;
|
||||
try {
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
|
||||
@ -21,7 +21,7 @@ export const handleWebhook = async (req: Request, res: Response) => {
|
||||
event = stripe.webhooks.constructEvent(
|
||||
req.body,
|
||||
sig,
|
||||
getStripeWebhookSecret()
|
||||
await getStripeWebhookSecret()
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
|
@ -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,4 +1,3 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { Action } from '../models';
|
||||
import {
|
||||
@ -36,33 +35,25 @@ const createActionUpdateSecret = async ({
|
||||
workspaceId: Types.ObjectId;
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
let action;
|
||||
try {
|
||||
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
|
||||
secretIds,
|
||||
n: 2
|
||||
}))
|
||||
.map((s) => ({
|
||||
oldSecretVersion: s.versions[0]._id,
|
||||
newSecretVersion: s.versions[1]._id
|
||||
}));
|
||||
|
||||
action = await new Action({
|
||||
name,
|
||||
user: userId,
|
||||
serviceAccount: serviceAccountId,
|
||||
serviceTokenData: serviceTokenDataId,
|
||||
workspace: workspaceId,
|
||||
payload: {
|
||||
secretVersions: latestSecretVersions
|
||||
}
|
||||
}).save();
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create update secret action');
|
||||
}
|
||||
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
|
||||
secretIds,
|
||||
n: 2
|
||||
}))
|
||||
.map((s) => ({
|
||||
oldSecretVersion: s.versions[0]._id,
|
||||
newSecretVersion: s.versions[1]._id
|
||||
}));
|
||||
|
||||
const action = await new Action({
|
||||
name,
|
||||
user: userId,
|
||||
serviceAccount: serviceAccountId,
|
||||
serviceTokenData: serviceTokenDataId,
|
||||
workspace: workspaceId,
|
||||
payload: {
|
||||
secretVersions: latestSecretVersions
|
||||
}
|
||||
}).save();
|
||||
|
||||
return action;
|
||||
}
|
||||
@ -90,33 +81,25 @@ const createActionSecret = async ({
|
||||
workspaceId: Types.ObjectId;
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
let action;
|
||||
try {
|
||||
// case: action is adding, deleting, or reading secrets
|
||||
// -> add new secret versions
|
||||
const latestSecretVersions = (await getLatestSecretVersionIds({
|
||||
secretIds
|
||||
}))
|
||||
.map((s) => ({
|
||||
newSecretVersion: s.versionId
|
||||
}));
|
||||
|
||||
action = await new Action({
|
||||
name,
|
||||
user: userId,
|
||||
serviceAccount: serviceAccountId,
|
||||
serviceTokenData: serviceTokenDataId,
|
||||
workspace: workspaceId,
|
||||
payload: {
|
||||
secretVersions: latestSecretVersions
|
||||
}
|
||||
}).save();
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create action create/read/delete secret action');
|
||||
}
|
||||
// case: action is adding, deleting, or reading secrets
|
||||
// -> add new secret versions
|
||||
const latestSecretVersions = (await getLatestSecretVersionIds({
|
||||
secretIds
|
||||
}))
|
||||
.map((s) => ({
|
||||
newSecretVersion: s.versionId
|
||||
}));
|
||||
|
||||
const action = await new Action({
|
||||
name,
|
||||
user: userId,
|
||||
serviceAccount: serviceAccountId,
|
||||
serviceTokenData: serviceTokenDataId,
|
||||
workspace: workspaceId,
|
||||
payload: {
|
||||
secretVersions: latestSecretVersions
|
||||
}
|
||||
}).save();
|
||||
|
||||
return action;
|
||||
}
|
||||
@ -140,19 +123,12 @@ const createActionClient = ({
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
}) => {
|
||||
let action;
|
||||
try {
|
||||
action = new Action({
|
||||
name,
|
||||
user: userId,
|
||||
serviceAccount: serviceAccountId,
|
||||
serviceTokenData: serviceTokenDataId
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create client action');
|
||||
}
|
||||
const action = new Action({
|
||||
name,
|
||||
user: userId,
|
||||
serviceAccount: serviceAccountId,
|
||||
serviceTokenData: serviceTokenDataId
|
||||
}).save();
|
||||
|
||||
return action;
|
||||
}
|
||||
@ -181,40 +157,34 @@ const createActionHelper = async ({
|
||||
secretIds?: Types.ObjectId[];
|
||||
}) => {
|
||||
let action;
|
||||
try {
|
||||
switch (name) {
|
||||
case ACTION_LOGIN:
|
||||
case ACTION_LOGOUT:
|
||||
action = await createActionClient({
|
||||
name,
|
||||
userId
|
||||
});
|
||||
break;
|
||||
case ACTION_ADD_SECRETS:
|
||||
case ACTION_READ_SECRETS:
|
||||
case ACTION_DELETE_SECRETS:
|
||||
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
|
||||
action = await createActionSecret({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
});
|
||||
break;
|
||||
case ACTION_UPDATE_SECRETS:
|
||||
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
|
||||
action = await createActionUpdateSecret({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create action');
|
||||
switch (name) {
|
||||
case ACTION_LOGIN:
|
||||
case ACTION_LOGOUT:
|
||||
action = await createActionClient({
|
||||
name,
|
||||
userId
|
||||
});
|
||||
break;
|
||||
case ACTION_ADD_SECRETS:
|
||||
case ACTION_READ_SECRETS:
|
||||
case ACTION_DELETE_SECRETS:
|
||||
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
|
||||
action = await createActionSecret({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
});
|
||||
break;
|
||||
case ACTION_UPDATE_SECRETS:
|
||||
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
|
||||
action = await createActionUpdateSecret({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return action;
|
||||
@ -222,4 +192,4 @@ const createActionHelper = async ({
|
||||
|
||||
export {
|
||||
createActionHelper
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,3 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Log,
|
||||
@ -32,27 +31,20 @@ const createLogHelper = async ({
|
||||
channel: string;
|
||||
ipAddress: string;
|
||||
}) => {
|
||||
let log;
|
||||
try {
|
||||
log = await new Log({
|
||||
user: userId,
|
||||
serviceAccount: serviceAccountId,
|
||||
serviceTokenData: serviceTokenDataId,
|
||||
workspace: workspaceId ?? undefined,
|
||||
actionNames: actions.map((a) => a.name),
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create log');
|
||||
}
|
||||
const log = await new Log({
|
||||
user: userId,
|
||||
serviceAccount: serviceAccountId,
|
||||
serviceTokenData: serviceTokenDataId,
|
||||
workspace: workspaceId ?? undefined,
|
||||
actionNames: actions.map((a) => a.name),
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
}).save();
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
export {
|
||||
createLogHelper
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,11 @@
|
||||
import { Types } from 'mongoose';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from "mongoose";
|
||||
import { Secret, ISecret } from "../../models";
|
||||
import {
|
||||
Secret,
|
||||
ISecret,
|
||||
} from '../../models';
|
||||
import {
|
||||
SecretSnapshot,
|
||||
SecretVersion,
|
||||
ISecretVersion
|
||||
} from '../models';
|
||||
SecretSnapshot,
|
||||
SecretVersion,
|
||||
ISecretVersion,
|
||||
FolderVersion,
|
||||
} from "../models";
|
||||
|
||||
/**
|
||||
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
|
||||
@ -19,56 +16,70 @@ import {
|
||||
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
|
||||
*/
|
||||
const takeSecretSnapshotHelper = async ({
|
||||
workspaceId
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId = "root",
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
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);
|
||||
|
||||
let secretSnapshot;
|
||||
try {
|
||||
const secretIds = (await Secret.find({
|
||||
workspace: workspaceId
|
||||
}, '_id')).map((s) => s._id);
|
||||
const latestSecretVersions = (
|
||||
await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
environment,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
secret: {
|
||||
$in: secretIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$secret",
|
||||
version: { $max: "$version" },
|
||||
versionId: { $max: "$_id" }, // secret version id
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 },
|
||||
},
|
||||
]).exec()
|
||||
).map((s) => s.versionId);
|
||||
const latestFolderVersion = await FolderVersion.findOne({
|
||||
environment,
|
||||
workspace: workspaceId,
|
||||
"nodes.id": folderId,
|
||||
}).sort({ "nodes.version": -1 });
|
||||
|
||||
const latestSecretVersions = (await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$secret',
|
||||
version: { $max: '$version' },
|
||||
versionId: { $max: '$_id' } // secret version id
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 }
|
||||
}
|
||||
])
|
||||
.exec())
|
||||
.map((s) => s.versionId);
|
||||
const latestSecretSnapshot = await SecretSnapshot.findOne({
|
||||
workspace: workspaceId,
|
||||
}).sort({ version: -1 });
|
||||
|
||||
const latestSecretSnapshot = await SecretSnapshot.findOne({
|
||||
workspace: workspaceId
|
||||
}).sort({ version: -1 });
|
||||
const secretSnapshot = await new SecretSnapshot({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
|
||||
secretVersions: latestSecretVersions,
|
||||
folderId,
|
||||
folderVersion: latestFolderVersion,
|
||||
}).save();
|
||||
|
||||
secretSnapshot = await new SecretSnapshot({
|
||||
workspace: workspaceId,
|
||||
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
|
||||
secretVersions: latestSecretVersions
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to take a secret snapshot');
|
||||
}
|
||||
|
||||
return secretSnapshot;
|
||||
}
|
||||
return secretSnapshot;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add secret versions [secretVersions] to the SecretVersion collection.
|
||||
@ -77,93 +88,35 @@ const takeSecretSnapshotHelper = async ({
|
||||
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
||||
*/
|
||||
const addSecretVersionsHelper = async ({
|
||||
secretVersions
|
||||
secretVersions,
|
||||
}: {
|
||||
secretVersions: ISecretVersion[]
|
||||
secretVersions: ISecretVersion[];
|
||||
}) => {
|
||||
let newSecretVersions;
|
||||
try {
|
||||
newSecretVersions = await SecretVersion.insertMany(secretVersions);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error(`Failed to add secret versions [err=${err}]`);
|
||||
}
|
||||
const newSecretVersions = await SecretVersion.insertMany(secretVersions);
|
||||
|
||||
return newSecretVersions;
|
||||
}
|
||||
return newSecretVersions;
|
||||
};
|
||||
|
||||
const markDeletedSecretVersionsHelper = async ({
|
||||
secretIds
|
||||
secretIds,
|
||||
}: {
|
||||
secretIds: Types.ObjectId[];
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
try {
|
||||
await SecretVersion.updateMany({
|
||||
secret: { $in: secretIds }
|
||||
}, {
|
||||
isDeleted: true
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to mark secret versions as deleted');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize secret versioning by setting previously unversioned
|
||||
* secrets to version 1 and begin populating secret versions.
|
||||
*/
|
||||
const initSecretVersioningHelper = async () => {
|
||||
try {
|
||||
|
||||
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
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to ensure that secrets are versioned');
|
||||
}
|
||||
}
|
||||
await SecretVersion.updateMany(
|
||||
{
|
||||
secret: { $in: secretIds },
|
||||
},
|
||||
{
|
||||
isDeleted: true,
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
takeSecretSnapshotHelper,
|
||||
addSecretVersionsHelper,
|
||||
markDeletedSecretVersionsHelper,
|
||||
initSecretVersioningHelper
|
||||
}
|
||||
takeSecretSnapshotHelper,
|
||||
addSecretVersionsHelper,
|
||||
markDeletedSecretVersionsHelper,
|
||||
};
|
||||
|
@ -1,110 +1,82 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
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;
|
||||
}
|
||||
|
||||
let latestSecretVersionIds: LatestSecretVersionId[];
|
||||
try {
|
||||
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());
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get latest secret versions');
|
||||
}
|
||||
|
||||
return latestSecretVersionIds;
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
let latestNSecretVersions;
|
||||
try {
|
||||
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] },
|
||||
},
|
||||
}
|
||||
]));
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get latest n secret versions');
|
||||
}
|
||||
|
||||
return latestNSecretVersions;
|
||||
}
|
||||
// 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] },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
export {
|
||||
getLatestSecretVersionIds,
|
||||
getLatestNSecretSecretVersionIds
|
||||
}
|
||||
return latestNSecretVersions;
|
||||
};
|
||||
|
||||
export { getLatestSecretVersionIds, getLatestNSecretSecretVersionIds };
|
||||
|
60
backend/src/ee/models/folderVersion.ts
Normal file
60
backend/src/ee/models/folderVersion.ts
Normal file
@ -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;
|
||||
|
20
backend/src/ee/routes/v1/cloudProducts.ts
Normal file
20
backend/src/ee/routes/v1/cloudProducts.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
validateRequest
|
||||
} from '../../../middleware';
|
||||
import { query } from 'express-validator';
|
||||
import { cloudProductsController } from '../../controllers/v1';
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
query('billing-cycle').exists().isIn(['monthly', 'yearly']),
|
||||
validateRequest,
|
||||
cloudProductsController.getCloudProducts
|
||||
);
|
||||
|
||||
export default router;
|
@ -1,11 +1,15 @@
|
||||
import secret from './secret';
|
||||
import secretSnapshot from './secretSnapshot';
|
||||
import organizations from './organizations';
|
||||
import workspace from './workspace';
|
||||
import action from './action';
|
||||
import cloudProducts from './cloudProducts';
|
||||
|
||||
export {
|
||||
secret,
|
||||
secretSnapshot,
|
||||
organizations,
|
||||
workspace,
|
||||
action
|
||||
action,
|
||||
cloudProducts
|
||||
}
|
87
backend/src/ee/routes/v1/organizations.ts
Normal file
87
backend/src/ee/routes/v1/organizations.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireOrganizationAuth,
|
||||
validateRequest
|
||||
} from '../../../middleware';
|
||||
import { param, body } from 'express-validator';
|
||||
import { organizationsController } from '../../controllers/v1';
|
||||
import {
|
||||
OWNER, ADMIN, MEMBER, ACCEPTED
|
||||
} from '../../../variables';
|
||||
|
||||
router.get(
|
||||
'/:organizationId/plan',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
param('organizationId').exists().trim(),
|
||||
validateRequest,
|
||||
organizationsController.getOrganizationPlan
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:organizationId/plan',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
param('organizationId').exists().trim(),
|
||||
body('productId').exists().isString(),
|
||||
validateRequest,
|
||||
organizationsController.updateOrganizationPlan
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:organizationId/billing-details/payment-methods',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
param('organizationId').exists().trim(),
|
||||
validateRequest,
|
||||
organizationsController.getOrganizationPmtMethods
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:organizationId/billing-details/payment-methods',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
param('organizationId').exists().trim(),
|
||||
body('success_url').exists().isString(),
|
||||
body('cancel_url').exists().isString(),
|
||||
validateRequest,
|
||||
organizationsController.addOrganizationPmtMethod
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:organizationId/billing-details/payment-methods/:pmtMethodId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
param('organizationId').exists().trim(),
|
||||
validateRequest,
|
||||
organizationsController.deleteOrganizationPmtMethod
|
||||
);
|
||||
|
||||
export default router;
|
@ -7,7 +7,7 @@ import {
|
||||
requireAuth,
|
||||
validateRequest
|
||||
} from '../../../middleware';
|
||||
import { param, body } from 'express-validator';
|
||||
import { param } from 'express-validator';
|
||||
import { ADMIN, MEMBER } from '../../../variables';
|
||||
import { secretSnapshotController } from '../../controllers/v1';
|
||||
|
||||
|
@ -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;
|
||||
|
@ -1,12 +1,133 @@
|
||||
import NodeCache from 'node-cache';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
getLicenseKey,
|
||||
getLicenseServerKey,
|
||||
getLicenseServerUrl
|
||||
} from '../../config';
|
||||
import {
|
||||
licenseKeyRequest,
|
||||
licenseServerKeyRequest,
|
||||
refreshLicenseServerKeyToken,
|
||||
refreshLicenseKeyToken
|
||||
} from '../../config/request';
|
||||
import { Organization } from '../../models';
|
||||
import { OrganizationNotFoundError } from '../../utils/errors';
|
||||
|
||||
interface FeatureSet {
|
||||
_id: string | null;
|
||||
slug: 'starter' | 'team' | 'pro' | 'enterprise' | null;
|
||||
tier: number;
|
||||
workspaceLimit: number | null;
|
||||
workspacesUsed: number;
|
||||
memberLimit: number | null;
|
||||
membersUsed: number;
|
||||
secretVersioning: boolean;
|
||||
pitRecovery: boolean;
|
||||
rbac: boolean;
|
||||
customRateLimits: boolean;
|
||||
customAlerts: boolean;
|
||||
auditLogs: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle Enterprise Edition license actions
|
||||
* Class to handle license/plan configurations:
|
||||
* - Infisical Cloud: Fetch and cache customer plans in [localFeatureSet]
|
||||
* - Self-hosted regular: Use default global feature set
|
||||
* - Self-hosted enterprise: Fetch and update global feature set
|
||||
*/
|
||||
class EELicenseService {
|
||||
|
||||
private readonly _isLicenseValid: boolean;
|
||||
private readonly _isLicenseValid: boolean; // TODO: deprecate
|
||||
|
||||
public instanceType: 'self-hosted' | 'enterprise-self-hosted' | 'cloud' = 'self-hosted';
|
||||
|
||||
public globalFeatureSet: FeatureSet = {
|
||||
_id: null,
|
||||
slug: null,
|
||||
tier: -1,
|
||||
workspaceLimit: null,
|
||||
workspacesUsed: 0,
|
||||
memberLimit: null,
|
||||
membersUsed: 0,
|
||||
secretVersioning: true,
|
||||
pitRecovery: true,
|
||||
rbac: true,
|
||||
customRateLimits: true,
|
||||
customAlerts: true,
|
||||
auditLogs: false
|
||||
}
|
||||
|
||||
public localFeatureSet: NodeCache;
|
||||
|
||||
constructor(licenseKey: string) {
|
||||
constructor() {
|
||||
this._isLicenseValid = true;
|
||||
this.localFeatureSet = new NodeCache({
|
||||
stdTTL: 300
|
||||
});
|
||||
}
|
||||
|
||||
public async getOrganizationPlan(organizationId: string): Promise<FeatureSet> {
|
||||
try {
|
||||
if (this.instanceType === 'cloud') {
|
||||
const cachedPlan = this.localFeatureSet.get<FeatureSet>(organizationId);
|
||||
if (cachedPlan) {
|
||||
return cachedPlan;
|
||||
}
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) throw OrganizationNotFoundError();
|
||||
|
||||
const { data: { currentPlan } } = await licenseServerKeyRequest.get(
|
||||
`${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) {
|
||||
return this.globalFeatureSet;
|
||||
}
|
||||
|
||||
return this.globalFeatureSet;
|
||||
}
|
||||
|
||||
public async initGlobalFeatureSet() {
|
||||
const licenseServerKey = await getLicenseServerKey();
|
||||
const licenseKey = await getLicenseKey();
|
||||
|
||||
try {
|
||||
if (licenseServerKey) {
|
||||
// license server key is present -> validate it
|
||||
const token = await refreshLicenseServerKeyToken()
|
||||
|
||||
if (token) {
|
||||
this.instanceType = 'cloud';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (licenseKey) {
|
||||
// license key is present -> validate it
|
||||
const token = await refreshLicenseKeyToken();
|
||||
|
||||
if (token) {
|
||||
const { data: { currentPlan } } = await licenseKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license/v1/plan`
|
||||
);
|
||||
|
||||
this.globalFeatureSet = currentPlan;
|
||||
this.instanceType = 'enterprise-self-hosted';
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// case: self-hosted free
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
}
|
||||
|
||||
public get isLicenseValid(): boolean {
|
||||
@ -14,4 +135,4 @@ class EELicenseService {
|
||||
}
|
||||
}
|
||||
|
||||
export default new EELicenseService('N/A');
|
||||
export default new EELicenseService();
|
@ -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;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcrypt';
|
||||
@ -20,6 +19,7 @@ import {
|
||||
import {
|
||||
getJwtAuthLifetime,
|
||||
getJwtAuthSecret,
|
||||
getJwtProviderAuthSecret,
|
||||
getJwtRefreshLifetime,
|
||||
getJwtRefreshSecret
|
||||
} from '../config';
|
||||
@ -42,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'];
|
||||
|
||||
@ -104,7 +103,7 @@ const getAuthUserPayload = async ({
|
||||
authTokenValue: string;
|
||||
}) => {
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(authTokenValue, getJwtAuthSecret())
|
||||
jwt.verify(authTokenValue, await getJwtAuthSecret())
|
||||
);
|
||||
|
||||
const user = await User.findOne({
|
||||
@ -263,16 +262,16 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
||||
payload: {
|
||||
userId
|
||||
},
|
||||
expiresIn: getJwtAuthLifetime(),
|
||||
secret: getJwtAuthSecret()
|
||||
expiresIn: await getJwtAuthLifetime(),
|
||||
secret: await getJwtAuthSecret()
|
||||
});
|
||||
|
||||
const refreshToken = createToken({
|
||||
payload: {
|
||||
userId
|
||||
},
|
||||
expiresIn: getJwtRefreshLifetime(),
|
||||
secret: getJwtRefreshSecret()
|
||||
expiresIn: await getJwtRefreshLifetime(),
|
||||
secret: await getJwtRefreshSecret()
|
||||
});
|
||||
|
||||
return {
|
||||
@ -319,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,
|
||||
|
@ -1,144 +1,90 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
Bot,
|
||||
BotKey,
|
||||
Secret,
|
||||
ISecret,
|
||||
IUser,
|
||||
User,
|
||||
IServiceAccount,
|
||||
ServiceAccount,
|
||||
IServiceTokenData,
|
||||
ServiceTokenData
|
||||
} from '../models';
|
||||
import {
|
||||
generateKeyPair,
|
||||
encryptSymmetric,
|
||||
decryptSymmetric,
|
||||
decryptAsymmetric
|
||||
Bot,
|
||||
BotKey,
|
||||
Secret,
|
||||
ISecret,
|
||||
IUser
|
||||
} from "../models";
|
||||
import {
|
||||
generateKeyPair,
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
decryptSymmetric128BitHexKeyUTF8,
|
||||
decryptAsymmetric
|
||||
} from '../utils/crypto';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../variables';
|
||||
import { getEncryptionKey } from '../config';
|
||||
import { BotNotFoundError, UnauthorizedRequestError } from '../utils/errors';
|
||||
import {
|
||||
validateMembership
|
||||
} from '../helpers/membership';
|
||||
import {
|
||||
validateUserClientForWorkspace
|
||||
} from '../helpers/user';
|
||||
import {
|
||||
validateServiceAccountClientForWorkspace
|
||||
} from '../helpers/serviceAccount';
|
||||
|
||||
/**
|
||||
* Validate authenticated clients for bot with id [botId] based
|
||||
* on any known permissions.
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.authData - authenticated client details
|
||||
* @param {Types.ObjectId} obj.botId - id of bot to validate against
|
||||
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
||||
*/
|
||||
const validateClientForBot = async ({
|
||||
authData,
|
||||
botId,
|
||||
acceptedRoles
|
||||
}: {
|
||||
authData: {
|
||||
authMode: string;
|
||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||
};
|
||||
botId: Types.ObjectId;
|
||||
acceptedRoles: Array<'admin' | 'member'>;
|
||||
}) => {
|
||||
const bot = await Bot.findById(botId);
|
||||
|
||||
if (!bot) throw BotNotFoundError();
|
||||
|
||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||
await validateUserClientForWorkspace({
|
||||
user: authData.authPayload,
|
||||
workspaceId: bot.workspace,
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||
await validateServiceAccountClientForWorkspace({
|
||||
serviceAccount: authData.authPayload,
|
||||
workspaceId: bot.workspace
|
||||
});
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed service token authorization for bot'
|
||||
});
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||
await validateUserClientForWorkspace({
|
||||
user: authData.authPayload,
|
||||
workspaceId: bot.workspace,
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
throw BotNotFoundError({
|
||||
message: 'Failed client authorization for bot'
|
||||
});
|
||||
}
|
||||
SECRET_SHARED,
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
ENCODING_SCHEME_BASE64
|
||||
} from "../variables";
|
||||
import {
|
||||
getEncryptionKey,
|
||||
getRootEncryptionKey,
|
||||
client
|
||||
} from "../config";
|
||||
import { InternalServerError } from "../utils/errors";
|
||||
|
||||
/**
|
||||
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.name - name of bot
|
||||
* @param {String} obj.workspaceId - id of workspace that bot belongs to
|
||||
*/
|
||||
const createBot = async ({
|
||||
name,
|
||||
workspaceId,
|
||||
name,
|
||||
workspaceId,
|
||||
}: {
|
||||
name: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
name: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
}) => {
|
||||
let bot;
|
||||
try {
|
||||
const { publicKey, privateKey } = generateKeyPair();
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext: privateKey,
|
||||
key: getEncryptionKey()
|
||||
});
|
||||
const encryptionKey = await getEncryptionKey();
|
||||
const rootEncryptionKey = await getRootEncryptionKey();
|
||||
|
||||
const { publicKey, privateKey } = generateKeyPair();
|
||||
|
||||
if (rootEncryptionKey) {
|
||||
const {
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
||||
|
||||
bot = await new Bot({
|
||||
name,
|
||||
workspace: workspaceId,
|
||||
isActive: false,
|
||||
publicKey,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
iv,
|
||||
tag
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create bot');
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
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'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return decrypted secrets for workspace with id [workspaceId]
|
||||
@ -148,125 +94,125 @@ const createBot = async ({
|
||||
* @param {String} obj.environment - environment
|
||||
*/
|
||||
const getSecretsHelper = async ({
|
||||
workspaceId,
|
||||
environment
|
||||
workspaceId,
|
||||
environment,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
}) => {
|
||||
const content = {} as any;
|
||||
try {
|
||||
const key = await getKey({ workspaceId });
|
||||
const secrets = await Secret.find({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
type: SECRET_SHARED
|
||||
});
|
||||
|
||||
secrets.forEach((secret: ISecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
const content = {} as any;
|
||||
const key = await getKey({ workspaceId: workspaceId.toString() });
|
||||
const secrets = await Secret.find({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
type: SECRET_SHARED,
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
key
|
||||
});
|
||||
secrets.forEach((secret: ISecret) => {
|
||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key,
|
||||
});
|
||||
|
||||
content[secretKey] = secretValue;
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get secrets');
|
||||
}
|
||||
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
key,
|
||||
});
|
||||
|
||||
return content;
|
||||
}
|
||||
content[secretKey] = secretValue;
|
||||
});
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return bot's copy of the workspace key for workspace
|
||||
* Return bot's copy of the workspace key for workspace
|
||||
* with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @returns {String} key - decrypted workspace key
|
||||
*/
|
||||
const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => {
|
||||
let key;
|
||||
try {
|
||||
const botKey = await BotKey.findOne({
|
||||
workspace: workspaceId
|
||||
}).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');
|
||||
|
||||
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: getEncryptionKey()
|
||||
});
|
||||
|
||||
key = decryptAsymmetric({
|
||||
ciphertext: botKey.encryptedKey,
|
||||
nonce: botKey.nonce,
|
||||
publicKey: botKey.sender.publicKey as string,
|
||||
privateKey: privateKeyBot
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get 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");
|
||||
|
||||
if (!botKey) throw new Error("Failed to find bot key");
|
||||
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId,
|
||||
}).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");
|
||||
|
||||
if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) {
|
||||
// case: encoding scheme is base64
|
||||
const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag);
|
||||
|
||||
return decryptAsymmetric({
|
||||
ciphertext: botKey.encryptedKey,
|
||||
nonce: botKey.nonce,
|
||||
publicKey: botKey.sender.publicKey as string,
|
||||
privateKey: privateKeyBot,
|
||||
});
|
||||
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
|
||||
|
||||
return key;
|
||||
}
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
|
||||
throw InternalServerError({
|
||||
message: "Failed to obtain bot's copy of workspace key needed for bot operations"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return symmetrically encrypted [plaintext] using the
|
||||
* key for workspace with id [workspaceId]
|
||||
* key for workspace with id [workspaceId]
|
||||
* @param {Object} obj1
|
||||
* @param {String} obj1.workspaceId - id of workspace
|
||||
* @param {String} obj1.plaintext - plaintext to encrypt
|
||||
*/
|
||||
const encryptSymmetricHelper = async ({
|
||||
workspaceId,
|
||||
plaintext
|
||||
workspaceId,
|
||||
plaintext,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
plaintext: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
plaintext: string;
|
||||
}) => {
|
||||
|
||||
try {
|
||||
const key = await getKey({ workspaceId });
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext,
|
||||
key
|
||||
});
|
||||
|
||||
return ({
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to perform symmetric encryption with bot');
|
||||
}
|
||||
}
|
||||
const key = await getKey({ workspaceId: workspaceId.toString() });
|
||||
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext,
|
||||
key,
|
||||
});
|
||||
|
||||
return {
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Return symmetrically decrypted [ciphertext] using the
|
||||
* key for workspace with id [workspaceId]
|
||||
@ -277,40 +223,30 @@ const encryptSymmetricHelper = async ({
|
||||
* @param {String} obj.tag - tag
|
||||
*/
|
||||
const decryptSymmetricHelper = async ({
|
||||
workspaceId,
|
||||
workspaceId,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}) => {
|
||||
const key = await getKey({ workspaceId: workspaceId.toString() });
|
||||
const plaintext = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}) => {
|
||||
let plaintext;
|
||||
try {
|
||||
const key = await getKey({ workspaceId });
|
||||
const plaintext = decryptSymmetric({
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
key
|
||||
});
|
||||
|
||||
return plaintext;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to perform symmetric decryption with bot');
|
||||
}
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
tag,
|
||||
key,
|
||||
});
|
||||
|
||||
return plaintext;
|
||||
};
|
||||
|
||||
export {
|
||||
validateClientForBot,
|
||||
createBot,
|
||||
getSecretsHelper,
|
||||
encryptSymmetricHelper,
|
||||
decryptSymmetricHelper
|
||||
}
|
||||
createBot,
|
||||
getSecretsHelper,
|
||||
encryptSymmetricHelper,
|
||||
decryptSymmetricHelper
|
||||
};
|
||||
|
@ -1,6 +1,4 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { EESecretService } from '../ee/services';
|
||||
import { SecretService } from '../services';
|
||||
import { getLogger } from '../utils/logger';
|
||||
|
||||
/**
|
||||
@ -20,12 +18,10 @@ const initDatabaseHelper = async ({
|
||||
// allow empty strings to pass the required validator
|
||||
mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string');
|
||||
|
||||
getLogger("database").info("Database connection established");
|
||||
|
||||
await EESecretService.initSecretVersioning();
|
||||
await SecretService.initSecretBlindIndexDataHelper();
|
||||
(await getLogger("database")).info("Database connection established");
|
||||
|
||||
} catch (err) {
|
||||
getLogger("database").error(`Unable to establish Database connection due to the error.\n${err}`);
|
||||
(await getLogger("database")).error(`Unable to establish Database connection due to the error.\n${err}`);
|
||||
}
|
||||
|
||||
return mongoose.connection;
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { Types } from 'mongoose';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Bot, IBot } from '../models';
|
||||
import { EVENT_PUSH_SECRETS } from '../variables';
|
||||
import { IntegrationService } from '../services';
|
||||
import { Types } from "mongoose";
|
||||
import { Bot, IBot } from "../models";
|
||||
import { EVENT_PUSH_SECRETS } from "../variables";
|
||||
import { IntegrationService } from "../services";
|
||||
|
||||
interface Event {
|
||||
name: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
payload: any;
|
||||
name: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -19,39 +18,25 @@ interface Event {
|
||||
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
||||
* @param {Object} obj.event.payload - payload of event (depends on event)
|
||||
*/
|
||||
const handleEventHelper = async ({
|
||||
event
|
||||
}: {
|
||||
event: Event;
|
||||
}) => {
|
||||
const {
|
||||
workspaceId,
|
||||
environment
|
||||
} = event;
|
||||
|
||||
// TODO: moduralize bot check into separate function
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!bot) return;
|
||||
|
||||
try {
|
||||
switch (event.name) {
|
||||
case EVENT_PUSH_SECRETS:
|
||||
IntegrationService.syncIntegrations({
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
}
|
||||
const handleEventHelper = async ({ event }: { event: Event }) => {
|
||||
const { workspaceId, environment } = event;
|
||||
|
||||
export {
|
||||
handleEventHelper
|
||||
}
|
||||
// TODO: moduralize bot check into separate function
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId,
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
if (!bot) return;
|
||||
|
||||
switch (event.name) {
|
||||
case EVENT_PUSH_SECRETS:
|
||||
IntegrationService.syncIntegrations({
|
||||
workspaceId,
|
||||
environment,
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
export { handleEventHelper };
|
||||
|
@ -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]
|
||||
@ -256,7 +158,7 @@ const syncIntegrationsHelper = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessId: access.accessId,
|
||||
accessId: access.accessId === undefined ? null : access.accessId,
|
||||
accessToken: access.accessToken
|
||||
});
|
||||
}
|
||||
@ -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,11 +381,10 @@ const setIntegrationAuthAccessHelper = async ({
|
||||
}
|
||||
|
||||
export {
|
||||
validateClientForIntegration,
|
||||
handleOAuthExchangeHelper,
|
||||
syncIntegrationsHelper,
|
||||
getIntegrationAuthRefreshHelper,
|
||||
getIntegrationAuthAccessHelper,
|
||||
setIntegrationAuthRefreshHelper,
|
||||
setIntegrationAuthAccessHelper
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Key, IKey } from '../models';
|
||||
|
||||
interface Key {
|
||||
@ -27,36 +26,30 @@ const pushKeys = async ({
|
||||
workspaceId: string;
|
||||
keys: Key[];
|
||||
}): Promise<void> => {
|
||||
try {
|
||||
// filter out already-inserted keys
|
||||
const keysSet = new Set(
|
||||
(
|
||||
await Key.find(
|
||||
{
|
||||
workspace: workspaceId
|
||||
},
|
||||
'receiver'
|
||||
)
|
||||
).map((k: IKey) => k.receiver.toString())
|
||||
);
|
||||
// filter out already-inserted keys
|
||||
const keysSet = new Set(
|
||||
(
|
||||
await Key.find(
|
||||
{
|
||||
workspace: workspaceId
|
||||
},
|
||||
'receiver'
|
||||
)
|
||||
).map((k: IKey) => k.receiver.toString())
|
||||
);
|
||||
|
||||
keys = keys.filter((key) => !keysSet.has(key.userId));
|
||||
keys = keys.filter((key) => !keysSet.has(key.userId));
|
||||
|
||||
// add new shared keys only
|
||||
await Key.insertMany(
|
||||
keys.map((k) => ({
|
||||
encryptedKey: k.encryptedKey,
|
||||
nonce: k.nonce,
|
||||
sender: userId,
|
||||
receiver: k.userId,
|
||||
workspace: workspaceId
|
||||
}))
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to push access keys');
|
||||
}
|
||||
// add new shared keys only
|
||||
await Key.insertMany(
|
||||
keys.map((k) => ({
|
||||
encryptedKey: k.encryptedKey,
|
||||
nonce: k.nonce,
|
||||
sender: userId,
|
||||
receiver: k.userId,
|
||||
workspace: workspaceId
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
export { pushKeys };
|
||||
|
@ -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 };
|
||||
|
@ -1,98 +1,14 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
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]
|
||||
@ -144,15 +60,7 @@ const validateMembershipOrg = async ({
|
||||
* @return {Object} membershipOrg - membership
|
||||
*/
|
||||
const findMembershipOrg = (queryObj: any) => {
|
||||
let membershipOrg;
|
||||
try {
|
||||
membershipOrg = MembershipOrg.findOne(queryObj);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to find organization membership');
|
||||
}
|
||||
|
||||
const membershipOrg = MembershipOrg.findOne(queryObj);
|
||||
return membershipOrg;
|
||||
};
|
||||
|
||||
@ -175,33 +83,27 @@ const addMembershipsOrg = async ({
|
||||
roles: string[];
|
||||
statuses: string[];
|
||||
}) => {
|
||||
try {
|
||||
const operations = userIds.map((userId, idx) => {
|
||||
return {
|
||||
updateOne: {
|
||||
filter: {
|
||||
user: userId,
|
||||
organization: organizationId,
|
||||
role: roles[idx],
|
||||
status: statuses[idx]
|
||||
},
|
||||
update: {
|
||||
user: userId,
|
||||
organization: organizationId,
|
||||
role: roles[idx],
|
||||
status: statuses[idx]
|
||||
},
|
||||
upsert: true
|
||||
}
|
||||
};
|
||||
});
|
||||
const operations = userIds.map((userId, idx) => {
|
||||
return {
|
||||
updateOne: {
|
||||
filter: {
|
||||
user: userId,
|
||||
organization: organizationId,
|
||||
role: roles[idx],
|
||||
status: statuses[idx]
|
||||
},
|
||||
update: {
|
||||
user: userId,
|
||||
organization: organizationId,
|
||||
role: roles[idx],
|
||||
status: statuses[idx]
|
||||
},
|
||||
upsert: true
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
await MembershipOrg.bulkWrite(operations as any);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to add users to organization');
|
||||
}
|
||||
await MembershipOrg.bulkWrite(operations as any);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -214,49 +116,41 @@ const deleteMembershipOrg = async ({
|
||||
}: {
|
||||
membershipOrgId: string;
|
||||
}) => {
|
||||
let deletedMembershipOrg;
|
||||
try {
|
||||
deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
|
||||
_id: membershipOrgId
|
||||
});
|
||||
const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
|
||||
_id: membershipOrgId
|
||||
});
|
||||
|
||||
if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership');
|
||||
if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership');
|
||||
|
||||
// delete keys associated with organization membership
|
||||
if (deletedMembershipOrg?.user) {
|
||||
// case: organization membership had a registered user
|
||||
// delete keys associated with organization membership
|
||||
if (deletedMembershipOrg?.user) {
|
||||
// case: organization membership had a registered user
|
||||
|
||||
const workspaces = (
|
||||
await Workspace.find({
|
||||
organization: deletedMembershipOrg.organization
|
||||
})
|
||||
).map((w) => w._id.toString());
|
||||
const workspaces = (
|
||||
await Workspace.find({
|
||||
organization: deletedMembershipOrg.organization
|
||||
})
|
||||
).map((w) => w._id.toString());
|
||||
|
||||
await Membership.deleteMany({
|
||||
user: deletedMembershipOrg.user,
|
||||
workspace: {
|
||||
$in: workspaces
|
||||
}
|
||||
});
|
||||
await Membership.deleteMany({
|
||||
user: deletedMembershipOrg.user,
|
||||
workspace: {
|
||||
$in: workspaces
|
||||
}
|
||||
});
|
||||
|
||||
await Key.deleteMany({
|
||||
receiver: deletedMembershipOrg.user,
|
||||
workspace: {
|
||||
$in: workspaces
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to delete organization membership');
|
||||
}
|
||||
await Key.deleteMany({
|
||||
receiver: deletedMembershipOrg.user,
|
||||
workspace: {
|
||||
$in: workspaces
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return deletedMembershipOrg;
|
||||
};
|
||||
|
||||
export {
|
||||
validateClientForMembershipOrg,
|
||||
validateMembershipOrg,
|
||||
findMembershipOrg,
|
||||
addMembershipsOrg,
|
||||
|
@ -25,7 +25,7 @@ const sendMail = async ({
|
||||
recipients: string[];
|
||||
substitutions: any;
|
||||
}) => {
|
||||
if (getSmtpConfigured()) {
|
||||
if (await getSmtpConfigured()) {
|
||||
try {
|
||||
const html = fs.readFileSync(
|
||||
path.resolve(__dirname, '../templates/' + template),
|
||||
@ -35,7 +35,7 @@ const sendMail = async ({
|
||||
const htmlToSend = temp(substitutions);
|
||||
|
||||
await smtpTransporter.sendMail({
|
||||
from: `"${getSmtpFromName()}" <${getSmtpFromAddress()}>`,
|
||||
from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`,
|
||||
to: recipients.join(', '),
|
||||
subject: subjectLine,
|
||||
html: htmlToSend
|
||||
|
@ -1,110 +1,25 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import Stripe from 'stripe';
|
||||
import { Types } from 'mongoose';
|
||||
import Stripe from "stripe";
|
||||
import { Types } from "mongoose";
|
||||
import { Organization, MembershipOrg } from "../models";
|
||||
import {
|
||||
IUser,
|
||||
User,
|
||||
IServiceAccount,
|
||||
ServiceAccount,
|
||||
IServiceTokenData,
|
||||
ServiceTokenData
|
||||
} from '../models';
|
||||
import { Organization, MembershipOrg } from '../models';
|
||||
import {
|
||||
ACCEPTED,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY,
|
||||
OWNER
|
||||
} from '../variables';
|
||||
import {
|
||||
getStripeSecretKey,
|
||||
getStripeProductPro,
|
||||
getStripeProductTeam,
|
||||
getStripeProductStarter
|
||||
ACCEPTED
|
||||
} from "../variables";
|
||||
import {
|
||||
getStripeSecretKey,
|
||||
getStripeProductPro,
|
||||
getStripeProductTeam,
|
||||
getStripeProductStarter,
|
||||
} from "../config";
|
||||
import {
|
||||
EELicenseService
|
||||
} from '../ee/services';
|
||||
import {
|
||||
getLicenseServerUrl
|
||||
} from '../config';
|
||||
import {
|
||||
UnauthorizedRequestError,
|
||||
OrganizationNotFoundError
|
||||
} from '../utils/errors';
|
||||
import {
|
||||
validateUserClientForOrganization
|
||||
} from '../helpers/user';
|
||||
import {
|
||||
validateServiceAccountClientForOrganization
|
||||
} from '../helpers/serviceAccount';
|
||||
|
||||
/**
|
||||
* Validate accepted clients for organization with id [organizationId]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.authData - authenticated client details
|
||||
* @param {Types.ObjectId} obj.organizationId - id of organization to validate against
|
||||
*/
|
||||
const validateClientForOrganization = async ({
|
||||
authData,
|
||||
organizationId,
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
}: {
|
||||
authData: {
|
||||
authMode: string;
|
||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||
},
|
||||
organizationId: Types.ObjectId;
|
||||
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
|
||||
acceptedStatuses: Array<'invited' | 'accepted'>;
|
||||
}) => {
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: 'Failed to find organization'
|
||||
});
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||
const membershipOrg = await validateUserClientForOrganization({
|
||||
user: authData.authPayload,
|
||||
organization,
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
|
||||
return ({ organization, membershipOrg });
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||
await validateServiceAccountClientForOrganization({
|
||||
serviceAccount: authData.authPayload,
|
||||
organization
|
||||
});
|
||||
|
||||
return ({ organization });
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed service token authorization for organization'
|
||||
});
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||
const membershipOrg = await validateUserClientForOrganization({
|
||||
user: authData.authPayload,
|
||||
organization,
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
|
||||
return ({ organization, membershipOrg });
|
||||
}
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed client authorization for organization'
|
||||
});
|
||||
}
|
||||
licenseServerKeyRequest,
|
||||
licenseKeyRequest
|
||||
} from '../config/request';
|
||||
|
||||
/**
|
||||
* Create an organization with name [name]
|
||||
@ -114,43 +29,37 @@ const validateClientForOrganization = async ({
|
||||
* @param {Object} organization - new organization
|
||||
*/
|
||||
const createOrganization = async ({
|
||||
name,
|
||||
email
|
||||
name,
|
||||
email,
|
||||
}: {
|
||||
name: string;
|
||||
email: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}) => {
|
||||
let organization;
|
||||
try {
|
||||
// register stripe account
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
let organization;
|
||||
// register stripe account
|
||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||
apiVersion: "2022-08-01",
|
||||
});
|
||||
|
||||
if (getStripeSecretKey()) {
|
||||
const customer = await stripe.customers.create({
|
||||
email,
|
||||
description: name
|
||||
});
|
||||
if (await getStripeSecretKey()) {
|
||||
const customer = await stripe.customers.create({
|
||||
email,
|
||||
description: name,
|
||||
});
|
||||
|
||||
organization = await new Organization({
|
||||
name,
|
||||
customerId: customer.id
|
||||
}).save();
|
||||
} else {
|
||||
organization = await new Organization({
|
||||
name
|
||||
}).save();
|
||||
}
|
||||
organization = await new Organization({
|
||||
name,
|
||||
customerId: customer.id,
|
||||
}).save();
|
||||
} else {
|
||||
organization = await new Organization({
|
||||
name,
|
||||
}).save();
|
||||
}
|
||||
|
||||
await initSubscriptionOrg({ organizationId: organization._id });
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email });
|
||||
Sentry.captureException(err);
|
||||
throw new Error(`Failed to create organization [err=${err}]`);
|
||||
}
|
||||
await initSubscriptionOrg({ organizationId: organization._id });
|
||||
|
||||
return organization;
|
||||
return organization;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -162,57 +71,52 @@ const createOrganization = async ({
|
||||
* @return {Subscription} obj.subscription - new subscription
|
||||
*/
|
||||
const initSubscriptionOrg = async ({
|
||||
organizationId
|
||||
organizationId,
|
||||
}: {
|
||||
organizationId: Types.ObjectId;
|
||||
organizationId: Types.ObjectId;
|
||||
}) => {
|
||||
let stripeSubscription;
|
||||
let subscription;
|
||||
try {
|
||||
// find organization
|
||||
const organization = await Organization.findOne({
|
||||
_id: organizationId
|
||||
});
|
||||
let stripeSubscription;
|
||||
let subscription;
|
||||
|
||||
if (organization) {
|
||||
if (organization.customerId) {
|
||||
// initialize starter subscription with quantity of 0
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
// find organization
|
||||
const organization = await Organization.findOne({
|
||||
_id: organizationId,
|
||||
});
|
||||
|
||||
const productToPriceMap = {
|
||||
starter: getStripeProductStarter(),
|
||||
team: getStripeProductTeam(),
|
||||
pro: getStripeProductPro()
|
||||
};
|
||||
if (organization) {
|
||||
if (organization.customerId) {
|
||||
// initialize starter subscription with quantity of 0
|
||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||
apiVersion: "2022-08-01",
|
||||
});
|
||||
|
||||
stripeSubscription = await stripe.subscriptions.create({
|
||||
customer: organization.customerId,
|
||||
items: [
|
||||
{
|
||||
price: productToPriceMap['starter'],
|
||||
quantity: 1
|
||||
}
|
||||
],
|
||||
payment_behavior: 'default_incomplete',
|
||||
proration_behavior: 'none',
|
||||
expand: ['latest_invoice.payment_intent']
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error('Failed to initialize free organization subscription');
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to initialize free organization subscription');
|
||||
}
|
||||
const productToPriceMap = {
|
||||
starter: await getStripeProductStarter(),
|
||||
team: await getStripeProductTeam(),
|
||||
pro: await getStripeProductPro(),
|
||||
};
|
||||
|
||||
return {
|
||||
stripeSubscription,
|
||||
subscription
|
||||
};
|
||||
stripeSubscription = await stripe.subscriptions.create({
|
||||
customer: organization.customerId,
|
||||
items: [
|
||||
{
|
||||
price: productToPriceMap["starter"],
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
payment_behavior: "default_incomplete",
|
||||
proration_behavior: "none",
|
||||
expand: ["latest_invoice.payment_intent"],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error("Failed to initialize free organization subscription");
|
||||
}
|
||||
|
||||
return {
|
||||
stripeSubscription,
|
||||
subscription,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@ -222,54 +126,55 @@ const initSubscriptionOrg = async ({
|
||||
* @param {Number} obj.organizationId - id of subscription's organization
|
||||
*/
|
||||
const updateSubscriptionOrgQuantity = async ({
|
||||
organizationId
|
||||
organizationId,
|
||||
}: {
|
||||
organizationId: string;
|
||||
organizationId: string;
|
||||
}) => {
|
||||
let stripeSubscription;
|
||||
try {
|
||||
// find organization
|
||||
const organization = await Organization.findOne({
|
||||
_id: organizationId
|
||||
});
|
||||
let stripeSubscription;
|
||||
// find organization
|
||||
const organization = await Organization.findOne({
|
||||
_id: organizationId,
|
||||
});
|
||||
|
||||
if (organization && organization.customerId) {
|
||||
const quantity = await MembershipOrg.countDocuments({
|
||||
organization: organizationId,
|
||||
status: ACCEPTED
|
||||
});
|
||||
if (organization && organization.customerId) {
|
||||
if (EELicenseService.instanceType === 'cloud') {
|
||||
// instance of Infisical is a cloud instance
|
||||
const quantity = await MembershipOrg.countDocuments({
|
||||
organization: new Types.ObjectId(organizationId),
|
||||
status: ACCEPTED,
|
||||
});
|
||||
|
||||
await licenseServerKeyRequest.patch(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`,
|
||||
{
|
||||
quantity
|
||||
}
|
||||
);
|
||||
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
EELicenseService.localFeatureSet.del(organizationId);
|
||||
}
|
||||
|
||||
if (EELicenseService.instanceType === 'enterprise-self-hosted') {
|
||||
// instance of Infisical is an enterprise self-hosted instance
|
||||
|
||||
const usedSeats = await MembershipOrg.countDocuments({
|
||||
status: ACCEPTED
|
||||
});
|
||||
|
||||
const subscription = (
|
||||
await stripe.subscriptions.list({
|
||||
customer: organization.customerId
|
||||
})
|
||||
).data[0];
|
||||
await licenseKeyRequest.patch(
|
||||
`${await getLicenseServerUrl()}/api/license/v1/license`,
|
||||
{
|
||||
usedSeats
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
stripeSubscription = await stripe.subscriptions.update(subscription.id, {
|
||||
items: [
|
||||
{
|
||||
id: subscription.items.data[0].id,
|
||||
price: subscription.items.data[0].price.id,
|
||||
quantity
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
|
||||
return stripeSubscription;
|
||||
return stripeSubscription;
|
||||
};
|
||||
|
||||
export {
|
||||
validateClientForOrganization,
|
||||
createOrganization,
|
||||
initSubscriptionOrg,
|
||||
updateSubscriptionOrgQuantity
|
||||
};
|
||||
createOrganization,
|
||||
initSubscriptionOrg,
|
||||
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) => {
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,16 +1,15 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { TokenData } from '../models';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { Types } from "mongoose";
|
||||
import { TokenData } from "../models";
|
||||
import crypto from "crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import {
|
||||
TOKEN_EMAIL_CONFIRMATION,
|
||||
TOKEN_EMAIL_MFA,
|
||||
TOKEN_EMAIL_ORG_INVITATION,
|
||||
TOKEN_EMAIL_PASSWORD_RESET
|
||||
} from '../variables';
|
||||
import { UnauthorizedRequestError } from '../utils/errors';
|
||||
import { getSaltRounds } from '../config';
|
||||
TOKEN_EMAIL_CONFIRMATION,
|
||||
TOKEN_EMAIL_MFA,
|
||||
TOKEN_EMAIL_ORG_INVITATION,
|
||||
TOKEN_EMAIL_PASSWORD_RESET,
|
||||
} from "../variables";
|
||||
import { UnauthorizedRequestError } from "../utils/errors";
|
||||
import { getSaltRounds } from "../config";
|
||||
|
||||
/**
|
||||
* Create and store a token in the database for purpose [type]
|
||||
@ -22,194 +21,197 @@ import { getSaltRounds } from '../config';
|
||||
* @returns {String} token - the created token
|
||||
*/
|
||||
const createTokenHelper = async ({
|
||||
type,
|
||||
email,
|
||||
phoneNumber,
|
||||
organizationId
|
||||
type,
|
||||
email,
|
||||
phoneNumber,
|
||||
organizationId,
|
||||
}: {
|
||||
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
|
||||
type:
|
||||
| "emailConfirmation"
|
||||
| "emailMfa"
|
||||
| "organizationInvitation"
|
||||
| "passwordReset";
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organizationId?: Types.ObjectId;
|
||||
}) => {
|
||||
let token, expiresAt, triesLeft;
|
||||
// generate random token based on specified token use-case
|
||||
// type [type]
|
||||
switch (type) {
|
||||
case TOKEN_EMAIL_CONFIRMATION:
|
||||
// generate random 6-digit code
|
||||
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
||||
expiresAt = new Date(new Date().getTime() + 86400000);
|
||||
break;
|
||||
case TOKEN_EMAIL_MFA:
|
||||
// generate random 6-digit code
|
||||
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
||||
triesLeft = 5;
|
||||
expiresAt = new Date(new Date().getTime() + 300000);
|
||||
break;
|
||||
case TOKEN_EMAIL_ORG_INVITATION:
|
||||
// generate random hex
|
||||
token = crypto.randomBytes(16).toString("hex");
|
||||
expiresAt = new Date(new Date().getTime() + 259200000);
|
||||
break;
|
||||
case TOKEN_EMAIL_PASSWORD_RESET:
|
||||
// generate random hex
|
||||
token = crypto.randomBytes(16).toString("hex");
|
||||
expiresAt = new Date(new Date().getTime() + 86400000);
|
||||
break;
|
||||
default:
|
||||
token = crypto.randomBytes(16).toString("hex");
|
||||
expiresAt = new Date();
|
||||
break;
|
||||
}
|
||||
|
||||
interface TokenDataQuery {
|
||||
type: string;
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organizationId?: Types.ObjectId
|
||||
}) => {
|
||||
let token, expiresAt, triesLeft;
|
||||
try {
|
||||
// generate random token based on specified token use-case
|
||||
// type [type]
|
||||
switch (type) {
|
||||
case TOKEN_EMAIL_CONFIRMATION:
|
||||
// generate random 6-digit code
|
||||
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
||||
expiresAt = new Date((new Date()).getTime() + 86400000);
|
||||
break;
|
||||
case TOKEN_EMAIL_MFA:
|
||||
// generate random 6-digit code
|
||||
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
||||
triesLeft = 5;
|
||||
expiresAt = new Date((new Date()).getTime() + 300000);
|
||||
break;
|
||||
case TOKEN_EMAIL_ORG_INVITATION:
|
||||
// generate random hex
|
||||
token = crypto.randomBytes(16).toString('hex');
|
||||
expiresAt = new Date((new Date()).getTime() + 259200000);
|
||||
break;
|
||||
case TOKEN_EMAIL_PASSWORD_RESET:
|
||||
// generate random hex
|
||||
token = crypto.randomBytes(16).toString('hex');
|
||||
expiresAt = new Date((new Date()).getTime() + 86400000);
|
||||
break;
|
||||
default:
|
||||
token = crypto.randomBytes(16).toString('hex');
|
||||
expiresAt = new Date();
|
||||
break;
|
||||
}
|
||||
|
||||
interface TokenDataQuery {
|
||||
type: string;
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organization?: Types.ObjectId;
|
||||
}
|
||||
|
||||
interface TokenDataUpdate {
|
||||
type: string;
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organization?: Types.ObjectId;
|
||||
tokenHash: string;
|
||||
triesLeft?: number;
|
||||
expiresAt: Date;
|
||||
}
|
||||
organization?: Types.ObjectId;
|
||||
}
|
||||
|
||||
const query: TokenDataQuery = { type };
|
||||
const update: TokenDataUpdate = {
|
||||
type,
|
||||
tokenHash: await bcrypt.hash(token, getSaltRounds()),
|
||||
expiresAt
|
||||
}
|
||||
interface TokenDataUpdate {
|
||||
type: string;
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organization?: Types.ObjectId;
|
||||
tokenHash: string;
|
||||
triesLeft?: number;
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
if (email) {
|
||||
query.email = email;
|
||||
update.email = email;
|
||||
}
|
||||
if (phoneNumber) {
|
||||
query.phoneNumber = phoneNumber;
|
||||
update.phoneNumber = phoneNumber;
|
||||
}
|
||||
if (organizationId) {
|
||||
query.organization = organizationId
|
||||
update.organization = organizationId
|
||||
}
|
||||
|
||||
if (triesLeft) {
|
||||
update.triesLeft = triesLeft;
|
||||
}
|
||||
|
||||
await TokenData.findOneAndUpdate(
|
||||
query,
|
||||
update,
|
||||
{
|
||||
new: true,
|
||||
upsert: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error(
|
||||
"Failed to create token"
|
||||
);
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
const query: TokenDataQuery = { type };
|
||||
const update: TokenDataUpdate = {
|
||||
type,
|
||||
tokenHash: await bcrypt.hash(token, await getSaltRounds()),
|
||||
expiresAt,
|
||||
};
|
||||
|
||||
if (email) {
|
||||
query.email = email;
|
||||
update.email = email;
|
||||
}
|
||||
if (phoneNumber) {
|
||||
query.phoneNumber = phoneNumber;
|
||||
update.phoneNumber = phoneNumber;
|
||||
}
|
||||
if (organizationId) {
|
||||
query.organization = organizationId;
|
||||
update.organization = organizationId;
|
||||
}
|
||||
|
||||
if (triesLeft) {
|
||||
update.triesLeft = triesLeft;
|
||||
}
|
||||
|
||||
await TokenData.findOneAndUpdate(query, update, {
|
||||
new: true,
|
||||
upsert: true,
|
||||
});
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.email - email associated with the token
|
||||
* @param {String} obj.token - value of the token
|
||||
*/
|
||||
const validateTokenHelper = async ({
|
||||
type,
|
||||
email,
|
||||
phoneNumber,
|
||||
organizationId,
|
||||
token
|
||||
type,
|
||||
email,
|
||||
phoneNumber,
|
||||
organizationId,
|
||||
token,
|
||||
}: {
|
||||
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
|
||||
type:
|
||||
| "emailConfirmation"
|
||||
| "emailMfa"
|
||||
| "organizationInvitation"
|
||||
| "passwordReset";
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organizationId?: Types.ObjectId;
|
||||
token: string;
|
||||
}) => {
|
||||
interface Query {
|
||||
type: string;
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organizationId?: Types.ObjectId;
|
||||
token: string;
|
||||
}) => {
|
||||
interface Query {
|
||||
type: string;
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organization?: Types.ObjectId;
|
||||
}
|
||||
organization?: Types.ObjectId;
|
||||
}
|
||||
|
||||
const query: Query = { type };
|
||||
const query: Query = { type };
|
||||
|
||||
if (email) { query.email = email; }
|
||||
if (phoneNumber) { query.phoneNumber = phoneNumber; }
|
||||
if (organizationId) { query.organization = organizationId; }
|
||||
if (email) {
|
||||
query.email = email;
|
||||
}
|
||||
if (phoneNumber) {
|
||||
query.phoneNumber = phoneNumber;
|
||||
}
|
||||
if (organizationId) {
|
||||
query.organization = organizationId;
|
||||
}
|
||||
|
||||
const tokenData = await TokenData.findOne(query).select('+tokenHash');
|
||||
|
||||
if (!tokenData) throw new Error('Failed to find token to validate');
|
||||
|
||||
if (tokenData.expiresAt < new Date()) {
|
||||
// case: token expired
|
||||
await TokenData.findByIdAndDelete(tokenData._id);
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'MFA session expired. Please log in again',
|
||||
context: {
|
||||
code: 'mfa_expired'
|
||||
}
|
||||
});
|
||||
}
|
||||
const tokenData = await TokenData.findOne(query).select("+tokenHash");
|
||||
|
||||
const isValid = await bcrypt.compare(token, tokenData.tokenHash);
|
||||
if (!isValid) {
|
||||
// case: token is not valid
|
||||
if (tokenData?.triesLeft !== undefined) {
|
||||
// case: token has a try-limit
|
||||
if (tokenData.triesLeft === 1) {
|
||||
// case: token is out of tries
|
||||
await TokenData.findByIdAndDelete(tokenData._id);
|
||||
} else {
|
||||
// case: token has more than 1 try left
|
||||
await TokenData.findByIdAndUpdate(tokenData._id, {
|
||||
triesLeft: tokenData.triesLeft - 1
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
}
|
||||
if (!tokenData) throw new Error("Failed to find token to validate");
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'MFA code is invalid',
|
||||
context: {
|
||||
code: 'mfa_invalid',
|
||||
triesLeft: tokenData.triesLeft - 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'MFA code is invalid',
|
||||
context: {
|
||||
code: 'mfa_invalid'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// case: token is valid
|
||||
if (tokenData.expiresAt < new Date()) {
|
||||
// case: token expired
|
||||
await TokenData.findByIdAndDelete(tokenData._id);
|
||||
}
|
||||
throw UnauthorizedRequestError({
|
||||
message: "MFA session expired. Please log in again",
|
||||
context: {
|
||||
code: "mfa_expired",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
createTokenHelper,
|
||||
validateTokenHelper
|
||||
}
|
||||
const isValid = await bcrypt.compare(token, tokenData.tokenHash);
|
||||
if (!isValid) {
|
||||
// case: token is not valid
|
||||
if (tokenData?.triesLeft !== undefined) {
|
||||
// case: token has a try-limit
|
||||
if (tokenData.triesLeft === 1) {
|
||||
// case: token is out of tries
|
||||
await TokenData.findByIdAndDelete(tokenData._id);
|
||||
} else {
|
||||
// case: token has more than 1 try left
|
||||
await TokenData.findByIdAndUpdate(
|
||||
tokenData._id,
|
||||
{
|
||||
triesLeft: tokenData.triesLeft - 1,
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: "MFA code is invalid",
|
||||
context: {
|
||||
code: "mfa_invalid",
|
||||
triesLeft: tokenData.triesLeft - 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: "MFA code is invalid",
|
||||
context: {
|
||||
code: "mfa_invalid",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// case: token is valid
|
||||
await TokenData.findByIdAndDelete(tokenData._id);
|
||||
};
|
||||
|
||||
export { createTokenHelper, validateTokenHelper };
|
||||
|
@ -1,25 +1,8 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
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]
|
||||
@ -27,17 +10,10 @@ import {
|
||||
* @param {String} obj.email - email of user to initialize
|
||||
* @returns {Object} user - the initialized user
|
||||
*/
|
||||
const setupAccount = async ({ email }: { email: string }) => {
|
||||
let user;
|
||||
try {
|
||||
user = await new User({
|
||||
email
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email });
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to set up account');
|
||||
}
|
||||
export const setupAccount = async ({ email }: { email: string }) => {
|
||||
const user = await new User({
|
||||
email
|
||||
}).save();
|
||||
|
||||
return user;
|
||||
};
|
||||
@ -60,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,
|
||||
@ -89,34 +65,27 @@ const completeAccount = async ({
|
||||
salt: string;
|
||||
verifier: string;
|
||||
}) => {
|
||||
let user;
|
||||
try {
|
||||
const options = {
|
||||
new: true
|
||||
};
|
||||
user = await User.findByIdAndUpdate(
|
||||
userId,
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
encryptionVersion,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
options
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to complete account set up');
|
||||
}
|
||||
const options = {
|
||||
new: true
|
||||
};
|
||||
const user = await User.findByIdAndUpdate(
|
||||
userId,
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
encryptionVersion,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
return user;
|
||||
};
|
||||
@ -128,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
|
||||
@ -163,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,137 +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 { getEncryptionKey } from '../config';
|
||||
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 });
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||
await validateServiceAccountClientForWorkspace({
|
||||
serviceAccount: authData.authPayload,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: authData.authPayload,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||
const membership = await validateUserClientForWorkspace({
|
||||
user: authData.authPayload,
|
||||
workspaceId,
|
||||
environment,
|
||||
acceptedRoles,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return ({ membership });
|
||||
}
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed client authorization for workspace'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a workspace with name [name] in organization with id [organizationId]
|
||||
* and a bot for it.
|
||||
@ -204,7 +81,6 @@ const deleteWorkspace = async ({ id }: { id: string }) => {
|
||||
};
|
||||
|
||||
export {
|
||||
validateClientForWorkspace,
|
||||
createWorkspace,
|
||||
deleteWorkspace
|
||||
};
|
||||
|
@ -1,194 +1,179 @@
|
||||
import mongoose from 'mongoose';
|
||||
import dotenv from 'dotenv';
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import infisical from 'infisical-node';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { DatabaseService } from './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
|
||||
} 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
|
||||
} 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,
|
||||
getSmtpHost
|
||||
} 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 () => {
|
||||
if (process.env.INFISICAL_TOKEN != "" || process.env.INFISICAL_TOKEN != undefined) {
|
||||
await infisical.connect({
|
||||
token: process.env.INFISICAL_TOKEN!
|
||||
});
|
||||
}
|
||||
await setup();
|
||||
|
||||
TelemetryService.logTelemetryMessage();
|
||||
setTransporter(initSmtp());
|
||||
await EELicenseService.initGlobalFeatureSet();
|
||||
|
||||
await DatabaseService.initDatabase(getMongoURL());
|
||||
if (getNodeEnv() !== 'test') {
|
||||
Sentry.init({
|
||||
dsn: getSentryDSN(),
|
||||
tracesSampleRate: 1.0,
|
||||
debug: getNodeEnv() === 'production' ? false : true,
|
||||
environment: 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: 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 (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);
|
||||
// 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);
|
||||
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);
|
||||
|
||||
// v3 routes (experimental)
|
||||
app.use('/api/v3/secrets', v3SecretsRouter);
|
||||
app.use('/api/v3/workspaces', v3WorkspacesRouter);
|
||||
return server;
|
||||
};
|
||||
|
||||
// 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(getPort(), () => {
|
||||
getLogger("backend-main").info(`Server started listening at port ${getPort()}`)
|
||||
});
|
||||
|
||||
await createTestUserForDevelopment();
|
||||
setUpHealthEndpoint(server);
|
||||
|
||||
server.on('close', async () => {
|
||||
await DatabaseService.closeDatabase();
|
||||
})
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
export default main();
|
||||
export default main();
|
||||
|
@ -1,7 +1,6 @@
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { IIntegrationAuth } from "../models";
|
||||
import request from '../config/request';
|
||||
import { standardRequest } from "../config/request";
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_AWS_PARAMETER_STORE,
|
||||
@ -26,7 +25,7 @@ import {
|
||||
INTEGRATION_FLYIO_API_URL,
|
||||
INTEGRATION_CIRCLECI_API_URL,
|
||||
INTEGRATION_TRAVISCI_API_URL,
|
||||
INTEGRATION_SUPABASE_API_URL
|
||||
INTEGRATION_SUPABASE_API_URL,
|
||||
} from "../variables";
|
||||
|
||||
interface App {
|
||||
@ -47,87 +46,80 @@ interface App {
|
||||
const getApps = async ({
|
||||
integrationAuth,
|
||||
accessToken,
|
||||
teamId
|
||||
teamId,
|
||||
}: {
|
||||
integrationAuth: IIntegrationAuth;
|
||||
accessToken: string;
|
||||
teamId?: string;
|
||||
}) => {
|
||||
|
||||
let apps: App[] = [];
|
||||
try {
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_AZURE_KEY_VAULT:
|
||||
apps = [];
|
||||
break;
|
||||
case INTEGRATION_AWS_PARAMETER_STORE:
|
||||
apps = [];
|
||||
break;
|
||||
case INTEGRATION_AWS_SECRET_MANAGER:
|
||||
apps = [];
|
||||
break;
|
||||
case INTEGRATION_HEROKU:
|
||||
apps = await getAppsHeroku({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
apps = await getAppsVercel({
|
||||
integrationAuth,
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
apps = await getAppsNetlify({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITHUB:
|
||||
apps = await getAppsGithub({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
apps = await getAppsGitlab({
|
||||
accessToken,
|
||||
teamId
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_RENDER:
|
||||
apps = await getAppsRender({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_RAILWAY:
|
||||
apps = await getAppsRailway({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_FLYIO:
|
||||
apps = await getAppsFlyio({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_CIRCLECI:
|
||||
apps = await getAppsCircleCI({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_TRAVISCI:
|
||||
apps = await getAppsTravisCI({
|
||||
accessToken,
|
||||
})
|
||||
break;
|
||||
case INTEGRATION_SUPABASE:
|
||||
apps = await getAppsSupabase({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get integration apps");
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_AZURE_KEY_VAULT:
|
||||
apps = [];
|
||||
break;
|
||||
case INTEGRATION_AWS_PARAMETER_STORE:
|
||||
apps = [];
|
||||
break;
|
||||
case INTEGRATION_AWS_SECRET_MANAGER:
|
||||
apps = [];
|
||||
break;
|
||||
case INTEGRATION_HEROKU:
|
||||
apps = await getAppsHeroku({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
apps = await getAppsVercel({
|
||||
integrationAuth,
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
apps = await getAppsNetlify({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITHUB:
|
||||
apps = await getAppsGithub({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
apps = await getAppsGitlab({
|
||||
accessToken,
|
||||
teamId,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_RENDER:
|
||||
apps = await getAppsRender({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_RAILWAY:
|
||||
apps = await getAppsRailway({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_FLYIO:
|
||||
apps = await getAppsFlyio({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_CIRCLECI:
|
||||
apps = await getAppsCircleCI({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_TRAVISCI:
|
||||
apps = await getAppsTravisCI({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_SUPABASE:
|
||||
apps = await getAppsSupabase({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return apps;
|
||||
@ -141,25 +133,18 @@ const getApps = async ({
|
||||
* @returns {String} apps.name - name of Heroku app
|
||||
*/
|
||||
const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
|
||||
let apps;
|
||||
try {
|
||||
const res = (
|
||||
await request.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
|
||||
headers: {
|
||||
Accept: "application/vnd.heroku+json; version=3",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
).data;
|
||||
const res = (
|
||||
await standardRequest.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
|
||||
headers: {
|
||||
Accept: "application/vnd.heroku+json; version=3",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
).data;
|
||||
|
||||
apps = res.map((a: any) => ({
|
||||
name: a.name,
|
||||
}));
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get Heroku integration apps");
|
||||
}
|
||||
const apps = res.map((a: any) => ({
|
||||
name: a.name,
|
||||
}));
|
||||
|
||||
return apps;
|
||||
};
|
||||
@ -178,33 +163,26 @@ const getAppsVercel = async ({
|
||||
integrationAuth: IIntegrationAuth;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let apps;
|
||||
try {
|
||||
const res = (
|
||||
await request.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
...(integrationAuth?.teamId
|
||||
? {
|
||||
params: {
|
||||
teamId: integrationAuth.teamId,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
).data;
|
||||
const res = (
|
||||
await standardRequest.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
...(integrationAuth?.teamId
|
||||
? {
|
||||
params: {
|
||||
teamId: integrationAuth.teamId,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
).data;
|
||||
|
||||
apps = res.projects.map((a: any) => ({
|
||||
name: a.name,
|
||||
appId: a.id
|
||||
}));
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get Vercel integration apps");
|
||||
}
|
||||
const apps = res.projects.map((a: any) => ({
|
||||
name: a.name,
|
||||
appId: a.id,
|
||||
}));
|
||||
|
||||
return apps;
|
||||
};
|
||||
@ -218,43 +196,41 @@ const getAppsVercel = async ({
|
||||
*/
|
||||
const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
|
||||
const apps: any = [];
|
||||
try {
|
||||
let page = 1;
|
||||
const perPage = 10;
|
||||
let hasMorePages = true;
|
||||
|
||||
// paginate through all sites
|
||||
while (hasMorePages) {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
per_page: String(perPage)
|
||||
});
|
||||
let page = 1;
|
||||
const perPage = 10;
|
||||
let hasMorePages = true;
|
||||
|
||||
const { data } = await request.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
|
||||
// paginate through all sites
|
||||
while (hasMorePages) {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
per_page: String(perPage),
|
||||
filter: 'all'
|
||||
});
|
||||
|
||||
const { data } = await standardRequest.get(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
data.map((a: any) => {
|
||||
apps.push({
|
||||
name: a.name,
|
||||
appId: a.site_id
|
||||
});
|
||||
});
|
||||
|
||||
if (data.length < perPage) {
|
||||
hasMorePages = false;
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
page++;
|
||||
data.map((a: any) => {
|
||||
apps.push({
|
||||
name: a.name,
|
||||
appId: a.site_id,
|
||||
});
|
||||
});
|
||||
|
||||
if (data.length < perPage) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get Netlify integration apps");
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
return apps;
|
||||
@ -268,35 +244,58 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
|
||||
* @returns {String} apps.name - name of Github site
|
||||
*/
|
||||
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
|
||||
let apps;
|
||||
try {
|
||||
const octokit = new Octokit({
|
||||
auth: accessToken,
|
||||
});
|
||||
interface GitHubApp {
|
||||
id: string;
|
||||
name: string;
|
||||
permissions: {
|
||||
admin: boolean;
|
||||
};
|
||||
owner: {
|
||||
login: string;
|
||||
};
|
||||
}
|
||||
|
||||
const repos = (
|
||||
await octokit.request(
|
||||
const octokit = new Octokit({
|
||||
auth: accessToken,
|
||||
});
|
||||
|
||||
const getAllRepos = async () => {
|
||||
let repos: GitHubApp[] = [];
|
||||
let page = 1;
|
||||
const per_page = 100;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await octokit.request(
|
||||
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
|
||||
{
|
||||
per_page: 100,
|
||||
per_page,
|
||||
page,
|
||||
}
|
||||
)
|
||||
).data;
|
||||
);
|
||||
|
||||
apps = repos
|
||||
.filter((a: any) => a.permissions.admin === true)
|
||||
.map((a: any) => {
|
||||
return ({
|
||||
appId: a.id,
|
||||
name: a.name,
|
||||
owner: a.owner.login,
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get Github repos");
|
||||
}
|
||||
if (response.data.length > 0) {
|
||||
repos = repos.concat(response.data);
|
||||
page++;
|
||||
} else {
|
||||
hasMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
return repos;
|
||||
};
|
||||
|
||||
const repos = await getAllRepos();
|
||||
|
||||
const apps = repos
|
||||
.filter((a: GitHubApp) => a.permissions.admin === true)
|
||||
.map((a: GitHubApp) => {
|
||||
return {
|
||||
appId: a.id,
|
||||
name: a.name,
|
||||
owner: a.owner.login,
|
||||
};
|
||||
});
|
||||
|
||||
return apps;
|
||||
};
|
||||
@ -310,29 +309,20 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
|
||||
* @returns {String} apps.appId - id of Render service
|
||||
*/
|
||||
const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
|
||||
let apps: any;
|
||||
try {
|
||||
const res = (
|
||||
await request.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Accept-Encoding': 'application/json',
|
||||
},
|
||||
})
|
||||
).data;
|
||||
|
||||
apps = res
|
||||
.map((a: any) => ({
|
||||
name: a.service.name,
|
||||
appId: a.service.id
|
||||
}));
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get Render services");
|
||||
}
|
||||
const res = (
|
||||
await standardRequest.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
})
|
||||
).data;
|
||||
|
||||
const apps = res.map((a: any) => ({
|
||||
name: a.service.name,
|
||||
appId: a.service.id,
|
||||
}));
|
||||
|
||||
return apps;
|
||||
};
|
||||
@ -345,49 +335,51 @@ const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
|
||||
* @returns {String} apps.name - name of Railway project
|
||||
* @returns {String} apps.appId - id of Railway project
|
||||
*
|
||||
*/
|
||||
*/
|
||||
const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
|
||||
let apps: any[] = [];
|
||||
try {
|
||||
const query = `
|
||||
query GetProjects($userId: String, $teamId: String) {
|
||||
projects(userId: $userId, teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
const query = `
|
||||
query GetProjects($userId: String, $teamId: String) {
|
||||
projects(userId: $userId, teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
`;
|
||||
|
||||
const variables = {};
|
||||
const variables = {};
|
||||
|
||||
const { data: { data: { projects: { edges }}} } = await request.post(INTEGRATION_RAILWAY_API_URL, {
|
||||
const {
|
||||
data: {
|
||||
data: {
|
||||
projects: { edges },
|
||||
},
|
||||
},
|
||||
} = await standardRequest.post(
|
||||
INTEGRATION_RAILWAY_API_URL,
|
||||
{
|
||||
query,
|
||||
variables,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept-Encoding': 'application/json'
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
apps = edges.map((e: any) => ({
|
||||
name: e.node.name,
|
||||
appId: e.node.id
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
const apps = edges.map((e: any) => ({
|
||||
name: e.node.name,
|
||||
appId: e.node.id,
|
||||
}));
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get Railway services");
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of apps for Fly.io integration
|
||||
@ -397,41 +389,40 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
|
||||
* @returns {String} apps.name - name of Fly.io apps
|
||||
*/
|
||||
const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
|
||||
let apps;
|
||||
try {
|
||||
const query = `
|
||||
query($role: String) {
|
||||
apps(type: "container", first: 400, role: $role) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
hostname
|
||||
}
|
||||
const query = `
|
||||
query($role: String) {
|
||||
apps(type: "container", first: 400, role: $role) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
hostname
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
`;
|
||||
|
||||
const res = (await request.post(INTEGRATION_FLYIO_API_URL, {
|
||||
query,
|
||||
variables: {
|
||||
role: null,
|
||||
const res = (
|
||||
await standardRequest.post(
|
||||
INTEGRATION_FLYIO_API_URL,
|
||||
{
|
||||
query,
|
||||
variables: {
|
||||
role: null,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
'Accept': 'application/json',
|
||||
'Accept-Encoding': 'application/json',
|
||||
},
|
||||
})).data.data.apps.nodes;
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
Accept: "application/json",
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
).data.data.apps.nodes;
|
||||
|
||||
apps = res.map((a: any) => ({
|
||||
name: a.name,
|
||||
}));
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get Fly.io apps");
|
||||
}
|
||||
const apps = res.map((a: any) => ({
|
||||
name: a.name,
|
||||
}));
|
||||
|
||||
return apps;
|
||||
};
|
||||
@ -444,63 +435,43 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
|
||||
* @returns {String} apps.name - name of CircleCI apps
|
||||
*/
|
||||
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
|
||||
let apps: any;
|
||||
try {
|
||||
const res = (
|
||||
await request.get(
|
||||
`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`,
|
||||
{
|
||||
headers: {
|
||||
"Circle-Token": accessToken,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
).data
|
||||
const res = (
|
||||
await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`, {
|
||||
headers: {
|
||||
"Circle-Token": accessToken,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
})
|
||||
).data;
|
||||
|
||||
const apps = res?.map((a: any) => {
|
||||
return {
|
||||
name: a?.reponame,
|
||||
};
|
||||
});
|
||||
|
||||
apps = res?.map((a: any) => {
|
||||
return {
|
||||
name: a?.reponame
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get CircleCI projects");
|
||||
}
|
||||
|
||||
return apps;
|
||||
};
|
||||
|
||||
const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
|
||||
let apps: any;
|
||||
try {
|
||||
const res = (
|
||||
await request.get(
|
||||
`${INTEGRATION_TRAVISCI_API_URL}/repos`,
|
||||
{
|
||||
headers: {
|
||||
"Authorization": `token ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
).data;
|
||||
const res = (
|
||||
await standardRequest.get(`${INTEGRATION_TRAVISCI_API_URL}/repos`, {
|
||||
headers: {
|
||||
Authorization: `token ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
})
|
||||
).data;
|
||||
|
||||
const apps = res?.map((a: any) => {
|
||||
return {
|
||||
name: a?.slug?.split("/")[1],
|
||||
appId: a?.id,
|
||||
};
|
||||
});
|
||||
|
||||
apps = res?.map((a: any) => {
|
||||
return {
|
||||
name: a?.slug?.split("/")[1],
|
||||
appId: a?.id,
|
||||
}
|
||||
});
|
||||
}catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get TravisCI projects");
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of repositories for GitLab integration
|
||||
@ -509,112 +480,98 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
|
||||
* @returns {Object[]} apps - names of GitLab sites
|
||||
* @returns {String} apps.name - name of GitLab site
|
||||
*/
|
||||
const getAppsGitlab = async ({
|
||||
const getAppsGitlab = async ({
|
||||
accessToken,
|
||||
teamId
|
||||
teamId,
|
||||
}: {
|
||||
accessToken: string;
|
||||
teamId?: string;
|
||||
}) => {
|
||||
const apps: App[] = [];
|
||||
|
||||
|
||||
let page = 1;
|
||||
const perPage = 10;
|
||||
let hasMorePages = true;
|
||||
try {
|
||||
|
||||
if (teamId) {
|
||||
// case: fetch projects for group with id [teamId] in GitLab
|
||||
|
||||
while (hasMorePages) {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
per_page: String(perPage)
|
||||
});
|
||||
if (teamId) {
|
||||
// case: fetch projects for group with id [teamId] in GitLab
|
||||
|
||||
const { data } = (
|
||||
await request.get(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
while (hasMorePages) {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
per_page: String(perPage),
|
||||
});
|
||||
|
||||
data.map((a: any) => {
|
||||
apps.push({
|
||||
name: a.name,
|
||||
appId: a.id
|
||||
});
|
||||
});
|
||||
|
||||
if (data.length < perPage) {
|
||||
hasMorePages = false;
|
||||
const { data } = await standardRequest.get(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
} else {
|
||||
// case: fetch projects for individual in GitLab
|
||||
|
||||
const { id } = (
|
||||
await request.get(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/user`,
|
||||
{
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
).data;
|
||||
|
||||
while (hasMorePages) {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
per_page: String(perPage)
|
||||
);
|
||||
|
||||
data.map((a: any) => {
|
||||
apps.push({
|
||||
name: a.name,
|
||||
appId: a.id,
|
||||
});
|
||||
});
|
||||
|
||||
const { data } = (
|
||||
await request.get(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
data.map((a: any) => {
|
||||
apps.push({
|
||||
name: a.name,
|
||||
appId: a.id
|
||||
});
|
||||
});
|
||||
|
||||
if (data.length < perPage) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
|
||||
page++;
|
||||
if (data.length < perPage) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get GitLab projects");
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
} else {
|
||||
// case: fetch projects for individual in GitLab
|
||||
|
||||
const { id } = (
|
||||
await standardRequest.get(`${INTEGRATION_GITLAB_API_URL}/v4/user`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
})
|
||||
).data;
|
||||
|
||||
while (hasMorePages) {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
per_page: String(perPage),
|
||||
});
|
||||
|
||||
const { data } = await standardRequest.get(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
data.map((a: any) => {
|
||||
apps.push({
|
||||
name: a.name,
|
||||
appId: a.id,
|
||||
});
|
||||
});
|
||||
|
||||
if (data.length < perPage) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
}
|
||||
|
||||
return apps;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of projects for Supabase integration
|
||||
@ -624,30 +581,23 @@ const getAppsGitlab = async ({
|
||||
* @returns {String} apps.name - name of Supabase app
|
||||
*/
|
||||
const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => {
|
||||
let apps: any;
|
||||
try {
|
||||
const { data } = await request.get(
|
||||
`${INTEGRATION_SUPABASE_API_URL}/v1/projects`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
const { data } = await standardRequest.get(
|
||||
`${INTEGRATION_SUPABASE_API_URL}/v1/projects`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const apps = data.map((a: any) => {
|
||||
return {
|
||||
name: a.name,
|
||||
appId: a.id,
|
||||
};
|
||||
});
|
||||
|
||||
apps = data.map((a: any) => {
|
||||
return {
|
||||
name: a.name,
|
||||
appId: a.id
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get Supabase projects');
|
||||
}
|
||||
|
||||
return apps;
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import request from '../config/request';
|
||||
import { standardRequest } from "../config/request";
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
@ -12,8 +11,8 @@ import {
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
INTEGRATION_GITHUB_TOKEN_URL,
|
||||
INTEGRATION_GITLAB_TOKEN_URL
|
||||
} from '../variables';
|
||||
INTEGRATION_GITLAB_TOKEN_URL,
|
||||
} from "../variables";
|
||||
import {
|
||||
getSiteURL,
|
||||
getClientIdAzure,
|
||||
@ -26,8 +25,8 @@ import {
|
||||
getClientIdGitHub,
|
||||
getClientSecretGitHub,
|
||||
getClientIdGitLab,
|
||||
getClientSecretGitLab
|
||||
} from '../config';
|
||||
getClientSecretGitLab,
|
||||
} from "../config";
|
||||
|
||||
interface ExchangeCodeAzureResponse {
|
||||
token_type: string;
|
||||
@ -93,49 +92,43 @@ interface ExchangeCodeGitlabResponse {
|
||||
*/
|
||||
const exchangeCode = async ({
|
||||
integration,
|
||||
code
|
||||
code,
|
||||
}: {
|
||||
integration: string;
|
||||
code: string;
|
||||
}) => {
|
||||
let obj = {} as any;
|
||||
|
||||
try {
|
||||
switch (integration) {
|
||||
case INTEGRATION_AZURE_KEY_VAULT:
|
||||
obj = await exchangeCodeAzure({
|
||||
code
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_HEROKU:
|
||||
obj = await exchangeCodeHeroku({
|
||||
code
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
obj = await exchangeCodeVercel({
|
||||
code
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
obj = await exchangeCodeNetlify({
|
||||
code
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITHUB:
|
||||
obj = await exchangeCodeGithub({
|
||||
code
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
obj = await exchangeCodeGitlab({
|
||||
code
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange');
|
||||
switch (integration) {
|
||||
case INTEGRATION_AZURE_KEY_VAULT:
|
||||
obj = await exchangeCodeAzure({
|
||||
code,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_HEROKU:
|
||||
obj = await exchangeCodeHeroku({
|
||||
code,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
obj = await exchangeCodeVercel({
|
||||
code,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
obj = await exchangeCodeNetlify({
|
||||
code,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITHUB:
|
||||
obj = await exchangeCodeGithub({
|
||||
code,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
obj = await exchangeCodeGitlab({
|
||||
code,
|
||||
});
|
||||
}
|
||||
|
||||
return obj;
|
||||
@ -143,43 +136,33 @@ const exchangeCode = async ({
|
||||
|
||||
/**
|
||||
* Return [accessToken] for Azure OAuth2 code-token exchange
|
||||
* @param param0
|
||||
* @param param0
|
||||
*/
|
||||
const exchangeCodeAzure = async ({
|
||||
code
|
||||
}: {
|
||||
code: string;
|
||||
}) => {
|
||||
const exchangeCodeAzure = async ({ code }: { code: string }) => {
|
||||
const accessExpiresAt = new Date();
|
||||
let res: ExchangeCodeAzureResponse;
|
||||
try {
|
||||
res = (await request.post(
|
||||
|
||||
const res: ExchangeCodeAzureResponse = (
|
||||
await standardRequest.post(
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
grant_type: "authorization_code",
|
||||
code: code,
|
||||
scope: 'https://vault.azure.net/.default openid offline_access',
|
||||
client_id: getClientIdAzure(),
|
||||
client_secret: getClientSecretAzure(),
|
||||
redirect_uri: `${getSiteURL()}/integrations/azure-key-vault/oauth2/callback`
|
||||
scope: "https://vault.azure.net/.default openid offline_access",
|
||||
client_id: await getClientIdAzure(),
|
||||
client_secret: await getClientSecretAzure(),
|
||||
redirect_uri: `${await getSiteURL()}/integrations/azure-key-vault/oauth2/callback`,
|
||||
} as any)
|
||||
)).data;
|
||||
)
|
||||
).data;
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + res.expires_in
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Azure');
|
||||
}
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||
|
||||
return ({
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accessExpiresAt
|
||||
});
|
||||
}
|
||||
accessExpiresAt,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
|
||||
@ -191,38 +174,28 @@ const exchangeCodeAzure = async ({
|
||||
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeHeroku = async ({
|
||||
code
|
||||
}: {
|
||||
code: string;
|
||||
}) => {
|
||||
let res: ExchangeCodeHerokuResponse;
|
||||
const exchangeCodeHeroku = async ({ code }: { code: string }) => {
|
||||
const accessExpiresAt = new Date();
|
||||
try {
|
||||
res = (await request.post(
|
||||
|
||||
const res: ExchangeCodeHerokuResponse = (
|
||||
await standardRequest.post(
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
grant_type: "authorization_code",
|
||||
code: code,
|
||||
client_secret: getClientSecretHeroku()
|
||||
client_secret: await getClientSecretHeroku(),
|
||||
} as any)
|
||||
)).data;
|
||||
)
|
||||
).data;
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + res.expires_in
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Heroku');
|
||||
}
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||
|
||||
return ({
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accessExpiresAt
|
||||
});
|
||||
}
|
||||
accessExpiresAt,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
||||
@ -235,30 +208,23 @@ const exchangeCodeHeroku = async ({
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeVercel = async ({ code }: { code: string }) => {
|
||||
let res: ExchangeCodeVercelResponse;
|
||||
try {
|
||||
res = (
|
||||
await request.post(
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
code: code,
|
||||
client_id: getClientIdVercel(),
|
||||
client_secret: getClientSecretVercel(),
|
||||
redirect_uri: `${getSiteURL()}/integrations/vercel/oauth2/callback`
|
||||
} as any)
|
||||
)
|
||||
).data;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error(`Failed OAuth2 code-token exchange with Vercel [err=${err}]`);
|
||||
}
|
||||
const res: ExchangeCodeVercelResponse = (
|
||||
await standardRequest.post(
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
code: code,
|
||||
client_id: await getClientIdVercel(),
|
||||
client_secret: await getClientSecretVercel(),
|
||||
redirect_uri: `${await getSiteURL()}/integrations/vercel/oauth2/callback`,
|
||||
} as any)
|
||||
)
|
||||
).data;
|
||||
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: null,
|
||||
accessExpiresAt: null,
|
||||
teamId: res.team_id
|
||||
teamId: res.team_id,
|
||||
};
|
||||
};
|
||||
|
||||
@ -273,47 +239,39 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeNetlify = async ({ code }: { code: string }) => {
|
||||
let res: ExchangeCodeNetlifyResponse;
|
||||
let accountId;
|
||||
try {
|
||||
res = (
|
||||
await request.post(
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_id: getClientIdNetlify(),
|
||||
client_secret: getClientSecretNetlify(),
|
||||
redirect_uri: `${getSiteURL()}/integrations/netlify/oauth2/callback`
|
||||
} as any)
|
||||
)
|
||||
).data;
|
||||
const res: ExchangeCodeNetlifyResponse = (
|
||||
await standardRequest.post(
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: code,
|
||||
client_id: await getClientIdNetlify(),
|
||||
client_secret: await getClientSecretNetlify(),
|
||||
redirect_uri: `${await getSiteURL()}/integrations/netlify/oauth2/callback`,
|
||||
} as any)
|
||||
)
|
||||
).data;
|
||||
|
||||
const res2 = await request.get('https://api.netlify.com/api/v1/sites', {
|
||||
const res2 = await standardRequest.get("https://api.netlify.com/api/v1/sites", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${res.access_token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const res3 = (
|
||||
await standardRequest.get("https://api.netlify.com/api/v1/accounts", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${res.access_token}`
|
||||
}
|
||||
});
|
||||
Authorization: `Bearer ${res.access_token}`,
|
||||
},
|
||||
})
|
||||
).data;
|
||||
|
||||
const res3 = (
|
||||
await request.get('https://api.netlify.com/api/v1/accounts', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${res.access_token}`
|
||||
}
|
||||
})
|
||||
).data;
|
||||
|
||||
accountId = res3[0].id;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Netlify');
|
||||
}
|
||||
const accountId = res3[0].id;
|
||||
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accountId
|
||||
accountId,
|
||||
};
|
||||
};
|
||||
|
||||
@ -328,33 +286,25 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeGithub = async ({ code }: { code: string }) => {
|
||||
let res: ExchangeCodeGithubResponse;
|
||||
try {
|
||||
res = (
|
||||
await request.get(INTEGRATION_GITHUB_TOKEN_URL, {
|
||||
params: {
|
||||
client_id: getClientIdGitHub(),
|
||||
client_secret: getClientSecretGitHub(),
|
||||
code: code,
|
||||
redirect_uri: `${getSiteURL()}/integrations/github/oauth2/callback`
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
})
|
||||
).data;
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Github');
|
||||
}
|
||||
const res: ExchangeCodeGithubResponse = (
|
||||
await standardRequest.get(INTEGRATION_GITHUB_TOKEN_URL, {
|
||||
params: {
|
||||
client_id: await getClientIdGitHub(),
|
||||
client_secret: await getClientSecretGitHub(),
|
||||
code: code,
|
||||
redirect_uri: `${await getSiteURL()}/integrations/github/oauth2/callback`,
|
||||
},
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
})
|
||||
).data;
|
||||
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: null,
|
||||
accessExpiresAt: null
|
||||
accessExpiresAt: null,
|
||||
};
|
||||
};
|
||||
|
||||
@ -369,42 +319,32 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeGitlab = async ({ code }: { code: string }) => {
|
||||
let res: ExchangeCodeGitlabResponse;
|
||||
const accessExpiresAt = new Date();
|
||||
|
||||
try {
|
||||
res = (
|
||||
await request.post(
|
||||
INTEGRATION_GITLAB_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_id: getClientIdGitLab(),
|
||||
client_secret: getClientSecretGitLab(),
|
||||
redirect_uri: `${getSiteURL()}/integrations/gitlab/oauth2/callback`
|
||||
} as any),
|
||||
{
|
||||
headers: {
|
||||
"Accept-Encoding": "application/json",
|
||||
}
|
||||
}
|
||||
)
|
||||
).data;
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + res.expires_in
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Gitlab');
|
||||
}
|
||||
const res: ExchangeCodeGitlabResponse = (
|
||||
await standardRequest.post(
|
||||
INTEGRATION_GITLAB_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: code,
|
||||
client_id: await getClientIdGitLab(),
|
||||
client_secret: await getClientSecretGitLab(),
|
||||
redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`,
|
||||
} as any),
|
||||
{
|
||||
headers: {
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
).data;
|
||||
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accessExpiresAt
|
||||
accessExpiresAt,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export { exchangeCode };
|
||||
|
@ -1,29 +1,24 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import request from '../config/request';
|
||||
import { standardRequest } from "../config/request";
|
||||
import { IIntegrationAuth } from "../models";
|
||||
import {
|
||||
IIntegrationAuth
|
||||
} from '../models';
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_GITLAB,
|
||||
} from '../variables';
|
||||
} from "../variables";
|
||||
import {
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_GITLAB_TOKEN_URL
|
||||
} from '../variables';
|
||||
import {
|
||||
IntegrationService
|
||||
} from '../services';
|
||||
INTEGRATION_GITLAB_TOKEN_URL,
|
||||
} from "../variables";
|
||||
import { IntegrationService } from "../services";
|
||||
import {
|
||||
getSiteURL,
|
||||
getClientIdAzure,
|
||||
getClientSecretAzure,
|
||||
getClientSecretHeroku,
|
||||
getClientIdGitLab,
|
||||
getClientSecretGitLab
|
||||
} from '../config';
|
||||
getClientSecretGitLab,
|
||||
} from "../config";
|
||||
|
||||
interface RefreshTokenAzureResponse {
|
||||
token_type: string;
|
||||
@ -60,60 +55,57 @@ interface RefreshTokenGitLabResponse {
|
||||
*/
|
||||
const exchangeRefresh = async ({
|
||||
integrationAuth,
|
||||
refreshToken
|
||||
refreshToken,
|
||||
}: {
|
||||
integrationAuth: IIntegrationAuth;
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
|
||||
interface TokenDetails {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
accessExpiresAt: Date;
|
||||
}
|
||||
|
||||
|
||||
let tokenDetails: TokenDetails;
|
||||
try {
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_AZURE_KEY_VAULT:
|
||||
tokenDetails = await exchangeRefreshAzure({
|
||||
refreshToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_HEROKU:
|
||||
tokenDetails = await exchangeRefreshHeroku({
|
||||
refreshToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
tokenDetails = await exchangeRefreshGitLab({
|
||||
refreshToken
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error('Failed to exchange token for incompatible integration');
|
||||
}
|
||||
|
||||
if (tokenDetails?.accessToken && tokenDetails?.refreshToken && tokenDetails?.accessExpiresAt) {
|
||||
await IntegrationService.setIntegrationAuthAccess({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
accessId: null,
|
||||
accessToken: tokenDetails.accessToken,
|
||||
accessExpiresAt: tokenDetails.accessExpiresAt
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_AZURE_KEY_VAULT:
|
||||
tokenDetails = await exchangeRefreshAzure({
|
||||
refreshToken,
|
||||
});
|
||||
|
||||
await IntegrationService.setIntegrationAuthRefresh({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
refreshToken: tokenDetails.refreshToken
|
||||
break;
|
||||
case INTEGRATION_HEROKU:
|
||||
tokenDetails = await exchangeRefreshHeroku({
|
||||
refreshToken,
|
||||
});
|
||||
}
|
||||
|
||||
return tokenDetails.accessToken;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get new OAuth2 access token');
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
tokenDetails = await exchangeRefreshGitLab({
|
||||
refreshToken,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error("Failed to exchange token for incompatible integration");
|
||||
}
|
||||
|
||||
if (
|
||||
tokenDetails?.accessToken &&
|
||||
tokenDetails?.refreshToken &&
|
||||
tokenDetails?.accessExpiresAt
|
||||
) {
|
||||
await IntegrationService.setIntegrationAuthAccess({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
accessId: null,
|
||||
accessToken: tokenDetails.accessToken,
|
||||
accessExpiresAt: tokenDetails.accessExpiresAt,
|
||||
});
|
||||
|
||||
await IntegrationService.setIntegrationAuthRefresh({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
refreshToken: tokenDetails.refreshToken,
|
||||
});
|
||||
}
|
||||
|
||||
return tokenDetails.accessToken;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -124,38 +116,30 @@ const exchangeRefresh = async ({
|
||||
* @returns
|
||||
*/
|
||||
const exchangeRefreshAzure = async ({
|
||||
refreshToken
|
||||
refreshToken,
|
||||
}: {
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
try {
|
||||
const accessExpiresAt = new Date();
|
||||
const { data }: { data: RefreshTokenAzureResponse } = await request.post(
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
client_id: getClientIdAzure(),
|
||||
scope: 'openid offline_access',
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token',
|
||||
client_secret: getClientSecretAzure()
|
||||
} as any)
|
||||
);
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + data.expires_in
|
||||
);
|
||||
const accessExpiresAt = new Date();
|
||||
const { data }: { data: RefreshTokenAzureResponse } = await standardRequest.post(
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
client_id: await getClientIdAzure(),
|
||||
scope: "openid offline_access",
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
client_secret: await getClientSecretAzure(),
|
||||
} as any)
|
||||
);
|
||||
|
||||
return ({
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
accessExpiresAt
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get refresh OAuth2 access token for Azure');
|
||||
}
|
||||
}
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
accessExpiresAt,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||
@ -165,39 +149,31 @@ const exchangeRefreshAzure = async ({
|
||||
* @returns
|
||||
*/
|
||||
const exchangeRefreshHeroku = async ({
|
||||
refreshToken
|
||||
refreshToken,
|
||||
}: {
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
try {
|
||||
const accessExpiresAt = new Date();
|
||||
const {
|
||||
data
|
||||
}: {
|
||||
data: RefreshTokenHerokuResponse
|
||||
} = await request.post(
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_secret: getClientSecretHeroku()
|
||||
} as any)
|
||||
);
|
||||
const accessExpiresAt = new Date();
|
||||
const {
|
||||
data,
|
||||
}: {
|
||||
data: RefreshTokenHerokuResponse;
|
||||
} = await standardRequest.post(
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_secret: await getClientSecretHeroku(),
|
||||
} as any)
|
||||
);
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + data.expires_in
|
||||
);
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||
|
||||
return ({
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
accessExpiresAt
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to refresh OAuth2 access token for Heroku');
|
||||
}
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
accessExpiresAt,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@ -208,45 +184,38 @@ const exchangeRefreshHeroku = async ({
|
||||
* @returns
|
||||
*/
|
||||
const exchangeRefreshGitLab = async ({
|
||||
refreshToken
|
||||
refreshToken,
|
||||
}: {
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
try {
|
||||
const accessExpiresAt = new Date();
|
||||
const {
|
||||
data
|
||||
}: {
|
||||
data: RefreshTokenGitLabResponse
|
||||
} = await request.post(
|
||||
INTEGRATION_GITLAB_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_id: getClientIdGitLab,
|
||||
client_secret: getClientSecretGitLab(),
|
||||
redirect_uri: `${getSiteURL()}/integrations/gitlab/oauth2/callback`
|
||||
} as any),
|
||||
{
|
||||
headers: {
|
||||
"Accept-Encoding": "application/json",
|
||||
}
|
||||
});
|
||||
const accessExpiresAt = new Date();
|
||||
const {
|
||||
data,
|
||||
}: {
|
||||
data: RefreshTokenGitLabResponse;
|
||||
} = await standardRequest.post(
|
||||
INTEGRATION_GITLAB_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_id: await getClientIdGitLab(),
|
||||
client_secret: await getClientSecretGitLab(),
|
||||
redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`,
|
||||
} as any),
|
||||
{
|
||||
headers: {
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + data.expires_in
|
||||
);
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||
|
||||
return ({
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
accessExpiresAt
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to refresh OAuth2 access token for GitLab');
|
||||
}
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
accessExpiresAt,
|
||||
};
|
||||
};
|
||||
|
||||
export { exchangeRefresh };
|
||||
|
@ -1,4 +1,3 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
IIntegrationAuth,
|
||||
IntegrationAuth,
|
||||
@ -22,34 +21,28 @@ const revokeAccess = async ({
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let deletedIntegrationAuth;
|
||||
try {
|
||||
// add any integration-specific revocation logic
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_HEROKU:
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
break;
|
||||
case INTEGRATION_GITHUB:
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
break;
|
||||
}
|
||||
// add any integration-specific revocation logic
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_HEROKU:
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
break;
|
||||
case INTEGRATION_GITHUB:
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
break;
|
||||
}
|
||||
|
||||
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||
_id: integrationAuth._id
|
||||
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||
_id: integrationAuth._id
|
||||
});
|
||||
|
||||
if (deletedIntegrationAuth) {
|
||||
await Integration.deleteMany({
|
||||
integrationAuth: deletedIntegrationAuth._id
|
||||
});
|
||||
|
||||
if (deletedIntegrationAuth) {
|
||||
await Integration.deleteMany({
|
||||
integrationAuth: deletedIntegrationAuth._id
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to delete integration authorization');
|
||||
}
|
||||
|
||||
return deletedIntegrationAuth;
|
||||
|
@ -37,8 +37,7 @@ import {
|
||||
INTEGRATION_TRAVISCI_API_URL,
|
||||
INTEGRATION_SUPABASE_API_URL
|
||||
} from "../variables";
|
||||
import request from '../config/request';
|
||||
import axios from "axios";
|
||||
import { standardRequest} from '../config/request';
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to [app] in integration named [integration]
|
||||
@ -215,7 +214,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
let result: GetAzureKeyVaultSecret[] = [];
|
||||
try {
|
||||
while (url) {
|
||||
const res = await request.get(url, {
|
||||
const res = await standardRequest.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
@ -242,7 +241,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf('/');
|
||||
}
|
||||
|
||||
const azureKeyVaultSecret = await request.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
|
||||
const azureKeyVaultSecret = await standardRequest.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
@ -308,7 +307,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
while (!isSecretSet && maxTries > 0) {
|
||||
// try to set secret
|
||||
try {
|
||||
await request.put(
|
||||
await standardRequest.put(
|
||||
`${integration.app}/secrets/${key}?api-version=7.3`,
|
||||
{
|
||||
value
|
||||
@ -325,7 +324,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
} catch (err) {
|
||||
const error: any = err;
|
||||
if (error?.response?.data?.error?.innererror?.code === 'ObjectIsDeletedButRecoverable') {
|
||||
await request.post(
|
||||
await standardRequest.post(
|
||||
`${integration.app}/deletedsecrets/${key}/recover?api-version=7.3`, {},
|
||||
{
|
||||
headers: {
|
||||
@ -355,7 +354,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
|
||||
for await (const deleteSecret of deleteSecrets) {
|
||||
const { key } = deleteSecret;
|
||||
await request.delete(`${integration.app}/secrets/${key}?api-version=7.3`, {
|
||||
await standardRequest.delete(`${integration.app}/secrets/${key}?api-version=7.3`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
@ -568,7 +567,7 @@ const syncSecretsHeroku = async ({
|
||||
}) => {
|
||||
try {
|
||||
const herokuSecrets = (
|
||||
await request.get(
|
||||
await standardRequest.get(
|
||||
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
|
||||
{
|
||||
headers: {
|
||||
@ -586,7 +585,7 @@ const syncSecretsHeroku = async ({
|
||||
}
|
||||
});
|
||||
|
||||
await request.patch(
|
||||
await standardRequest.patch(
|
||||
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
|
||||
secrets,
|
||||
{
|
||||
@ -642,7 +641,7 @@ const syncSecretsVercel = async ({
|
||||
: {}),
|
||||
};
|
||||
|
||||
const vercelSecrets: VercelSecret[] = (await request.get(
|
||||
const vercelSecrets: VercelSecret[] = (await standardRequest.get(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
|
||||
{
|
||||
params,
|
||||
@ -675,7 +674,7 @@ const syncSecretsVercel = async ({
|
||||
for await (const vercelSecret of vercelSecrets) {
|
||||
if (vercelSecret.type === 'encrypted') {
|
||||
// case: secret is encrypted -> need to decrypt
|
||||
const decryptedSecret = (await request.get(
|
||||
const decryptedSecret = (await standardRequest.get(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`,
|
||||
{
|
||||
params,
|
||||
@ -747,7 +746,7 @@ const syncSecretsVercel = async ({
|
||||
|
||||
// Sync/push new secrets
|
||||
if (newSecrets.length > 0) {
|
||||
await request.post(
|
||||
await standardRequest.post(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
|
||||
newSecrets,
|
||||
{
|
||||
@ -763,7 +762,7 @@ const syncSecretsVercel = async ({
|
||||
for await (const secret of updateSecrets) {
|
||||
if (secret.type !== 'sensitive') {
|
||||
const { id, ...updatedSecret } = secret;
|
||||
await request.patch(
|
||||
await standardRequest.patch(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
updatedSecret,
|
||||
{
|
||||
@ -778,7 +777,7 @@ const syncSecretsVercel = async ({
|
||||
}
|
||||
|
||||
for await (const secret of deleteSecrets) {
|
||||
await request.delete(
|
||||
await standardRequest.delete(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
{
|
||||
params,
|
||||
@ -837,7 +836,7 @@ const syncSecretsNetlify = async ({
|
||||
});
|
||||
|
||||
const res = (
|
||||
await request.get(
|
||||
await standardRequest.get(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||
{
|
||||
params: getParams,
|
||||
@ -951,7 +950,7 @@ const syncSecretsNetlify = async ({
|
||||
});
|
||||
|
||||
if (newSecrets.length > 0) {
|
||||
await request.post(
|
||||
await standardRequest.post(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||
newSecrets,
|
||||
{
|
||||
@ -966,7 +965,7 @@ const syncSecretsNetlify = async ({
|
||||
|
||||
if (updateSecrets.length > 0) {
|
||||
updateSecrets.forEach(async (secret: NetlifySecret) => {
|
||||
await request.patch(
|
||||
await standardRequest.patch(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||
{
|
||||
context: secret.values[0].context,
|
||||
@ -985,7 +984,7 @@ const syncSecretsNetlify = async ({
|
||||
|
||||
if (deleteSecrets.length > 0) {
|
||||
deleteSecrets.forEach(async (key: string) => {
|
||||
await request.delete(
|
||||
await standardRequest.delete(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`,
|
||||
{
|
||||
params: syncParams,
|
||||
@ -1000,7 +999,7 @@ const syncSecretsNetlify = async ({
|
||||
|
||||
if (deleteSecretValues.length > 0) {
|
||||
deleteSecretValues.forEach(async (secret: NetlifySecret) => {
|
||||
await request.delete(
|
||||
await standardRequest.delete(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`,
|
||||
{
|
||||
params: syncParams,
|
||||
@ -1151,7 +1150,7 @@ const syncSecretsRender = async ({
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
await request.put(
|
||||
await standardRequest.put(
|
||||
`${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`,
|
||||
Object.keys(secrets).map((key) => ({
|
||||
key,
|
||||
@ -1203,7 +1202,7 @@ const syncSecretsRailway = async ({
|
||||
variables: secrets
|
||||
};
|
||||
|
||||
await request.post(INTEGRATION_RAILWAY_API_URL, {
|
||||
await standardRequest.post(INTEGRATION_RAILWAY_API_URL, {
|
||||
query,
|
||||
variables: {
|
||||
input,
|
||||
@ -1261,7 +1260,7 @@ const syncSecretsFlyio = async ({
|
||||
}
|
||||
`;
|
||||
|
||||
await request.post(INTEGRATION_FLYIO_API_URL, {
|
||||
await standardRequest.post(INTEGRATION_FLYIO_API_URL, {
|
||||
query: SetSecrets,
|
||||
variables: {
|
||||
input: {
|
||||
@ -1296,7 +1295,7 @@ const syncSecretsFlyio = async ({
|
||||
}
|
||||
}`;
|
||||
|
||||
const getSecretsRes = (await request.post(INTEGRATION_FLYIO_API_URL, {
|
||||
const getSecretsRes = (await standardRequest.post(INTEGRATION_FLYIO_API_URL, {
|
||||
query: GetSecrets,
|
||||
variables: {
|
||||
appName: integration.app,
|
||||
@ -1332,7 +1331,7 @@ const syncSecretsFlyio = async ({
|
||||
}
|
||||
}`;
|
||||
|
||||
await request.post(INTEGRATION_FLYIO_API_URL, {
|
||||
await standardRequest.post(INTEGRATION_FLYIO_API_URL, {
|
||||
query: DeleteSecrets,
|
||||
variables: {
|
||||
input: {
|
||||
@ -1373,7 +1372,7 @@ const syncSecretsCircleCI = async ({
|
||||
}) => {
|
||||
try {
|
||||
const circleciOrganizationDetail = (
|
||||
await request.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, {
|
||||
await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, {
|
||||
headers: {
|
||||
"Circle-Token": accessToken,
|
||||
"Accept-Encoding": "application/json",
|
||||
@ -1386,7 +1385,7 @@ const syncSecretsCircleCI = async ({
|
||||
// sync secrets to CircleCI
|
||||
Object.keys(secrets).forEach(
|
||||
async (key) =>
|
||||
await request.post(
|
||||
await standardRequest.post(
|
||||
`${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
|
||||
{
|
||||
name: key,
|
||||
@ -1403,7 +1402,7 @@ const syncSecretsCircleCI = async ({
|
||||
|
||||
// get secrets from CircleCI
|
||||
const getSecretsRes = (
|
||||
await request.get(
|
||||
await standardRequest.get(
|
||||
`${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
|
||||
{
|
||||
headers: {
|
||||
@ -1417,7 +1416,7 @@ const syncSecretsCircleCI = async ({
|
||||
// delete secrets from CircleCI
|
||||
getSecretsRes.forEach(async (sec: any) => {
|
||||
if (!(sec.name in secrets)) {
|
||||
await request.delete(
|
||||
await standardRequest.delete(
|
||||
`${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`,
|
||||
{
|
||||
headers: {
|
||||
@ -1454,7 +1453,7 @@ const syncSecretsTravisCI = async ({
|
||||
try {
|
||||
// get secrets from travis-ci
|
||||
const getSecretsRes = (
|
||||
await request.get(
|
||||
await standardRequest.get(
|
||||
`${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`,
|
||||
{
|
||||
headers: {
|
||||
@ -1476,7 +1475,7 @@ const syncSecretsTravisCI = async ({
|
||||
if (!(key in getSecretsRes)) {
|
||||
// case: secret does not exist in travis ci
|
||||
// -> add secret
|
||||
await request.post(
|
||||
await standardRequest.post(
|
||||
`${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`,
|
||||
{
|
||||
env_var: {
|
||||
@ -1495,7 +1494,7 @@ const syncSecretsTravisCI = async ({
|
||||
} else {
|
||||
// case: secret exists in travis ci
|
||||
// -> update/set secret
|
||||
await request.patch(
|
||||
await standardRequest.patch(
|
||||
`${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`,
|
||||
{
|
||||
env_var: {
|
||||
@ -1517,7 +1516,7 @@ const syncSecretsTravisCI = async ({
|
||||
for await (const key of Object.keys(getSecretsRes)) {
|
||||
if (!(key in secrets)){
|
||||
// delete secret
|
||||
await request.delete(
|
||||
await standardRequest.delete(
|
||||
`${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`,
|
||||
{
|
||||
headers: {
|
||||
@ -1554,23 +1553,48 @@ const syncSecretsGitLab = async ({
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
// get secrets from gitlab
|
||||
const getSecretsRes = (
|
||||
await request.get(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`,
|
||||
{
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
interface GitLabSecret {
|
||||
key: string;
|
||||
value: string;
|
||||
environment_scope: string;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return allEnvVariables;
|
||||
};
|
||||
|
||||
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
|
||||
const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) =>
|
||||
secret.environment_scope === integration.targetEnvironment
|
||||
);
|
||||
|
||||
for await (const key of Object.keys(secrets)) {
|
||||
const existingSecret = getSecretsRes.find((s: any) => s.key == key);
|
||||
if (!existingSecret) {
|
||||
await request.post(
|
||||
await standardRequest.post(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`,
|
||||
{
|
||||
key: key,
|
||||
@ -1578,7 +1602,7 @@ const syncSecretsGitLab = async ({
|
||||
protected: false,
|
||||
masked: false,
|
||||
raw: false,
|
||||
environment_scope:'*'
|
||||
environment_scope: integration.targetEnvironment
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
@ -1589,29 +1613,31 @@ const syncSecretsGitLab = async ({
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// udpate secret
|
||||
await request.put(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}`,
|
||||
{
|
||||
...existingSecret,
|
||||
value: secrets[existingSecret.key]
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "application/json",
|
||||
// update secret
|
||||
if (secrets[key] !== existingSecret.value) {
|
||||
await standardRequest.put(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
|
||||
{
|
||||
...existingSecret,
|
||||
value: secrets[existingSecret.key]
|
||||
},
|
||||
}
|
||||
)
|
||||
{
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete secrets
|
||||
for await (const sec of getSecretsRes) {
|
||||
if (!(sec.key in secrets)) {
|
||||
await request.delete(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}`,
|
||||
await standardRequest.delete(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}?filter[environment_scope]=${integration.targetEnvironment}`,
|
||||
{
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
@ -1620,7 +1646,8 @@ const syncSecretsGitLab = async ({
|
||||
);
|
||||
}
|
||||
}
|
||||
}catch (err) {
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to sync secrets to GitLab");
|
||||
@ -1645,7 +1672,7 @@ const syncSecretsSupabase = async ({
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
const { data: getSecretsRes } = await request.get(
|
||||
const { data: getSecretsRes } = await standardRequest.get(
|
||||
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
|
||||
{
|
||||
headers: {
|
||||
@ -1665,7 +1692,7 @@ const syncSecretsSupabase = async ({
|
||||
}
|
||||
);
|
||||
|
||||
await request.post(
|
||||
await standardRequest.post(
|
||||
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
|
||||
modifiedFormatForSecretInjection,
|
||||
{
|
||||
@ -1683,7 +1710,7 @@ const syncSecretsSupabase = async ({
|
||||
}
|
||||
});
|
||||
|
||||
await request.delete(
|
||||
await standardRequest.delete(
|
||||
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
|
||||
{
|
||||
headers: {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import * as Sentry from "@sentry/node";
|
||||
import {
|
||||
IIntegrationAuth
|
||||
} from '../models';
|
||||
@ -6,7 +5,7 @@ import {
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_GITLAB_API_URL
|
||||
} from '../variables';
|
||||
import request from '../config/request';
|
||||
import { standardRequest } from '../config/request';
|
||||
|
||||
interface Team {
|
||||
name: string;
|
||||
@ -31,21 +30,15 @@ const getTeams = async ({
|
||||
}) => {
|
||||
|
||||
let teams: Team[] = [];
|
||||
try {
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_GITLAB:
|
||||
teams = await getTeamsGitLab({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get integration teams');
|
||||
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_GITLAB:
|
||||
teams = await getTeamsGitLab({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
return teams;
|
||||
}
|
||||
|
||||
@ -63,30 +56,24 @@ const getTeamsGitLab = async ({
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let teams: Team[] = [];
|
||||
try {
|
||||
const res = (await request.get(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/groups`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
const res = (await standardRequest.get(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/groups`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
)).data;
|
||||
|
||||
teams = res.map((t: any) => ({
|
||||
name: t.name,
|
||||
teamId: t.id
|
||||
}));
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get GitLab integration teams");
|
||||
}
|
||||
}
|
||||
)).data;
|
||||
|
||||
teams = res.map((t: any) => ({
|
||||
name: t.name,
|
||||
teamId: t.id
|
||||
}));
|
||||
|
||||
return teams;
|
||||
}
|
||||
|
||||
export {
|
||||
getTeams
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
41
backend/src/interfaces/utils/crypto.ts
Normal file
41
backend/src/interfaces/utils/crypto.ts
Normal file
@ -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;
|
||||
}
|
||||
|
1
backend/src/interfaces/utils/index.ts
Normal file
1
backend/src/interfaces/utils/index.ts
Normal file
@ -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 = (error: RequestError | Error, req, res, next) => {
|
||||
if (res.headersSent) return next();
|
||||
if (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 })
|
||||
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,11 +1,6 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { IntegrationAuth, IWorkspace } from '../models';
|
||||
import { IntegrationService } from '../services';
|
||||
import { validateClientForIntegrationAuth } from '../helpers/integrationAuth';
|
||||
import { validateMembership } from '../helpers/membership';
|
||||
import { UnauthorizedRequestError } from '../utils/errors';
|
||||
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';
|
||||
|
||||
|
@ -26,7 +26,7 @@ const requireMfaAuth = async (
|
||||
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
|
||||
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(AUTH_TOKEN_VALUE, getJwtMfaSecret())
|
||||
jwt.verify(AUTH_TOKEN_VALUE, await getJwtMfaSecret())
|
||||
);
|
||||
|
||||
const user = await User.findOne({
|
||||
|
@ -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';
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user