mirror of
https://github.com/Infisical/infisical.git
synced 2025-04-06 22:14:48 +00:00
Compare commits
678 Commits
folders
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
41dd2fda8a | |||
22ca4f2e92 | |||
5882eb6f8a | |||
c13d5e29f4 | |||
d99c54ca50 | |||
9dd0dac2f9 | |||
98efffafaa | |||
342ee50063 | |||
553cf11ad2 | |||
4616cffecd | |||
39feb9a6ae | |||
82c1f8607d | |||
d4c3cbb53a | |||
1dea6749ba | |||
631eac803e | |||
facabc683b | |||
4b99a9ea93 | |||
445afb397c | |||
7d554f46d5 | |||
bbef7d415c | |||
bb7b398fa7 | |||
570457c7c9 | |||
1b77b1d70b | |||
0f697a91ab | |||
df6d23d1d3 | |||
0187d3012b | |||
4299a76fcd | |||
2bae6cf084 | |||
22beebc5d0 | |||
6cb0a20675 | |||
00fae0023a | |||
0377219a7a | |||
00dfcfcf4e | |||
f5441e9996 | |||
ee2fb33b50 | |||
c51b194ba6 | |||
2920ba5195 | |||
cd837b07aa | |||
a8e71e8170 | |||
5fa96411d6 | |||
329ab8ae61 | |||
3242d9b44e | |||
8ce48fea43 | |||
b011144258 | |||
674828e8e4 | |||
c0563aff77 | |||
7cec42a7fb | |||
78493d9521 | |||
49b3e8b538 | |||
a3fca200fc | |||
158eb584d2 | |||
e8bffb7217 | |||
604810ebd2 | |||
d4108d1fab | |||
4d6ae0eef8 | |||
8193490d7f | |||
0deba5e345 | |||
a2055194c5 | |||
8c0d643a37 | |||
547a1fd142 | |||
04765ffb94 | |||
6b9aa200b5 | |||
5667e47b31 | |||
a8ed187443 | |||
c5be497052 | |||
77d47e071b | |||
4bf2407d13 | |||
846f5c6680 | |||
6f1f07c9a5 | |||
aaca66e5a4 | |||
b9dad5c3f0 | |||
3a79a855cb | |||
e28d0cbace | |||
c0fbe82ecb | |||
b0e7304bff | |||
5a1b6acc93 | |||
5f5ed5d0a9 | |||
bfee0a6d30 | |||
08868681d8 | |||
6dee858154 | |||
0c18bd71c4 | |||
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 | |||
4bda67c9f7 | |||
e5c5e4cca2 | |||
803a97fdfc | |||
9e42a7a33e | |||
7127b60867 | |||
bcba2e9c2c | |||
34c79b08bc | |||
aacdaf4556 | |||
a7484f8be5 | |||
51154925fd | |||
e1bf31b371 | |||
3817831577 | |||
3846c42c00 | |||
03110c8a83 | |||
e0d5644b3a | |||
c7172337ed | |||
7183546e7e | |||
d717430947 | |||
5922921896 | |||
66ce269f42 | |||
f79e1d754d | |||
5a906d412b | |||
1bb3115880 | |||
7d8c6eb6b7 | |||
4dd96704f0 | |||
2e428f9d66 | |||
7a926fbdac | |||
0d3999c7e5 | |||
24913217c6 | |||
c581fde65e | |||
fa9a7301d9 | |||
3add40bfbd | |||
d4206cdbd8 | |||
3adbb7316a | |||
3e022346cd | |||
7fc01df93e | |||
9f944135b9 | |||
afdf971014 | |||
ad5852fe3a | |||
c1b97841cf | |||
b0107d28d4 | |||
9f1f709b57 | |||
dd4c4e1473 | |||
92e04c45e7 | |||
44a7eb8123 | |||
7a2192cf95 | |||
0ad8075197 | |||
b258cbd852 | |||
f1c2512600 | |||
1348c94154 | |||
11ac5d18ff | |||
bb60e1d327 | |||
70668d7783 | |||
be2cf54d6e | |||
acb90ee0f7 | |||
b62ea41e02 | |||
48cd2bddfe | |||
884394866e | |||
44c716aba3 | |||
763ec1aa0f | |||
338d287d35 | |||
df83e8ceb9 | |||
8f08c4955f | |||
d1c62d655d | |||
8e2837c8e8 | |||
aa27308f5a | |||
2d22c96a97 | |||
b4839eaac8 | |||
92df5e1a2f | |||
df2e0e03ff | |||
5585893cfe | |||
e348e4678e | |||
4a36dcd1ed | |||
619fe553ef | |||
4c41a7f1cf | |||
04d46099f6 | |||
250428c64f | |||
d40758a43d | |||
6a3d6ecbe5 | |||
d6ed456ebd | |||
f99bb253df | |||
0c3c15be91 | |||
d9afe90885 | |||
fcb677d990 | |||
5fb7b55fdf | |||
49559fbc5f | |||
12d8e144d1 | |||
3eb810b979 | |||
c1f39b866f | |||
954335bd37 | |||
fe115a7476 | |||
00442992d2 | |||
12e16b4a03 | |||
56c35293eb | |||
d38432e0d6 | |||
cfc9470a6f | |||
3907c99b5b | |||
903560a2d1 | |||
6d8b16fc85 | |||
6b1f704a44 | |||
3dfb85b03f | |||
0b7508b40c | |||
7eae2392fe | |||
b21a8b4574 | |||
3b30095629 | |||
5c15fab46e | |||
806448a7f9 | |||
d824305fd6 | |||
83ddba29e2 | |||
4489adeefa | |||
242f362682 | |||
0a9dc7ac46 | |||
99dd661c56 | |||
1fe1afbb8e | |||
83be9efee8 | |||
1b1cb4a1de | |||
dfa33e63cb | |||
ac8b13116f | |||
810554e13c | |||
3791ba2609 | |||
ed7dbb655c | |||
dda5f75450 | |||
e2c67ffbef | |||
2a64d657d3 | |||
e2139882da | |||
c7a402c4cb | |||
73ddad8dac | |||
8c450d51da | |||
bec80de174 | |||
c768383f7e | |||
9df8e8926d | |||
689ac6a8fe | |||
576381cd58 | |||
8a67549ec5 | |||
5032450b1c | |||
802cb80416 | |||
e0ac12be14 | |||
afa7b35d50 | |||
e5e15d26bf | |||
192e3beb46 | |||
9c3a426cb1 | |||
7e15e733f8 | |||
365daa97a8 | |||
710364e3a1 | |||
f6e23127ac | |||
5855c859e5 | |||
12478130d0 | |||
ecb182ad03 | |||
553703decb | |||
cbf05b7c31 | |||
0fe4a3c033 | |||
e8e8ff5563 | |||
dbe75eeecb | |||
57ee287dd2 | |||
98dcc42db2 | |||
aa108d575d | |||
56cc77e0e8 | |||
c175519d70 | |||
a3093de55b | |||
00b17d250e | |||
eb94ad5ba4 | |||
a3b35a9228 | |||
5bbe09e4be | |||
a7880db871 | |||
db8ce00536 | |||
d54753289a | |||
c40546945f | |||
5508434563 | |||
a3b2d1c838 | |||
35d345f17e | |||
aa53de9070 | |||
b7142a1f24 | |||
cca9476975 | |||
7ec5f0d342 | |||
bdb71d1051 | |||
278f1caa19 | |||
dd10bf1702 | |||
7c33b6159f | |||
510d5f0ffd | |||
84d46a428c | |||
520c294e45 | |||
c797901778 | |||
9c18adf35f | |||
6d7628cdc0 | |||
381652cbb2 | |||
acc0198637 | |||
6af326685b | |||
91c83e04be | |||
c9e12d33bd | |||
9e2a03244e | |||
2a8a90c0c5 | |||
54e099f8a8 | |||
88c0a46de3 | |||
356f0ac860 | |||
49a690b7b2 | |||
830368b812 | |||
d19c2936e6 | |||
592cef67bc | |||
676d0a7bf2 | |||
5149f526d4 | |||
4875cac4ef | |||
729aacc154 | |||
68deea28b7 | |||
d36d7bfce6 | |||
1c4649cc9e | |||
77e537c35d | |||
49f26d591b | |||
94b18e6fc4 | |||
0008ff9a98 | |||
5cb6c663bb | |||
a90375ea3d | |||
9cf921bb1c | |||
5ec1a1eedf | |||
6cb036bb38 | |||
30472505ce | |||
fef1adb34f | |||
93d07c34ab | |||
3a0ce7c084 | |||
4cce75d128 | |||
b42c33107e | |||
686d3c409d | |||
8fb473c57c | |||
ebdcccb6ca | |||
ba240f9e29 | |||
95bb9e2586 | |||
273f4228d7 | |||
c8b6eb0d6c | |||
fc41be9db8 | |||
164da9d8e0 | |||
767943368e | |||
e37f584d75 | |||
b2663fb3e0 | |||
4899c4de5b | |||
744caf8c79 | |||
da888e27ad | |||
28369411f7 | |||
b7a1689aeb | |||
c034b62b71 | |||
a6b9400a4a | |||
aa5d761081 | |||
2cc8e59ca8 | |||
a031e84ab8 | |||
e2df6e94a5 | |||
9db69430b5 | |||
00feee6903 | |||
10dd747899 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
backend/node_modules
|
||||
frontend/node_modules
|
22
.env.example
22
.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= # required
|
||||
SMTP_USERNAME= # required
|
||||
SMTP_PASSWORD= # required
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_FROM_ADDRESS= # required
|
||||
SMTP_FROM_NAME=Infisical
|
||||
# Mail/SMTP
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=
|
||||
SMTP_NAME=
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
|
||||
# Integration
|
||||
# Optional only if integration is used
|
||||
@ -66,4 +67,7 @@ STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_PRODUCT_STARTER=
|
||||
STRIPE_PRODUCT_TEAM=
|
||||
STRIPE_PRODUCT_PRO=
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
||||
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 Width: | Height: | 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 Width: | Height: | Size: 28 KiB |
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
@ -1,6 +1,6 @@
|
||||
# Description 📣
|
||||
|
||||
*Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.*
|
||||
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||
|
||||
## Type ✨
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
# Tests 🛠️
|
||||
|
||||
*Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. You may want to add screenshots when relevant and possible*
|
||||
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. You may want to add screenshots when relevant and possible -->
|
||||
|
||||
```sh
|
||||
# Here's some code block to paste some code snippets
|
||||
|
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
|
||||
|
35
.github/workflows/docker-image.yml
vendored
35
.github/workflows/docker-image.yml
vendored
@ -1,14 +1,25 @@
|
||||
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
|
||||
run: npm ci --only-production
|
||||
working-directory: backend
|
||||
- name: 🧪 Run tests
|
||||
run: npm run test:ci
|
||||
working-directory: backend
|
||||
- name: Save commit hashes for tag
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
@ -45,15 +56,19 @@ jobs:
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
push: true
|
||||
context: backend
|
||||
tags: infisical/backend:${{ steps.commit.outputs.short }},
|
||||
infisical/backend:latest
|
||||
tags: |
|
||||
infisical/backend:${{ steps.commit.outputs.short }}
|
||||
infisical/backend:latest
|
||||
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
|
||||
@ -94,8 +109,10 @@ jobs:
|
||||
push: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
context: frontend
|
||||
tags: infisical/frontend:${{ steps.commit.outputs.short }},
|
||||
infisical/frontend:latest
|
||||
tags: |
|
||||
infisical/frontend:${{ steps.commit.outputs.short }}
|
||||
infisical/frontend:latest
|
||||
infisical/frontend:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
@ -122,7 +139,7 @@ jobs:
|
||||
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||
- name: Save DigitalOcean kubeconfig with short-lived credentials
|
||||
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179
|
||||
- name: switch to gamma namespace
|
||||
- name: switch to gamma namespace
|
||||
run: kubectl config set-context --current --namespace=gamma
|
||||
- name: test kubectl
|
||||
run: kubectl get ingress
|
||||
@ -135,4 +152,4 @@ jobs:
|
||||
exit 1
|
||||
else
|
||||
echo "Helm upgrade was successful"
|
||||
fi
|
||||
fi
|
||||
|
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
|
107
Dockerfile.standalone-infisical
Normal file
107
Dockerfile.standalone-infisical
Normal file
@ -0,0 +1,107 @@
|
||||
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
|
||||
ARG INTERCOM_ID
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
||||
|
||||
# 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
|
||||
ARG INTERCOM_ID
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
||||
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
||||
|
||||
COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts
|
||||
COPY --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"]
|
||||
|
||||
|
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.
|
6131
backend/package-lock.json
generated
6131
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,39 +1,45 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-secrets-manager": "^3.267.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.39.0",
|
||||
"@sentry/tracing": "^7.19.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.1311.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",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"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.7.2",
|
||||
"mongoose": "^6.10.5",
|
||||
"nanoid": "^3.3.6",
|
||||
"node-cache": "^5.1.2",
|
||||
"nodemailer": "^6.8.0",
|
||||
"posthog-node": "^2.5.4",
|
||||
"passport": "^0.6.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"posthog-node": "^2.6.0",
|
||||
"query-string": "^7.1.3",
|
||||
"request-ip": "^3.3.0",
|
||||
"rate-limit-mongo": "^2.3.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stripe": "^10.7.0",
|
||||
"swagger-autogen": "^2.22.0",
|
||||
@ -52,12 +58,12 @@
|
||||
"start": "node build/index.js",
|
||||
"dev": "nodemon",
|
||||
"swagger-autogen": "node ./swagger/index.ts",
|
||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
|
||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build && cp -R ./src/data ./build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint-and-fix": "eslint . --ext .ts --fix",
|
||||
"lint-staged": "lint-staged",
|
||||
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
|
||||
"test": "cross-env NODE_ENV=test jest --testTimeout=10000 --detectOpenHandles",
|
||||
"test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles; npm run posttest",
|
||||
"test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json",
|
||||
"posttest": "docker compose -f test-resources/docker-compose.test.yml down"
|
||||
},
|
||||
@ -80,11 +86,12 @@
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/jest": "^29.2.4",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.3",
|
||||
"@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",
|
||||
|
@ -1532,7 +1532,15 @@
|
||||
"/api/v1/invite-org/signup": {
|
||||
"post": {
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "host",
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
@ -2071,6 +2079,15 @@
|
||||
"targetEnvironment": {
|
||||
"example": "any"
|
||||
},
|
||||
"targetEnvironmentId": {
|
||||
"example": "any"
|
||||
},
|
||||
"targetService": {
|
||||
"example": "any"
|
||||
},
|
||||
"targetServiceId": {
|
||||
"example": "any"
|
||||
},
|
||||
"owner": {
|
||||
"example": "any"
|
||||
},
|
||||
@ -2297,6 +2314,13 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "teamId",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@ -2309,6 +2333,107 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/integration-auth/{integrationAuthId}/teams": {
|
||||
"get": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "integrationAuthId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/integration-auth/{integrationAuthId}/vercel/branches": {
|
||||
"get": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "integrationAuthId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "appId",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/integration-auth/{integrationAuthId}/railway/environments": {
|
||||
"get": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "integrationAuthId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "appId",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/integration-auth/{integrationAuthId}/railway/services": {
|
||||
"get": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "integrationAuthId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "appId",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/signup/complete-account/signup": {
|
||||
"post": {
|
||||
"description": "",
|
||||
@ -2870,9 +2995,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
@ -2882,6 +3004,26 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/organizations/{organizationId}/service-accounts": {
|
||||
"get": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "organizationId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/workspace/{workspaceId}/environments": {
|
||||
"post": {
|
||||
"description": "",
|
||||
@ -4018,9 +4160,6 @@
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
@ -4073,6 +4212,138 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/service-accounts/me": {
|
||||
"get": {
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/service-accounts/{serviceAccountId}": {
|
||||
"get": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "serviceAccountId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "serviceAccountId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/service-accounts/": {
|
||||
"post": {
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/service-accounts/{serviceAccountId}/name": {
|
||||
"patch": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "serviceAccountId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"example": "any"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/service-accounts/{serviceAccountId}/permissions/workspace": {
|
||||
"get": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "serviceAccountId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "serviceAccountId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
@ -4080,6 +4351,90 @@
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"environment": {
|
||||
"example": "any"
|
||||
},
|
||||
"workspaceId": {
|
||||
"example": "any"
|
||||
},
|
||||
"read": {
|
||||
"example": "any"
|
||||
},
|
||||
"write": {
|
||||
"example": "any"
|
||||
},
|
||||
"encryptedKey": {
|
||||
"example": "any"
|
||||
},
|
||||
"nonce": {
|
||||
"example": "any"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/service-accounts/{serviceAccountId}/permissions/workspace/{serviceAccountWorkspacePermissionId}": {
|
||||
"delete": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "serviceAccountId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "serviceAccountWorkspacePermissionId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/service-accounts/{serviceAccountId}/keys": {
|
||||
"get": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "serviceAccountId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "workspaceId",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -4149,6 +4504,297 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v3/secrets/": {
|
||||
"get": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "workspaceId",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "environment",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v3/secrets/{secretName}": {
|
||||
"post": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "secretName",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaceId": {
|
||||
"example": "any"
|
||||
},
|
||||
"environment": {
|
||||
"example": "any"
|
||||
},
|
||||
"type": {
|
||||
"example": "any"
|
||||
},
|
||||
"secretKeyCiphertext": {
|
||||
"example": "any"
|
||||
},
|
||||
"secretKeyIV": {
|
||||
"example": "any"
|
||||
},
|
||||
"secretKeyTag": {
|
||||
"example": "any"
|
||||
},
|
||||
"secretValueCiphertext": {
|
||||
"example": "any"
|
||||
},
|
||||
"secretValueIV": {
|
||||
"example": "any"
|
||||
},
|
||||
"secretValueTag": {
|
||||
"example": "any"
|
||||
},
|
||||
"secretCommentCiphertext": {
|
||||
"example": "any"
|
||||
},
|
||||
"secretCommentIV": {
|
||||
"example": "any"
|
||||
},
|
||||
"secretCommentTag": {
|
||||
"example": "any"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "secretName",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "workspaceId",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "environment",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "secretName",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaceId": {
|
||||
"example": "any"
|
||||
},
|
||||
"environment": {
|
||||
"example": "any"
|
||||
},
|
||||
"type": {
|
||||
"example": "any"
|
||||
},
|
||||
"secretValueCiphertext": {
|
||||
"example": "any"
|
||||
},
|
||||
"secretValueIV": {
|
||||
"example": "any"
|
||||
},
|
||||
"secretValueTag": {
|
||||
"example": "any"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "secretName",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaceId": {
|
||||
"example": "any"
|
||||
},
|
||||
"environment": {
|
||||
"example": "any"
|
||||
},
|
||||
"type": {
|
||||
"example": "any"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v3/workspaces/{workspaceId}/secrets/blind-index-status": {
|
||||
"get": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "workspaceId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v3/workspaces/{workspaceId}/secrets": {
|
||||
"get": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "workspaceId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v3/workspaces/{workspaceId}/secrets/names": {
|
||||
"post": {
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "workspaceId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secretsToUpdate": {
|
||||
"example": "any"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/status": {
|
||||
"get": {
|
||||
"description": "",
|
||||
|
@ -1,51 +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')!;
|
||||
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
|
||||
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 ((await client.getSecret('HTTPS_ENABLED')).secretValue == undefined || (await client.getSecret('HTTPS_ENABLED')).secretValue == "") {
|
||||
// default when no value present
|
||||
return 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;
|
@ -1,29 +1,39 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response } from 'express';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import * as bigintConversion from 'bigint-conversion';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const jsrp = require('jsrp');
|
||||
import { User, LoginSRPDetail } from '../../models';
|
||||
import {
|
||||
User,
|
||||
LoginSRPDetail,
|
||||
TokenVersion
|
||||
} from '../../models';
|
||||
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
|
||||
import { checkUserDevice } from '../../helpers/user';
|
||||
import {
|
||||
ACTION_LOGIN,
|
||||
ACTION_LOGOUT
|
||||
ACTION_LOGOUT,
|
||||
AUTH_MODE_JWT
|
||||
} from '../../variables';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
import {
|
||||
BadRequestError,
|
||||
UnauthorizedRequestError
|
||||
} from '../../utils/errors';
|
||||
import { EELogService } from '../../ee/services';
|
||||
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
|
||||
import { getChannelFromUserAgent } from '../../utils/posthog';
|
||||
import {
|
||||
getNodeEnv,
|
||||
getJwtRefreshSecret,
|
||||
getJwtAuthLifetime,
|
||||
getJwtAuthSecret
|
||||
getJwtAuthSecret,
|
||||
getHttpsEnabled
|
||||
} from '../../config';
|
||||
|
||||
declare module 'jsonwebtoken' {
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
userId: string;
|
||||
refreshVersion?: number;
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,47 +44,39 @@ declare module 'jsonwebtoken' {
|
||||
* @returns
|
||||
*/
|
||||
export const login1 = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
email,
|
||||
clientPublicKey
|
||||
}: { email: string; clientPublicKey: string } = req.body;
|
||||
const {
|
||||
email,
|
||||
clientPublicKey
|
||||
}: { email: string; clientPublicKey: string } = req.body;
|
||||
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select('+salt +verifier');
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select('+salt +verifier');
|
||||
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier
|
||||
},
|
||||
async () => {
|
||||
// generate server-side public key
|
||||
const serverPublicKey = server.getPublicKey();
|
||||
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: email,
|
||||
clientPublicKey: clientPublicKey,
|
||||
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||
}, { upsert: true, returnNewDocument: false })
|
||||
await LoginSRPDetail.findOneAndReplace({ email: email }, {
|
||||
email: email,
|
||||
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'
|
||||
});
|
||||
}
|
||||
return res.status(200).send({
|
||||
serverPublicKey,
|
||||
salt: user.salt
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -85,84 +87,80 @@ export const login1 = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const login2 = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email, clientProof } = req.body;
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
||||
const { email, clientProof } = req.body;
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
||||
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
|
||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email })
|
||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email })
|
||||
|
||||
if (!loginSRPDetailFromDB) {
|
||||
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
||||
}
|
||||
if (!loginSRPDetailFromDB) {
|
||||
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
||||
}
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier,
|
||||
b: loginSRPDetailFromDB.serverBInt
|
||||
},
|
||||
async () => {
|
||||
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier,
|
||||
b: loginSRPDetailFromDB.serverBInt
|
||||
},
|
||||
async () => {
|
||||
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
||||
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
// issue tokens
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
// issue tokens
|
||||
|
||||
await checkUserDevice({
|
||||
user,
|
||||
ip: req.ip,
|
||||
userAgent: req.headers['user-agent'] ?? ''
|
||||
});
|
||||
await checkUserDevice({
|
||||
user,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers['user-agent'] ?? ''
|
||||
});
|
||||
|
||||
const tokens = await issueAuthTokens({ userId: user._id.toString() });
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers['user-agent'] ?? ''
|
||||
});
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: getNodeEnv() === 'production' ? true : false
|
||||
});
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
|
||||
const loginAction = await EELogService.createAction({
|
||||
name: ACTION_LOGIN,
|
||||
userId: user._id
|
||||
});
|
||||
|
||||
loginAction && await EELogService.createLog({
|
||||
userId: user._id,
|
||||
actions: [loginAction],
|
||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
// return (access) token in response
|
||||
return res.status(200).send({
|
||||
token: tokens.token,
|
||||
publicKey: user.publicKey,
|
||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
tag: user.tag
|
||||
});
|
||||
}
|
||||
const loginAction = await EELogService.createAction({
|
||||
name: ACTION_LOGIN,
|
||||
userId: user._id
|
||||
});
|
||||
|
||||
return res.status(400).send({
|
||||
message: 'Failed to authenticate. Try again?'
|
||||
loginAction && await EELogService.createLog({
|
||||
userId: user._id,
|
||||
actions: [loginAction],
|
||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
// return (access) token in response
|
||||
return res.status(200).send({
|
||||
token: tokens.token,
|
||||
publicKey: user.publicKey,
|
||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
tag: user.tag
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to authenticate. Try again?'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: 'Failed to authenticate. Try again?'
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -172,44 +170,59 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const logout = async (req: Request, res: Response) => {
|
||||
try {
|
||||
await clearTokens({
|
||||
userId: req.user._id.toString()
|
||||
});
|
||||
|
||||
// clear httpOnly cookie
|
||||
res.cookie('jid', '', {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: getNodeEnv() === 'production' ? true : false
|
||||
});
|
||||
|
||||
const logoutAction = await EELogService.createAction({
|
||||
name: ACTION_LOGOUT,
|
||||
userId: req.user._id
|
||||
});
|
||||
|
||||
logoutAction && await EELogService.createLog({
|
||||
userId: req.user._id,
|
||||
actions: [logoutAction],
|
||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to logout'
|
||||
});
|
||||
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
|
||||
await clearTokens(req.authData.tokenVersionId)
|
||||
}
|
||||
|
||||
// clear httpOnly cookie
|
||||
res.cookie('jid', '', {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: (await getHttpsEnabled()) as boolean
|
||||
});
|
||||
|
||||
const logoutAction = await EELogService.createAction({
|
||||
name: ACTION_LOGOUT,
|
||||
userId: req.user._id
|
||||
});
|
||||
|
||||
logoutAction && await EELogService.createLog({
|
||||
userId: req.user._id,
|
||||
actions: [logoutAction],
|
||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully logged out.'
|
||||
});
|
||||
};
|
||||
|
||||
export const getCommonPasswords = async (req: Request, res: Response) => {
|
||||
const commonPasswords = fs.readFileSync(
|
||||
path.resolve(__dirname, '../../data/' + 'common_passwords.txt'),
|
||||
'utf8'
|
||||
).split('\n');
|
||||
|
||||
return res.status(200).send(commonPasswords);
|
||||
}
|
||||
|
||||
export const revokeAllSessions = async (req: Request, res: Response) => {
|
||||
await TokenVersion.updateMany({
|
||||
user: req.user._id
|
||||
}, {
|
||||
$inc: {
|
||||
refreshVersion: 1,
|
||||
accessVersion: 1
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully revoked all sessions.'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return user is authenticated
|
||||
* @param req
|
||||
@ -223,47 +236,55 @@ export const checkAuth = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return new token by redeeming refresh token
|
||||
* Return new JWT access token by first validating the refresh token
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getNewToken = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const refreshToken = req.cookies.jid;
|
||||
const refreshToken = req.cookies.jid;
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new Error('Failed to find token in request cookies');
|
||||
}
|
||||
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(refreshToken, getJwtRefreshSecret())
|
||||
);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: decodedToken.userId
|
||||
}).select('+publicKey');
|
||||
|
||||
if (!user) throw new Error('Failed to authenticate unfound user');
|
||||
if (!user?.publicKey)
|
||||
throw new Error('Failed to authenticate not fully set up account');
|
||||
|
||||
const token = createToken({
|
||||
payload: {
|
||||
userId: decodedToken.userId
|
||||
},
|
||||
expiresIn: getJwtAuthLifetime(),
|
||||
secret: getJwtAuthSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
token
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Invalid request'
|
||||
});
|
||||
if (!refreshToken) {
|
||||
throw new Error('Failed to find refresh token in request cookies');
|
||||
}
|
||||
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(refreshToken, await getJwtRefreshSecret())
|
||||
);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: decodedToken.userId
|
||||
}).select('+publicKey +refreshVersion +accessVersion');
|
||||
|
||||
if (!user) throw new Error('Failed to authenticate unfound user');
|
||||
if (!user?.publicKey)
|
||||
throw new Error('Failed to authenticate not fully set up account');
|
||||
|
||||
const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId);
|
||||
|
||||
if (!tokenVersion) throw UnauthorizedRequestError({
|
||||
message: 'Failed to validate refresh token'
|
||||
});
|
||||
|
||||
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({
|
||||
message: 'Failed to validate refresh token'
|
||||
});
|
||||
|
||||
const token = createToken({
|
||||
payload: {
|
||||
userId: decodedToken.userId,
|
||||
tokenVersionId: tokenVersion._id.toString(),
|
||||
accessVersion: tokenVersion.refreshVersion
|
||||
},
|
||||
expiresIn: await getJwtAuthLifetime(),
|
||||
secret: await getJwtAuthSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
token
|
||||
});
|
||||
};
|
||||
|
||||
export const handleAuthProviderCallback = (req: Request, res: Response) => {
|
||||
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { Bot, BotKey } from '../../models';
|
||||
import { createBot } from '../../helpers/bot';
|
||||
|
||||
@ -16,33 +16,24 @@ interface BotKey {
|
||||
* @returns
|
||||
*/
|
||||
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
||||
let bot;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
bot = await Bot.findOne({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
// case: bot doesn't exist for workspace with id [workspaceId]
|
||||
// -> create a new bot and return it
|
||||
bot = await createBot({
|
||||
name: 'Infisical Bot',
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get bot for workspace'
|
||||
});
|
||||
}
|
||||
let bot = await Bot.findOne({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
// case: bot doesn't exist for workspace with id [workspaceId]
|
||||
// -> create a new bot and return it
|
||||
bot = await createBot({
|
||||
name: 'Infisical Bot',
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
bot
|
||||
});
|
||||
return res.status(200).send({
|
||||
bot
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -52,54 +43,44 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const setBotActiveState = async (req: Request, res: Response) => {
|
||||
let bot;
|
||||
try {
|
||||
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
|
||||
|
||||
if (isActive) {
|
||||
// bot state set to active -> share workspace key with bot
|
||||
if (!botKey?.encryptedKey || !botKey?.nonce) {
|
||||
return res.status(400).send({
|
||||
message: 'Failed to set bot state to active - missing bot key'
|
||||
});
|
||||
}
|
||||
|
||||
await BotKey.findOneAndUpdate({
|
||||
workspace: req.bot.workspace
|
||||
}, {
|
||||
encryptedKey: botKey.encryptedKey,
|
||||
nonce: botKey.nonce,
|
||||
sender: req.user._id,
|
||||
bot: req.bot._id,
|
||||
workspace: req.bot.workspace
|
||||
}, {
|
||||
upsert: true,
|
||||
new: true
|
||||
});
|
||||
} else {
|
||||
// case: bot state set to inactive -> delete bot's workspace key
|
||||
await BotKey.deleteOne({
|
||||
bot: req.bot._id
|
||||
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
|
||||
|
||||
if (isActive) {
|
||||
// bot state set to active -> share workspace key with bot
|
||||
if (!botKey?.encryptedKey || !botKey?.nonce) {
|
||||
return res.status(400).send({
|
||||
message: 'Failed to set bot state to active - missing bot key'
|
||||
});
|
||||
}
|
||||
|
||||
bot = await Bot.findOneAndUpdate({
|
||||
_id: req.bot._id
|
||||
|
||||
await BotKey.findOneAndUpdate({
|
||||
workspace: req.bot.workspace
|
||||
}, {
|
||||
isActive
|
||||
encryptedKey: botKey.encryptedKey,
|
||||
nonce: botKey.nonce,
|
||||
sender: req.user._id,
|
||||
bot: req.bot._id,
|
||||
workspace: req.bot.workspace
|
||||
}, {
|
||||
upsert: true,
|
||||
new: true
|
||||
});
|
||||
|
||||
if (!bot) throw new Error('Failed to update bot active state');
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to update bot active state'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// case: bot state set to inactive -> delete bot's workspace key
|
||||
await BotKey.deleteOne({
|
||||
bot: req.bot._id
|
||||
});
|
||||
}
|
||||
|
||||
let bot = await Bot.findOneAndUpdate({
|
||||
_id: req.bot._id
|
||||
}, {
|
||||
isActive
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
|
||||
if (!bot) throw new Error('Failed to update bot active state');
|
||||
|
||||
return res.status(200).send({
|
||||
bot
|
||||
|
@ -14,7 +14,6 @@ import * as stripeController from './stripeController';
|
||||
import * as userActionController from './userActionController';
|
||||
import * as userController from './userController';
|
||||
import * as workspaceController from './workspaceController';
|
||||
import * as secretsFolderController from './secretsFolderController'
|
||||
|
||||
export {
|
||||
authController,
|
||||
@ -32,6 +31,5 @@ export {
|
||||
stripeController,
|
||||
userActionController,
|
||||
userController,
|
||||
workspaceController,
|
||||
secretsFolderController
|
||||
workspaceController
|
||||
};
|
||||
|
@ -1,45 +1,40 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import * as Sentry from '@sentry/node';
|
||||
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,
|
||||
getTeams,
|
||||
revokeAccess
|
||||
} from '../../integrations';
|
||||
import {
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_RAILWAY_API_URL
|
||||
} from '../../variables';
|
||||
import { standardRequest } from '../../config/request';
|
||||
|
||||
/***
|
||||
* Return integration authorization with id [integrationAuthId]
|
||||
*/
|
||||
export const getIntegrationAuth = async (req: Request, res: Response) => {
|
||||
let integrationAuth;
|
||||
try {
|
||||
const { integrationAuthId } = req.params;
|
||||
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) return res.status(400).send({
|
||||
message: 'Failed to find integration authorization'
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get integration authorization'
|
||||
});
|
||||
}
|
||||
|
||||
const { integrationAuthId } = req.params;
|
||||
const integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) return res.status(400).send({
|
||||
message: 'Failed to find integration authorization'
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
integrationAuth
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
@ -56,33 +51,25 @@ export const oAuthExchange = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { workspaceId, code, integration } = req.body;
|
||||
if (!INTEGRATION_SET.has(integration))
|
||||
throw new Error('Failed to validate integration');
|
||||
|
||||
const environments = req.membership.workspace?.environments || [];
|
||||
if(environments.length === 0){
|
||||
throw new Error("Failed to get environments")
|
||||
}
|
||||
|
||||
const integrationAuth = await IntegrationService.handleOAuthExchange({
|
||||
workspaceId,
|
||||
integration,
|
||||
code,
|
||||
environment: environments[0].slug,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
integrationAuth
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get OAuth2 code-token exchange'
|
||||
});
|
||||
}
|
||||
const { workspaceId, code, integration } = req.body;
|
||||
if (!INTEGRATION_SET.has(integration))
|
||||
throw new Error('Failed to validate integration');
|
||||
|
||||
const environments = req.membership.workspace?.environments || [];
|
||||
if(environments.length === 0){
|
||||
throw new Error("Failed to get environments")
|
||||
}
|
||||
|
||||
const integrationAuth = await IntegrationService.handleOAuthExchange({
|
||||
workspaceId,
|
||||
integration,
|
||||
code,
|
||||
environment: environments[0].slug,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
integrationAuth
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -99,53 +86,53 @@ export const saveIntegrationAccessToken = async (
|
||||
// TODO: check if access token is valid for each integration
|
||||
|
||||
let integrationAuth;
|
||||
try {
|
||||
const {
|
||||
workspaceId,
|
||||
accessId,
|
||||
accessToken,
|
||||
integration
|
||||
}: {
|
||||
workspaceId: string;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
integration: string;
|
||||
} = req.body;
|
||||
const {
|
||||
workspaceId,
|
||||
accessId,
|
||||
accessToken,
|
||||
url,
|
||||
namespace,
|
||||
integration
|
||||
}: {
|
||||
workspaceId: string;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
url: string;
|
||||
namespace: string;
|
||||
integration: string;
|
||||
} = req.body;
|
||||
|
||||
const bot = await Bot.findOne({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!bot) throw new Error('Bot must be enabled to save integration access token');
|
||||
const bot = await Bot.findOne({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!bot) throw new Error('Bot must be enabled to save integration access token');
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
integration
|
||||
}, {
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
integration
|
||||
}, {
|
||||
new: true,
|
||||
upsert: true
|
||||
});
|
||||
|
||||
// encrypt and save integration access details
|
||||
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
accessId,
|
||||
accessToken,
|
||||
accessExpiresAt: undefined
|
||||
});
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to save integration access token');
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to save access token for integration'
|
||||
});
|
||||
}
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
integration
|
||||
}, {
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
integration,
|
||||
url,
|
||||
namespace,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
}, {
|
||||
new: true,
|
||||
upsert: true
|
||||
});
|
||||
|
||||
// encrypt and save integration access details
|
||||
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
accessId,
|
||||
accessToken,
|
||||
accessExpiresAt: undefined
|
||||
});
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to save integration access token');
|
||||
|
||||
return res.status(200).send({
|
||||
integrationAuth
|
||||
@ -159,22 +146,13 @@ export const saveIntegrationAccessToken = async (
|
||||
* @returns
|
||||
*/
|
||||
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||
let apps;
|
||||
try {
|
||||
const teamId = req.query.teamId as string;
|
||||
|
||||
apps = await getApps({
|
||||
integrationAuth: req.integrationAuth,
|
||||
accessToken: req.accessToken,
|
||||
...teamId && { teamId }
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to get integration authorization applications",
|
||||
});
|
||||
}
|
||||
const teamId = req.query.teamId as string;
|
||||
|
||||
const apps = await getApps({
|
||||
integrationAuth: req.integrationAuth,
|
||||
accessToken: req.accessToken,
|
||||
...teamId && { teamId }
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
apps
|
||||
@ -188,22 +166,203 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
|
||||
let teams;
|
||||
try {
|
||||
teams = await getTeams({
|
||||
integrationAuth: req.integrationAuth,
|
||||
accessToken: req.accessToken
|
||||
const teams = await getTeams({
|
||||
integrationAuth: req.integrationAuth,
|
||||
accessToken: req.accessToken
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
teams
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of available Vercel (preview) branches for Vercel project with
|
||||
* id [appId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getIntegrationAuthVercelBranches = async (req: Request, res: Response) => {
|
||||
const { integrationAuthId } = req.params;
|
||||
const appId = req.query.appId as string;
|
||||
|
||||
interface VercelBranch {
|
||||
ref: string;
|
||||
lastCommit: string;
|
||||
isProtected: boolean;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
projectId: appId,
|
||||
...(req.integrationAuth.teamId ? {
|
||||
teamId: req.integrationAuth.teamId
|
||||
} : {})
|
||||
});
|
||||
|
||||
let branches: string[] = [];
|
||||
|
||||
if (appId && appId !== '') {
|
||||
const { data }: { data: VercelBranch[] } = await standardRequest.get(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v1/integrations/git-branches`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${req.accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
branches = data.map((b) => b.ref);
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
branches
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of Railway environments for Railway project with
|
||||
* id [appId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: Response) => {
|
||||
const { integrationAuthId } = req.params;
|
||||
const appId = req.query.appId as string;
|
||||
|
||||
interface RailwayEnvironment {
|
||||
node: {
|
||||
id: string;
|
||||
name: string;
|
||||
isEphemeral: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
interface Environment {
|
||||
environmentId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
let environments: Environment[] = [];
|
||||
|
||||
if (appId && appId !== '') {
|
||||
const query = `
|
||||
query GetEnvironments($projectId: String!, $after: String, $before: String, $first: Int, $isEphemeral: Boolean, $last: Int) {
|
||||
environments(projectId: $projectId, after: $after, before: $before, first: $first, isEphemeral: $isEphemeral, last: $last) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
isEphemeral
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const variables = {
|
||||
projectId: appId
|
||||
}
|
||||
|
||||
const { data: { data: { environments: { edges } } } } = await standardRequest.post(INTEGRATION_RAILWAY_API_URL, {
|
||||
query,
|
||||
variables,
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${req.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to get integration authorization teams"
|
||||
|
||||
environments = edges.map((e: RailwayEnvironment) => {
|
||||
return ({
|
||||
name: e.node.name,
|
||||
environmentId: e.node.id
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
teams
|
||||
environments
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of Railway services for Railway project with id
|
||||
* [appId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getIntegrationAuthRailwayServices = async (req: Request, res: Response) => {
|
||||
const { integrationAuthId } = req.params;
|
||||
const appId = req.query.appId as string;
|
||||
|
||||
interface RailwayService {
|
||||
node: {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface Service {
|
||||
name: string;
|
||||
serviceId: string;
|
||||
}
|
||||
|
||||
let services: Service[] = [];
|
||||
|
||||
const query = `
|
||||
query project($id: String!) {
|
||||
project(id: $id) {
|
||||
createdAt
|
||||
deletedAt
|
||||
id
|
||||
description
|
||||
expiredAt
|
||||
isPublic
|
||||
isTempProject
|
||||
isUpdatable
|
||||
name
|
||||
prDeploys
|
||||
teamId
|
||||
updatedAt
|
||||
upstreamUrl
|
||||
services {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
if (appId && appId !== '') {
|
||||
const variables = {
|
||||
id: appId
|
||||
}
|
||||
|
||||
const { data: { data: { project: { services: { edges } } } } } = await standardRequest.post(INTEGRATION_RAILWAY_API_URL, {
|
||||
query,
|
||||
variables
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${req.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
services = edges.map((e: RailwayService) => ({
|
||||
name: e.node.name,
|
||||
serviceId: e.node.id
|
||||
}));
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
services
|
||||
});
|
||||
}
|
||||
|
||||
@ -214,19 +373,10 @@ export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
||||
let integrationAuth;
|
||||
try {
|
||||
integrationAuth = await revokeAccess({
|
||||
integrationAuth: req.integrationAuth,
|
||||
accessToken: req.accessToken,
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to delete integration authorization",
|
||||
});
|
||||
}
|
||||
const integrationAuth = await revokeAccess({
|
||||
integrationAuth: req.integrationAuth,
|
||||
accessToken: req.accessToken,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
integrationAuth,
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Integration
|
||||
} from '../../models';
|
||||
@ -14,54 +13,50 @@ import { eventPushSecrets } from '../../events';
|
||||
* @returns
|
||||
*/
|
||||
export const createIntegration = async (req: Request, res: Response) => {
|
||||
let integration;
|
||||
const {
|
||||
integrationAuthId,
|
||||
app,
|
||||
appId,
|
||||
isActive,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region
|
||||
} = req.body;
|
||||
|
||||
// TODO: validate [sourceEnvironment] and [targetEnvironment]
|
||||
|
||||
try {
|
||||
const {
|
||||
integrationAuthId,
|
||||
app,
|
||||
appId,
|
||||
isActive,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
owner,
|
||||
path,
|
||||
region
|
||||
} = req.body;
|
||||
|
||||
// TODO: validate [sourceEnvironment] and [targetEnvironment]
|
||||
|
||||
// initialize new integration after saving integration access token
|
||||
integration = await new Integration({
|
||||
workspace: req.integrationAuth.workspace._id,
|
||||
environment: sourceEnvironment,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
integration: req.integrationAuth.integration,
|
||||
integrationAuth: new Types.ObjectId(integrationAuthId)
|
||||
}).save();
|
||||
|
||||
if (integration) {
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: integration.workspace.toString()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to create integration'
|
||||
});
|
||||
}
|
||||
// initialize new integration after saving integration access token
|
||||
const integration = await new Integration({
|
||||
workspace: req.integrationAuth.workspace._id,
|
||||
environment: sourceEnvironment,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
integration: req.integrationAuth.integration,
|
||||
integrationAuth: new Types.ObjectId(integrationAuthId)
|
||||
}).save();
|
||||
|
||||
if (integration) {
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: integration.workspace,
|
||||
environment: sourceEnvironment
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
integration,
|
||||
@ -75,51 +70,43 @@ export const createIntegration = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const updateIntegration = async (req: Request, res: Response) => {
|
||||
let integration;
|
||||
|
||||
// TODO: add integration-specific validation to ensure that each
|
||||
// integration has the correct fields populated in [Integration]
|
||||
|
||||
try {
|
||||
const {
|
||||
const {
|
||||
environment,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner, // github-specific integration param
|
||||
} = req.body;
|
||||
|
||||
const integration = await Integration.findOneAndUpdate(
|
||||
{
|
||||
_id: req.integration._id,
|
||||
},
|
||||
{
|
||||
environment,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner, // github-specific integration param
|
||||
} = req.body;
|
||||
|
||||
integration = await Integration.findOneAndUpdate(
|
||||
{
|
||||
_id: req.integration._id,
|
||||
},
|
||||
{
|
||||
environment,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner,
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (integration) {
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: integration.workspace.toString(),
|
||||
}),
|
||||
});
|
||||
owner,
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to update integration",
|
||||
);
|
||||
|
||||
if (integration) {
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: integration.workspace,
|
||||
environment
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@ -136,22 +123,13 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const deleteIntegration = async (req: Request, res: Response) => {
|
||||
let integration;
|
||||
try {
|
||||
const { integrationId } = req.params;
|
||||
const { integrationId } = req.params;
|
||||
|
||||
integration = await Integration.findOneAndDelete({
|
||||
_id: integrationId,
|
||||
});
|
||||
const integration = await Integration.findOneAndDelete({
|
||||
_id: integrationId,
|
||||
});
|
||||
|
||||
if (!integration) throw new Error("Failed to find integration");
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to delete integration",
|
||||
});
|
||||
}
|
||||
if (!integration) throw new Error("Failed to find integration");
|
||||
|
||||
return res.status(200).send({
|
||||
integration,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Key } from '../../models';
|
||||
import { findMembership } from '../../helpers/membership';
|
||||
|
||||
@ -11,34 +10,26 @@ import { findMembership } from '../../helpers/membership';
|
||||
* @returns
|
||||
*/
|
||||
export const uploadKey = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { key } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
const { key } = req.body;
|
||||
|
||||
// validate membership of receiver
|
||||
const receiverMembership = await findMembership({
|
||||
user: key.userId,
|
||||
workspace: workspaceId
|
||||
});
|
||||
// validate membership of receiver
|
||||
const receiverMembership = await findMembership({
|
||||
user: key.userId,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!receiverMembership) {
|
||||
throw new Error('Failed receiver membership validation for workspace');
|
||||
}
|
||||
if (!receiverMembership) {
|
||||
throw new Error('Failed receiver membership validation for workspace');
|
||||
}
|
||||
|
||||
await new Key({
|
||||
encryptedKey: key.encryptedKey,
|
||||
nonce: key.nonce,
|
||||
sender: req.user._id,
|
||||
receiver: key.userId,
|
||||
workspace: workspaceId
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to upload key to workspace'
|
||||
});
|
||||
}
|
||||
await new Key({
|
||||
encryptedKey: key.encryptedKey,
|
||||
nonce: key.nonce,
|
||||
sender: req.user._id,
|
||||
receiver: key.userId,
|
||||
workspace: workspaceId
|
||||
}).save();
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully uploaded key to workspace'
|
||||
@ -52,25 +43,16 @@ export const uploadKey = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const getLatestKey = async (req: Request, res: Response) => {
|
||||
let latestKey;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// get latest key
|
||||
latestKey = await Key.find({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(1)
|
||||
.populate('sender', '+publicKey');
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get latest key'
|
||||
});
|
||||
}
|
||||
// get latest key
|
||||
const latestKey = await Key.find({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(1)
|
||||
.populate('sender', '+publicKey');
|
||||
|
||||
const resObj: any = {};
|
||||
|
||||
@ -79,4 +61,4 @@ export const getLatestKey = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
return res.status(200).send(resObj);
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,3 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response } from 'express';
|
||||
import { Membership, MembershipOrg, User, Key } from '../../models';
|
||||
import {
|
||||
@ -16,25 +15,16 @@ import { getSiteURL } from '../../config';
|
||||
* @returns
|
||||
*/
|
||||
export const validateMembership = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { workspaceId } = req.params;
|
||||
// validate membership
|
||||
const membership = await findMembership({
|
||||
user: req.user._id,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
// validate membership
|
||||
const membership = await findMembership({
|
||||
user: req.user._id,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new Error('Failed to validate membership');
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed workspace connection check'
|
||||
});
|
||||
}
|
||||
if (!membership) {
|
||||
throw new Error('Failed to validate membership');
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Workspace membership confirmed'
|
||||
@ -48,48 +38,39 @@ export const validateMembership = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const deleteMembership = async (req: Request, res: Response) => {
|
||||
let deletedMembership;
|
||||
try {
|
||||
const { membershipId } = req.params;
|
||||
const { membershipId } = req.params;
|
||||
|
||||
// check if membership to delete exists
|
||||
const membershipToDelete = await Membership.findOne({
|
||||
_id: membershipId
|
||||
}).populate('user');
|
||||
// check if membership to delete exists
|
||||
const membershipToDelete = await Membership.findOne({
|
||||
_id: membershipId
|
||||
}).populate('user');
|
||||
|
||||
if (!membershipToDelete) {
|
||||
throw new Error(
|
||||
"Failed to delete workspace membership that doesn't exist"
|
||||
);
|
||||
}
|
||||
if (!membershipToDelete) {
|
||||
throw new Error(
|
||||
"Failed to delete workspace membership that doesn't exist"
|
||||
);
|
||||
}
|
||||
|
||||
// check if user is a member and admin of the workspace
|
||||
// whose membership we wish to delete
|
||||
const membership = await Membership.findOne({
|
||||
user: req.user._id,
|
||||
workspace: membershipToDelete.workspace
|
||||
});
|
||||
// check if user is a member and admin of the workspace
|
||||
// whose membership we wish to delete
|
||||
const membership = await Membership.findOne({
|
||||
user: req.user._id,
|
||||
workspace: membershipToDelete.workspace
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new Error('Failed to validate workspace membership');
|
||||
}
|
||||
if (!membership) {
|
||||
throw new Error('Failed to validate workspace membership');
|
||||
}
|
||||
|
||||
if (membership.role !== ADMIN) {
|
||||
// user is not an admin member of the workspace
|
||||
throw new Error('Insufficient role for deleting workspace membership');
|
||||
}
|
||||
if (membership.role !== ADMIN) {
|
||||
// user is not an admin member of the workspace
|
||||
throw new Error('Insufficient role for deleting workspace membership');
|
||||
}
|
||||
|
||||
// delete workspace membership
|
||||
deletedMembership = await deleteMember({
|
||||
membershipId: membershipToDelete._id.toString()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete membership'
|
||||
});
|
||||
}
|
||||
// delete workspace membership
|
||||
const deletedMembership = await deleteMember({
|
||||
membershipId: membershipToDelete._id.toString()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
deletedMembership
|
||||
@ -103,49 +84,40 @@ export const deleteMembership = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||
let membershipToChangeRole;
|
||||
try {
|
||||
const { membershipId } = req.params;
|
||||
const { role } = req.body;
|
||||
const { membershipId } = req.params;
|
||||
const { role } = req.body;
|
||||
|
||||
if (![ADMIN, MEMBER].includes(role)) {
|
||||
throw new Error('Failed to validate role');
|
||||
}
|
||||
if (![ADMIN, MEMBER].includes(role)) {
|
||||
throw new Error('Failed to validate role');
|
||||
}
|
||||
|
||||
// validate target membership
|
||||
membershipToChangeRole = await findMembership({
|
||||
_id: membershipId
|
||||
});
|
||||
// validate target membership
|
||||
const membershipToChangeRole = await findMembership({
|
||||
_id: membershipId
|
||||
});
|
||||
|
||||
if (!membershipToChangeRole) {
|
||||
throw new Error('Failed to find membership to change role');
|
||||
}
|
||||
if (!membershipToChangeRole) {
|
||||
throw new Error('Failed to find membership to change role');
|
||||
}
|
||||
|
||||
// check if user is a member and admin of target membership's
|
||||
// workspace
|
||||
const membership = await findMembership({
|
||||
user: req.user._id,
|
||||
workspace: membershipToChangeRole.workspace
|
||||
});
|
||||
// check if user is a member and admin of target membership's
|
||||
// workspace
|
||||
const membership = await findMembership({
|
||||
user: req.user._id,
|
||||
workspace: membershipToChangeRole.workspace
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new Error('Failed to validate membership');
|
||||
}
|
||||
if (!membership) {
|
||||
throw new Error('Failed to validate membership');
|
||||
}
|
||||
|
||||
if (membership.role !== ADMIN) {
|
||||
// user is not an admin member of the workspace
|
||||
throw new Error('Insufficient role for changing member roles');
|
||||
}
|
||||
if (membership.role !== ADMIN) {
|
||||
// user is not an admin member of the workspace
|
||||
throw new Error('Insufficient role for changing member roles');
|
||||
}
|
||||
|
||||
membershipToChangeRole.role = role;
|
||||
await membershipToChangeRole.save();
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to change membership role'
|
||||
});
|
||||
}
|
||||
membershipToChangeRole.role = role;
|
||||
await membershipToChangeRole.save();
|
||||
|
||||
return res.status(200).send({
|
||||
membership: membershipToChangeRole
|
||||
@ -159,75 +131,66 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const inviteUserToWorkspace = async (req: Request, res: Response) => {
|
||||
let invitee, latestKey;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { email }: { email: string } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
const { email }: { email: string } = req.body;
|
||||
|
||||
invitee = await User.findOne({
|
||||
email
|
||||
}).select('+publicKey');
|
||||
const invitee = await User.findOne({
|
||||
email
|
||||
}).select('+publicKey');
|
||||
|
||||
if (!invitee || !invitee?.publicKey)
|
||||
throw new Error('Failed to validate invitee');
|
||||
if (!invitee || !invitee?.publicKey)
|
||||
throw new Error('Failed to validate invitee');
|
||||
|
||||
// validate invitee's workspace membership - ensure member isn't
|
||||
// already a member of the workspace
|
||||
const inviteeMembership = await Membership.findOne({
|
||||
user: invitee._id,
|
||||
workspace: workspaceId
|
||||
});
|
||||
// validate invitee's workspace membership - ensure member isn't
|
||||
// already a member of the workspace
|
||||
const inviteeMembership = await Membership.findOne({
|
||||
user: invitee._id,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (inviteeMembership)
|
||||
throw new Error('Failed to add existing member of workspace');
|
||||
if (inviteeMembership)
|
||||
throw new Error('Failed to add existing member of workspace');
|
||||
|
||||
// validate invitee's organization membership - ensure that only
|
||||
// (accepted) organization members can be added to the workspace
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: invitee._id,
|
||||
organization: req.membership.workspace.organization,
|
||||
status: ACCEPTED
|
||||
});
|
||||
// validate invitee's organization membership - ensure that only
|
||||
// (accepted) organization members can be added to the workspace
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: invitee._id,
|
||||
organization: req.membership.workspace.organization,
|
||||
status: ACCEPTED
|
||||
});
|
||||
|
||||
if (!membershipOrg)
|
||||
throw new Error("Failed to validate invitee's organization membership");
|
||||
if (!membershipOrg)
|
||||
throw new Error("Failed to validate invitee's organization membership");
|
||||
|
||||
// get latest key
|
||||
latestKey = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.populate('sender', '+publicKey');
|
||||
// get latest key
|
||||
const latestKey = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.populate('sender', '+publicKey');
|
||||
|
||||
// create new workspace membership
|
||||
const m = await new Membership({
|
||||
user: invitee._id,
|
||||
workspace: workspaceId,
|
||||
role: MEMBER
|
||||
}).save();
|
||||
// create new workspace membership
|
||||
const m = await new Membership({
|
||||
user: invitee._id,
|
||||
workspace: workspaceId,
|
||||
role: MEMBER
|
||||
}).save();
|
||||
|
||||
await sendMail({
|
||||
template: 'workspaceInvitation.handlebars',
|
||||
subjectLine: 'Infisical workspace invitation',
|
||||
recipients: [invitee.email],
|
||||
substitutions: {
|
||||
inviterFirstName: req.user.firstName,
|
||||
inviterEmail: req.user.email,
|
||||
workspaceName: req.membership.workspace.name,
|
||||
callback_url: getSiteURL() + '/login'
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to invite user to workspace'
|
||||
});
|
||||
}
|
||||
await sendMail({
|
||||
template: 'workspaceInvitation.handlebars',
|
||||
subjectLine: 'Infisical workspace invitation',
|
||||
recipients: [invitee.email],
|
||||
substitutions: {
|
||||
inviterFirstName: req.user.firstName,
|
||||
inviterEmail: req.user.email,
|
||||
workspaceName: req.membership.workspace.name,
|
||||
callback_url: (await getSiteURL()) + '/login'
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
invitee,
|
||||
latestKey
|
||||
});
|
||||
};
|
||||
};
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { Types } from 'mongoose';
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { MembershipOrg, Organization, User } from '../../models';
|
||||
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
||||
import { sendMail } from '../../helpers/nodemailer';
|
||||
import { TokenService } from '../../services';
|
||||
import { EELicenseService } from '../../ee/services';
|
||||
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
|
||||
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
|
||||
import { validateUserEmail } from '../../validation';
|
||||
|
||||
/**
|
||||
* Delete organization membership with id [membershipOrgId] from organization
|
||||
@ -16,52 +18,43 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured
|
||||
* @returns
|
||||
*/
|
||||
export const deleteMembershipOrg = async (req: Request, res: Response) => {
|
||||
let membershipOrgToDelete;
|
||||
try {
|
||||
const { membershipOrgId } = req.params;
|
||||
const { membershipOrgId } = req.params;
|
||||
|
||||
// check if organization membership to delete exists
|
||||
membershipOrgToDelete = await MembershipOrg.findOne({
|
||||
_id: membershipOrgId
|
||||
}).populate('user');
|
||||
// check if organization membership to delete exists
|
||||
const membershipOrgToDelete = await MembershipOrg.findOne({
|
||||
_id: membershipOrgId
|
||||
}).populate('user');
|
||||
|
||||
if (!membershipOrgToDelete) {
|
||||
throw new Error(
|
||||
"Failed to delete organization membership that doesn't exist"
|
||||
);
|
||||
}
|
||||
if (!membershipOrgToDelete) {
|
||||
throw new Error(
|
||||
"Failed to delete organization membership that doesn't exist"
|
||||
);
|
||||
}
|
||||
|
||||
// check if user is a member and admin of the organization
|
||||
// whose membership we wish to delete
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: req.user._id,
|
||||
organization: membershipOrgToDelete.organization
|
||||
});
|
||||
// check if user is a member and admin of the organization
|
||||
// whose membership we wish to delete
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: req.user._id,
|
||||
organization: membershipOrgToDelete.organization
|
||||
});
|
||||
|
||||
if (!membershipOrg) {
|
||||
throw new Error('Failed to validate organization membership');
|
||||
}
|
||||
if (!membershipOrg) {
|
||||
throw new Error('Failed to validate organization membership');
|
||||
}
|
||||
|
||||
if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) {
|
||||
// user is not an admin member of the organization
|
||||
throw new Error('Insufficient role for deleting organization membership');
|
||||
}
|
||||
if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) {
|
||||
// user is not an admin member of the organization
|
||||
throw new Error('Insufficient role for deleting organization membership');
|
||||
}
|
||||
|
||||
// delete organization membership
|
||||
const deletedMembershipOrg = await deleteMemberFromOrg({
|
||||
membershipOrgId: membershipOrgToDelete._id.toString()
|
||||
});
|
||||
// delete organization membership
|
||||
const deletedMembershipOrg = await deleteMemberFromOrg({
|
||||
membershipOrgId: membershipOrgToDelete._id.toString()
|
||||
});
|
||||
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId: membershipOrg.organization.toString()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete organization membership'
|
||||
});
|
||||
}
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId: membershipOrg.organization.toString()
|
||||
});
|
||||
|
||||
return membershipOrgToDelete;
|
||||
};
|
||||
@ -77,14 +70,6 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
||||
// [membershipOrgId]
|
||||
|
||||
let membershipToChangeRole;
|
||||
// try {
|
||||
// } catch (err) {
|
||||
// Sentry.setUser({ email: req.user.email });
|
||||
// Sentry.captureException(err);
|
||||
// return res.status(400).send({
|
||||
// message: 'Failed to change organization membership role'
|
||||
// });
|
||||
// }
|
||||
|
||||
return res.status(200).send({
|
||||
membershipOrg: membershipToChangeRole
|
||||
@ -100,103 +85,114 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||
let invitee, inviteeMembershipOrg, completeInviteLink;
|
||||
try {
|
||||
const { organizationId, inviteeEmail } = req.body;
|
||||
const host = req.headers.host;
|
||||
const siteUrl = `${req.protocol}://${host}`;
|
||||
const { organizationId, inviteeEmail } = req.body;
|
||||
const host = req.headers.host;
|
||||
const siteUrl = `${req.protocol}://${host}`;
|
||||
|
||||
// validate membership
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: req.user._id,
|
||||
organization: organizationId
|
||||
});
|
||||
// validate membership
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: req.user._id,
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
if (!membershipOrg) {
|
||||
throw new Error('Failed to validate organization membership');
|
||||
}
|
||||
if (!membershipOrg) {
|
||||
throw new Error('Failed to validate organization membership');
|
||||
}
|
||||
|
||||
const plan = await EELicenseService.getOrganizationPlan(organizationId);
|
||||
|
||||
if (plan.memberLimit !== null) {
|
||||
// case: limit imposed on number of members allowed
|
||||
|
||||
if (plan.membersUsed >= plan.memberLimit) {
|
||||
// case: number of members used exceeds the number of members allowed
|
||||
return res.status(400).send({
|
||||
message: 'Failed to invite member due to member limit reached. Upgrade plan to invite more members.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
invitee = await User.findOne({
|
||||
email: inviteeEmail
|
||||
}).select('+publicKey');
|
||||
invitee = await User.findOne({
|
||||
email: inviteeEmail
|
||||
}).select('+publicKey');
|
||||
|
||||
if (invitee) {
|
||||
// case: invitee is an existing user
|
||||
if (invitee) {
|
||||
// case: invitee is an existing user
|
||||
|
||||
inviteeMembershipOrg = await MembershipOrg.findOne({
|
||||
user: invitee._id,
|
||||
organization: organizationId
|
||||
});
|
||||
inviteeMembershipOrg = await MembershipOrg.findOne({
|
||||
user: invitee._id,
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
|
||||
throw new Error(
|
||||
'Failed to invite an existing member of the organization'
|
||||
);
|
||||
}
|
||||
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
|
||||
throw new Error(
|
||||
'Failed to invite an existing member of the organization'
|
||||
);
|
||||
}
|
||||
|
||||
if (!inviteeMembershipOrg) {
|
||||
await new MembershipOrg({
|
||||
user: invitee,
|
||||
inviteEmail: inviteeEmail,
|
||||
organization: organizationId,
|
||||
role: MEMBER,
|
||||
status: invitee?.publicKey ? ACCEPTED : INVITED
|
||||
}).save();
|
||||
}
|
||||
} else {
|
||||
// check if invitee has been invited before
|
||||
inviteeMembershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: inviteeEmail,
|
||||
organization: organizationId
|
||||
});
|
||||
if (!inviteeMembershipOrg) {
|
||||
|
||||
await new MembershipOrg({
|
||||
user: invitee,
|
||||
inviteEmail: inviteeEmail,
|
||||
organization: organizationId,
|
||||
role: MEMBER,
|
||||
status: INVITED
|
||||
}).save();
|
||||
}
|
||||
} else {
|
||||
// check if invitee has been invited before
|
||||
inviteeMembershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: inviteeEmail,
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
if (!inviteeMembershipOrg) {
|
||||
// case: invitee has never been invited before
|
||||
if (!inviteeMembershipOrg) {
|
||||
// case: invitee has never been invited before
|
||||
|
||||
// validate that email is not disposable
|
||||
validateUserEmail(inviteeEmail);
|
||||
|
||||
await new MembershipOrg({
|
||||
inviteEmail: inviteeEmail,
|
||||
organization: organizationId,
|
||||
role: MEMBER,
|
||||
status: INVITED
|
||||
}).save();
|
||||
}
|
||||
}
|
||||
await new MembershipOrg({
|
||||
inviteEmail: inviteeEmail,
|
||||
organization: organizationId,
|
||||
role: MEMBER,
|
||||
status: INVITED
|
||||
}).save();
|
||||
}
|
||||
}
|
||||
|
||||
const organization = await Organization.findOne({ _id: organizationId });
|
||||
const organization = await Organization.findOne({ _id: organizationId });
|
||||
|
||||
if (organization) {
|
||||
const token = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||
email: inviteeEmail,
|
||||
organizationId: organization._id
|
||||
});
|
||||
if (organization) {
|
||||
|
||||
await sendMail({
|
||||
template: 'organizationInvitation.handlebars',
|
||||
subjectLine: 'Infisical organization invitation',
|
||||
recipients: [inviteeEmail],
|
||||
substitutions: {
|
||||
inviterFirstName: req.user.firstName,
|
||||
inviterEmail: req.user.email,
|
||||
organizationName: organization.name,
|
||||
email: inviteeEmail,
|
||||
token,
|
||||
callback_url: getSiteURL() + '/signupinvite'
|
||||
}
|
||||
});
|
||||
const token = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||
email: inviteeEmail,
|
||||
organizationId: organization._id
|
||||
});
|
||||
|
||||
if (!getSmtpConfigured()) {
|
||||
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}`
|
||||
}
|
||||
}
|
||||
await sendMail({
|
||||
template: 'organizationInvitation.handlebars',
|
||||
subjectLine: 'Infisical organization invitation',
|
||||
recipients: [inviteeEmail],
|
||||
substitutions: {
|
||||
inviterFirstName: req.user.firstName,
|
||||
inviterEmail: req.user.email,
|
||||
organizationName: organization.name,
|
||||
email: inviteeEmail,
|
||||
organizationId: organization._id.toString(),
|
||||
token,
|
||||
callback_url: (await getSiteURL()) + '/signupinvite'
|
||||
}
|
||||
});
|
||||
|
||||
await updateSubscriptionOrgQuantity({ organizationId });
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to send organization invite'
|
||||
});
|
||||
}
|
||||
if (!(await getSmtpConfigured())) {
|
||||
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`
|
||||
}
|
||||
}
|
||||
|
||||
await updateSubscriptionOrgQuantity({ organizationId });
|
||||
|
||||
return res.status(200).send({
|
||||
message: `Sent an invite link to ${req.body.inviteeEmail}`,
|
||||
@ -212,61 +208,62 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
let user, token;
|
||||
try {
|
||||
const { email, code } = req.body;
|
||||
let user;
|
||||
const {
|
||||
email,
|
||||
organizationId,
|
||||
code
|
||||
} = req.body;
|
||||
|
||||
user = await User.findOne({ email }).select('+publicKey');
|
||||
user = await User.findOne({ email }).select('+publicKey');
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: email,
|
||||
status: INVITED
|
||||
});
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: email,
|
||||
status: INVITED,
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
if (!membershipOrg)
|
||||
throw new Error('Failed to find any invitations for email');
|
||||
if (!membershipOrg)
|
||||
throw new Error('Failed to find any invitations for email');
|
||||
|
||||
await TokenService.validateToken({
|
||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||
email,
|
||||
organizationId: membershipOrg.organization,
|
||||
token: code
|
||||
});
|
||||
await TokenService.validateToken({
|
||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||
email,
|
||||
organizationId: membershipOrg.organization,
|
||||
token: code
|
||||
});
|
||||
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
// membership can be approved and redirected to login/dashboard
|
||||
membershipOrg.status = ACCEPTED;
|
||||
await membershipOrg.save();
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
// 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',
|
||||
user,
|
||||
});
|
||||
}
|
||||
return res.status(200).send({
|
||||
message: 'Successfully verified email',
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
// initialize user account
|
||||
user = await new User({
|
||||
email
|
||||
}).save();
|
||||
}
|
||||
if (!user) {
|
||||
// initialize user account
|
||||
user = await new User({
|
||||
email
|
||||
}).save();
|
||||
}
|
||||
|
||||
// generate temporary signup token
|
||||
token = createToken({
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: getJwtSignupLifetime(),
|
||||
secret: getJwtSignupSecret()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
error: 'Failed email magic link verification for organization invitation'
|
||||
});
|
||||
}
|
||||
// generate temporary signup token
|
||||
const token = createToken({
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: await getJwtSignupLifetime(),
|
||||
secret: await getJwtSignupSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully verified email',
|
||||
|
@ -1,4 +1,3 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response } from 'express';
|
||||
import Stripe from 'stripe';
|
||||
import {
|
||||
@ -15,20 +14,12 @@ import _ from 'lodash';
|
||||
import { getStripeSecretKey, getSiteURL } from '../../config';
|
||||
|
||||
export const getOrganizations = async (req: Request, res: Response) => {
|
||||
let organizations;
|
||||
try {
|
||||
organizations = (
|
||||
await MembershipOrg.find({
|
||||
user: req.user._id
|
||||
}).populate('organization')
|
||||
).map((m) => m.organization);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get organizations'
|
||||
});
|
||||
}
|
||||
const organizations = (
|
||||
await MembershipOrg.find({
|
||||
user: req.user._id,
|
||||
status: ACCEPTED
|
||||
}).populate('organization')
|
||||
).map((m) => m.organization);
|
||||
|
||||
return res.status(200).send({
|
||||
organizations
|
||||
@ -43,33 +34,24 @@ export const getOrganizations = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const createOrganization = async (req: Request, res: Response) => {
|
||||
let organization;
|
||||
try {
|
||||
const { organizationName } = req.body;
|
||||
const { organizationName } = req.body;
|
||||
|
||||
if (organizationName.length < 1) {
|
||||
throw new Error('Organization names must be at least 1-character long');
|
||||
}
|
||||
if (organizationName.length < 1) {
|
||||
throw new Error('Organization names must be at least 1-character long');
|
||||
}
|
||||
|
||||
// create organization and add user as member
|
||||
organization = await create({
|
||||
email: req.user.email,
|
||||
name: organizationName
|
||||
});
|
||||
// create organization and add user as member
|
||||
const organization = await create({
|
||||
email: req.user.email,
|
||||
name: organizationName
|
||||
});
|
||||
|
||||
await addMembershipsOrg({
|
||||
userIds: [req.user._id.toString()],
|
||||
organizationId: organization._id.toString(),
|
||||
roles: [OWNER],
|
||||
statuses: [ACCEPTED]
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to create organization'
|
||||
});
|
||||
}
|
||||
await addMembershipsOrg({
|
||||
userIds: [req.user._id.toString()],
|
||||
organizationId: organization._id.toString(),
|
||||
roles: [OWNER],
|
||||
statuses: [ACCEPTED]
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
organization
|
||||
@ -83,17 +65,7 @@ export const createOrganization = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganization = async (req: Request, res: Response) => {
|
||||
let organization;
|
||||
try {
|
||||
organization = req.membershipOrg.organization;
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to find organization'
|
||||
});
|
||||
}
|
||||
|
||||
const organization = req.organization
|
||||
return res.status(200).send({
|
||||
organization
|
||||
});
|
||||
@ -106,20 +78,11 @@ export const getOrganization = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationMembers = async (req: Request, res: Response) => {
|
||||
let users;
|
||||
try {
|
||||
const { organizationId } = req.params;
|
||||
const { organizationId } = req.params;
|
||||
|
||||
users = await MembershipOrg.find({
|
||||
organization: organizationId
|
||||
}).populate('user', '+publicKey');
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get organization members'
|
||||
});
|
||||
}
|
||||
const users = await MembershipOrg.find({
|
||||
organization: organizationId
|
||||
}).populate('user', '+publicKey');
|
||||
|
||||
return res.status(200).send({
|
||||
users
|
||||
@ -136,35 +99,26 @@ export const getOrganizationWorkspaces = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
let workspaces;
|
||||
try {
|
||||
const { organizationId } = req.params;
|
||||
const { organizationId } = req.params;
|
||||
|
||||
const workspacesSet = new Set(
|
||||
(
|
||||
await Workspace.find(
|
||||
{
|
||||
organization: organizationId
|
||||
},
|
||||
'_id'
|
||||
)
|
||||
).map((w) => w._id.toString())
|
||||
);
|
||||
const workspacesSet = new Set(
|
||||
(
|
||||
await Workspace.find(
|
||||
{
|
||||
organization: organizationId
|
||||
},
|
||||
'_id'
|
||||
)
|
||||
).map((w) => w._id.toString())
|
||||
);
|
||||
|
||||
workspaces = (
|
||||
await Membership.find({
|
||||
user: req.user._id
|
||||
}).populate('workspace')
|
||||
)
|
||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||
.map((m) => m.workspace);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get my workspaces'
|
||||
});
|
||||
}
|
||||
const workspaces = (
|
||||
await Membership.find({
|
||||
user: req.user._id
|
||||
}).populate('workspace')
|
||||
)
|
||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||
.map((m) => m.workspace);
|
||||
|
||||
return res.status(200).send({
|
||||
workspaces
|
||||
@ -178,29 +132,20 @@ export const getOrganizationWorkspaces = async (
|
||||
* @returns
|
||||
*/
|
||||
export const changeOrganizationName = async (req: Request, res: Response) => {
|
||||
let organization;
|
||||
try {
|
||||
const { organizationId } = req.params;
|
||||
const { name } = req.body;
|
||||
const { organizationId } = req.params;
|
||||
const { name } = req.body;
|
||||
|
||||
organization = await Organization.findOneAndUpdate(
|
||||
{
|
||||
_id: organizationId
|
||||
},
|
||||
{
|
||||
name
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to change organization name'
|
||||
});
|
||||
}
|
||||
const organization = await Organization.findOneAndUpdate(
|
||||
{
|
||||
_id: organizationId
|
||||
},
|
||||
{
|
||||
name
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully changed organization name',
|
||||
@ -218,20 +163,11 @@ export const getOrganizationIncidentContacts = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
let incidentContactsOrg;
|
||||
try {
|
||||
const { organizationId } = req.params;
|
||||
const { organizationId } = req.params;
|
||||
|
||||
incidentContactsOrg = await IncidentContactOrg.find({
|
||||
organization: organizationId
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get organization incident contacts'
|
||||
});
|
||||
}
|
||||
const incidentContactsOrg = await IncidentContactOrg.find({
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
incidentContactsOrg
|
||||
@ -248,23 +184,14 @@ export const addOrganizationIncidentContact = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
let incidentContactOrg;
|
||||
try {
|
||||
const { organizationId } = req.params;
|
||||
const { email } = req.body;
|
||||
const { organizationId } = req.params;
|
||||
const { email } = req.body;
|
||||
|
||||
incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
|
||||
{ email, organization: organizationId },
|
||||
{ email, organization: organizationId },
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to add incident contact for organization'
|
||||
});
|
||||
}
|
||||
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
|
||||
{ email, organization: organizationId },
|
||||
{ email, organization: organizationId },
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
incidentContactOrg
|
||||
@ -281,22 +208,13 @@ export const deleteOrganizationIncidentContact = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
let incidentContactOrg;
|
||||
try {
|
||||
const { organizationId } = req.params;
|
||||
const { email } = req.body;
|
||||
const { organizationId } = req.params;
|
||||
const { email } = req.body;
|
||||
|
||||
incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
|
||||
email,
|
||||
organization: organizationId
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete organization incident contact'
|
||||
});
|
||||
}
|
||||
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
|
||||
email,
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully deleted organization incident contact',
|
||||
@ -316,41 +234,33 @@ export const createOrganizationPortalSession = async (
|
||||
res: Response
|
||||
) => {
|
||||
let session;
|
||||
try {
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
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,
|
||||
type: 'card'
|
||||
});
|
||||
// check if there is a payment method on file
|
||||
const paymentMethods = await stripe.paymentMethods.list({
|
||||
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.organization.customerId,
|
||||
mode: 'setup',
|
||||
payment_method_types: ['card'],
|
||||
success_url: (await getSiteURL()) + '/dashboard',
|
||||
cancel_url: (await getSiteURL()) + '/dashboard'
|
||||
});
|
||||
} else {
|
||||
session = await stripe.billingPortal.sessions.create({
|
||||
customer: req.organization.customerId,
|
||||
return_url: (await getSiteURL()) + '/dashboard'
|
||||
});
|
||||
}
|
||||
|
||||
if (paymentMethods.data.length < 1) {
|
||||
// case: no payment method on file
|
||||
session = await stripe.checkout.sessions.create({
|
||||
customer: req.membershipOrg.organization.customerId,
|
||||
mode: 'setup',
|
||||
payment_method_types: ['card'],
|
||||
success_url: getSiteURL() + '/dashboard',
|
||||
cancel_url: getSiteURL() + '/dashboard'
|
||||
});
|
||||
} else {
|
||||
session = await stripe.billingPortal.sessions.create({
|
||||
customer: req.membershipOrg.organization.customerId,
|
||||
return_url: getSiteURL() + '/dashboard'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({ url: session.url });
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to redirect to organization billing portal'
|
||||
});
|
||||
}
|
||||
return res.status(200).send({ url: session.url });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -363,22 +273,13 @@ export const getOrganizationSubscriptions = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
let subscriptions;
|
||||
try {
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
|
||||
subscriptions = await stripe.subscriptions.list({
|
||||
customer: req.membershipOrg.organization.customerId
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get organization subscriptions'
|
||||
});
|
||||
}
|
||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
|
||||
const subscriptions = await stripe.subscriptions.list({
|
||||
customer: req.organization.customerId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
subscriptions
|
||||
@ -424,4 +325,4 @@ export const getOrganizationMembersAndTheirWorkspaces = async (
|
||||
});
|
||||
|
||||
return res.json(userToWorkspaceIds);
|
||||
};
|
||||
};
|
||||
|
@ -1,15 +1,25 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const jsrp = require('jsrp');
|
||||
import * as bigintConversion from 'bigint-conversion';
|
||||
import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { sendMail } from '../../helpers/nodemailer';
|
||||
import {
|
||||
createToken,
|
||||
sendMail,
|
||||
clearTokens
|
||||
} from '../../helpers';
|
||||
import { TokenService } from '../../services';
|
||||
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables';
|
||||
import {
|
||||
TOKEN_EMAIL_PASSWORD_RESET,
|
||||
AUTH_MODE_JWT
|
||||
} from '../../variables';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config';
|
||||
import {
|
||||
getSiteURL,
|
||||
getJwtSignupLifetime,
|
||||
getJwtSignupSecret,
|
||||
getHttpsEnabled
|
||||
} from '../../config';
|
||||
|
||||
/**
|
||||
* Password reset step 1: Send email verification link to email [email]
|
||||
@ -20,43 +30,35 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../conf
|
||||
*/
|
||||
export const emailPasswordReset = async (req: Request, res: Response) => {
|
||||
let email: string;
|
||||
try {
|
||||
email = req.body.email;
|
||||
email = req.body.email;
|
||||
|
||||
const user = await User.findOne({ email }).select('+publicKey');
|
||||
if (!user || !user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
const user = await User.findOne({ email }).select('+publicKey');
|
||||
if (!user || !user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
|
||||
return res.status(403).send({
|
||||
error: 'Failed to send email verification for password reset'
|
||||
});
|
||||
}
|
||||
|
||||
const token = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_PASSWORD_RESET,
|
||||
email
|
||||
});
|
||||
|
||||
await sendMail({
|
||||
template: 'passwordReset.handlebars',
|
||||
subjectLine: 'Infisical password reset',
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
email,
|
||||
token,
|
||||
callback_url: getSiteURL() + '/password-reset'
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to send email for account recovery'
|
||||
});
|
||||
}
|
||||
return res.status(403).send({
|
||||
message: "If an account exists with this email, a password reset link has been sent"
|
||||
});
|
||||
}
|
||||
|
||||
const token = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_PASSWORD_RESET,
|
||||
email
|
||||
});
|
||||
|
||||
await sendMail({
|
||||
template: 'passwordReset.handlebars',
|
||||
subjectLine: 'Infisical password reset',
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
email,
|
||||
token,
|
||||
callback_url: (await getSiteURL()) + '/password-reset'
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: `Sent an email for account recovery to ${email}`
|
||||
message:"If an account exists with this email, a password reset link has been sent"
|
||||
});
|
||||
}
|
||||
|
||||
@ -67,40 +69,31 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
||||
let user, token;
|
||||
try {
|
||||
const { email, code } = req.body;
|
||||
const { email, code } = req.body;
|
||||
|
||||
user = await User.findOne({ email }).select('+publicKey');
|
||||
if (!user || !user?.publicKey) {
|
||||
// case: user doesn't exist with email [email] or
|
||||
// hasn't even completed their account
|
||||
return res.status(403).send({
|
||||
error: 'Failed email verification for password reset'
|
||||
});
|
||||
}
|
||||
|
||||
await TokenService.validateToken({
|
||||
type: TOKEN_EMAIL_PASSWORD_RESET,
|
||||
email,
|
||||
token: code
|
||||
});
|
||||
const user = await User.findOne({ email }).select('+publicKey');
|
||||
if (!user || !user?.publicKey) {
|
||||
// case: user doesn't exist with email [email] or
|
||||
// hasn't even completed their account
|
||||
return res.status(403).send({
|
||||
error: 'Failed email verification for password reset'
|
||||
});
|
||||
}
|
||||
|
||||
await TokenService.validateToken({
|
||||
type: TOKEN_EMAIL_PASSWORD_RESET,
|
||||
email,
|
||||
token: code
|
||||
});
|
||||
|
||||
// generate temporary password-reset token
|
||||
token = createToken({
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: getJwtSignupLifetime(),
|
||||
secret: getJwtSignupSecret()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed email verification for password reset'
|
||||
});
|
||||
}
|
||||
// generate temporary password-reset token
|
||||
const token = createToken({
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: await getJwtSignupLifetime(),
|
||||
secret: await getJwtSignupSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully verified email',
|
||||
@ -117,44 +110,38 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const srp1 = async (req: Request, res: Response) => {
|
||||
// return salt, serverPublicKey as part of first step of SRP protocol
|
||||
try {
|
||||
const { clientPublicKey } = req.body;
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).select('+salt +verifier');
|
||||
|
||||
const { clientPublicKey } = req.body;
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).select('+salt +verifier');
|
||||
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier
|
||||
},
|
||||
async () => {
|
||||
// generate server-side public key
|
||||
const serverPublicKey = server.getPublicKey();
|
||||
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: req.user.email }, {
|
||||
email: req.user.email,
|
||||
clientPublicKey: clientPublicKey,
|
||||
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||
}, { upsert: true, returnNewDocument: false })
|
||||
await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, {
|
||||
email: req.user.email,
|
||||
clientPublicKey: clientPublicKey,
|
||||
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||
}, { upsert: true, returnNewDocument: false })
|
||||
|
||||
return res.status(200).send({
|
||||
serverPublicKey,
|
||||
salt: user.salt
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serverPublicKey,
|
||||
salt: user.salt
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
error: 'Failed to start change password process'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Change account SRP authentication information for user
|
||||
@ -165,80 +152,85 @@ export const srp1 = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const changePassword = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
clientProof,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
} = req.body;
|
||||
const {
|
||||
clientProof,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
} = req.body;
|
||||
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).select('+salt +verifier');
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).select('+salt +verifier');
|
||||
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
|
||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
|
||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
|
||||
|
||||
if (!loginSRPDetailFromDB) {
|
||||
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
||||
}
|
||||
if (!loginSRPDetailFromDB) {
|
||||
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
||||
}
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier,
|
||||
b: loginSRPDetailFromDB.serverBInt
|
||||
},
|
||||
async () => {
|
||||
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier,
|
||||
b: loginSRPDetailFromDB.serverBInt
|
||||
},
|
||||
async () => {
|
||||
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
||||
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
// change password
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
// change password
|
||||
|
||||
await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
|
||||
await clearTokens(req.authData.tokenVersionId)
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully changed password'
|
||||
});
|
||||
}
|
||||
// clear httpOnly cookie
|
||||
|
||||
res.cookie('jid', '', {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: (await getHttpsEnabled()) as boolean
|
||||
});
|
||||
|
||||
return res.status(400).send({
|
||||
error: 'Failed to change password. Try again?'
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
error: 'Failed to change password. Try again?'
|
||||
});
|
||||
}
|
||||
return res.status(200).send({
|
||||
message: 'Successfully changed password'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
error: 'Failed to change password. Try again?'
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -252,69 +244,61 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
|
||||
// requires verifying [clientProof] as part of second step of SRP protocol
|
||||
// as initiated in /srp1
|
||||
|
||||
try {
|
||||
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
|
||||
req.body;
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).select('+salt +verifier');
|
||||
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
|
||||
req.body;
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).select('+salt +verifier');
|
||||
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
|
||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
|
||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
|
||||
|
||||
if (!loginSRPDetailFromDB) {
|
||||
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
||||
}
|
||||
if (!loginSRPDetailFromDB) {
|
||||
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
||||
}
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier,
|
||||
b: loginSRPDetailFromDB.serverBInt
|
||||
},
|
||||
async () => {
|
||||
server.setClientPublicKey(
|
||||
loginSRPDetailFromDB.clientPublicKey
|
||||
);
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier,
|
||||
b: loginSRPDetailFromDB.serverBInt
|
||||
},
|
||||
async () => {
|
||||
server.setClientPublicKey(
|
||||
loginSRPDetailFromDB.clientPublicKey
|
||||
);
|
||||
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
// create new or replace backup private key
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
// create new or replace backup private key
|
||||
|
||||
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
|
||||
{ user: req.user._id },
|
||||
{
|
||||
user: req.user._id,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
).select('+user, encryptedPrivateKey');
|
||||
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
|
||||
{ user: req.user._id },
|
||||
{
|
||||
user: req.user._id,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
).select('+user, encryptedPrivateKey');
|
||||
|
||||
// issue tokens
|
||||
return res.status(200).send({
|
||||
message: 'Successfully updated backup private key',
|
||||
backupPrivateKey
|
||||
});
|
||||
}
|
||||
// issue tokens
|
||||
return res.status(200).send({
|
||||
message: 'Successfully updated backup private key',
|
||||
backupPrivateKey
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: 'Failed to update backup private key'
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to update backup private key'
|
||||
});
|
||||
}
|
||||
return res.status(400).send({
|
||||
message: 'Failed to update backup private key'
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -324,20 +308,11 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
||||
let backupPrivateKey;
|
||||
try {
|
||||
backupPrivateKey = await BackupPrivateKey.findOne({
|
||||
user: req.user._id
|
||||
}).select('+encryptedPrivateKey +iv +tag');
|
||||
const backupPrivateKey = await BackupPrivateKey.findOne({
|
||||
user: req.user._id
|
||||
}).select('+encryptedPrivateKey +iv +tag');
|
||||
|
||||
if (!backupPrivateKey) throw new Error('Failed to find backup private key');
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get backup private key'
|
||||
});
|
||||
}
|
||||
if (!backupPrivateKey) throw new Error('Failed to find backup private key');
|
||||
|
||||
return res.status(200).send({
|
||||
backupPrivateKey
|
||||
@ -345,44 +320,36 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
export const resetPassword = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
} = req.body;
|
||||
const {
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
} = req.body;
|
||||
|
||||
await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get backup private key'
|
||||
});
|
||||
}
|
||||
await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully reset password'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { Key, Secret } from '../../models';
|
||||
import {
|
||||
v1PushSecrets as push,
|
||||
@ -9,7 +9,7 @@ import {
|
||||
import { pushKeys } from '../../helpers/key';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
import { EventService } from '../../services';
|
||||
import { getPostHogClient } from '../../services';
|
||||
import { TelemetryService } from '../../services';
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
@ -36,65 +36,56 @@ interface PushSecret {
|
||||
*/
|
||||
export const pushSecrets = async (req: Request, res: Response) => {
|
||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
let { secrets }: { secrets: PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
try {
|
||||
const postHogClient = getPostHogClient();
|
||||
let { secrets }: { secrets: PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
// sanitize secrets
|
||||
secrets = secrets.filter(
|
||||
(s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== ''
|
||||
);
|
||||
|
||||
// sanitize secrets
|
||||
secrets = secrets.filter(
|
||||
(s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== ''
|
||||
);
|
||||
await push({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
});
|
||||
|
||||
await push({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
});
|
||||
await pushKeys({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
keys
|
||||
});
|
||||
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets pushed',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await pushKeys({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
keys
|
||||
});
|
||||
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets pushed',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId
|
||||
})
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to upload workspace secrets'
|
||||
});
|
||||
}
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment
|
||||
})
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully uploaded workspace secrets'
|
||||
@ -111,55 +102,48 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
||||
export const pullSecrets = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
let key;
|
||||
try {
|
||||
const postHogClient = getPostHogClient();
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
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;
|
||||
|
||||
secrets = await pull({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: channel ? channel : 'cli',
|
||||
ipAddress: req.ip
|
||||
});
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
key = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.populate('sender', '+publicKey');
|
||||
|
||||
if (channel !== 'cli') {
|
||||
secrets = reformatPullSecrets({ secrets });
|
||||
}
|
||||
secrets = await pull({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: channel ? channel : 'cli',
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
if (postHogClient) {
|
||||
// capture secrets pushed event in production
|
||||
postHogClient.capture({
|
||||
distinctId: req.user.email,
|
||||
event: 'secrets pulled',
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to pull workspace secrets'
|
||||
key = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.populate('sender', '+publicKey');
|
||||
|
||||
if (channel !== 'cli') {
|
||||
secrets = reformatPullSecrets({ secrets });
|
||||
}
|
||||
|
||||
if (postHogClient) {
|
||||
// capture secrets pushed event in production
|
||||
postHogClient.capture({
|
||||
distinctId: req.user.email,
|
||||
event: 'secrets pulled',
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -180,54 +164,47 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
let key;
|
||||
try {
|
||||
const postHogClient = getPostHogClient();
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
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;
|
||||
|
||||
secrets = await pull({
|
||||
userId: req.serviceToken.user._id.toString(),
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: 'cli',
|
||||
ipAddress: req.ip
|
||||
});
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
key = {
|
||||
encryptedKey: req.serviceToken.encryptedKey,
|
||||
nonce: req.serviceToken.nonce,
|
||||
sender: {
|
||||
publicKey: req.serviceToken.publicKey
|
||||
},
|
||||
receiver: req.serviceToken.user,
|
||||
workspace: req.serviceToken.workspace
|
||||
};
|
||||
secrets = await pull({
|
||||
userId: req.serviceToken.user._id.toString(),
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: 'cli',
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
if (postHogClient) {
|
||||
// capture secrets pulled event in production
|
||||
postHogClient.capture({
|
||||
distinctId: req.serviceToken.user.email,
|
||||
event: 'secrets pulled',
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.serviceToken.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to pull workspace secrets'
|
||||
key = {
|
||||
encryptedKey: req.serviceToken.encryptedKey,
|
||||
nonce: req.serviceToken.nonce,
|
||||
sender: {
|
||||
publicKey: req.serviceToken.publicKey
|
||||
},
|
||||
receiver: req.serviceToken.user,
|
||||
workspace: req.serviceToken.workspace
|
||||
};
|
||||
|
||||
if (postHogClient) {
|
||||
// capture secrets pulled event in production
|
||||
postHogClient.capture({
|
||||
distinctId: req.serviceToken.user.email,
|
||||
event: 'secrets pulled',
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -235,4 +212,4 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
secrets: reformatPullSecrets({ secrets }),
|
||||
key
|
||||
});
|
||||
};
|
||||
};
|
||||
|
@ -1,89 +1,218 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Secret } from '../../models';
|
||||
import Folder from '../../models/folder';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
import { ROOT_FOLDER_PATH, getFolderPath, getParentPath, normalizePath, validateFolderName } from '../../utils/folder';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import { validateMembership } from '../../helpers/membership';
|
||||
import { Request, Response } from "express";
|
||||
import { Secret } from "../../models";
|
||||
import Folder from "../../models/folder";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import {
|
||||
appendFolder,
|
||||
deleteFolderById,
|
||||
getAllFolderIds,
|
||||
searchByFolderIdWithDir,
|
||||
searchByFolderId,
|
||||
validateFolderName,
|
||||
generateFolderId,
|
||||
getParentFromFolderId,
|
||||
} from "../../services/FolderService";
|
||||
import { ADMIN, MEMBER } from "../../variables";
|
||||
import { validateMembership } from "../../helpers/membership";
|
||||
import { FolderVersion } from "../../ee/models";
|
||||
import { EESecretService } from "../../ee/services";
|
||||
|
||||
// TODO
|
||||
// verify workspace id/environment
|
||||
export const createFolder = async (req: Request, res: Response) => {
|
||||
const { workspaceId, environment, folderName, parentFolderId } = req.body
|
||||
const { workspaceId, environment, folderName, parentFolderId } = req.body;
|
||||
if (!validateFolderName(folderName)) {
|
||||
throw BadRequestError({ message: "Folder name cannot contain spaces. Only underscore and dashes" })
|
||||
throw BadRequestError({
|
||||
message: "Folder name cannot contain spaces. Only underscore and dashes",
|
||||
});
|
||||
}
|
||||
|
||||
if (parentFolderId) {
|
||||
const parentFolder = await Folder.find({ environment: environment, workspace: workspaceId, id: parentFolderId });
|
||||
if (!parentFolder) {
|
||||
throw BadRequestError({ message: "The parent folder doesn't exist" })
|
||||
}
|
||||
}
|
||||
|
||||
let completePath = await getFolderPath(parentFolderId)
|
||||
if (completePath == ROOT_FOLDER_PATH) {
|
||||
completePath = ""
|
||||
}
|
||||
|
||||
const currentFolderPath = completePath + "/" + folderName // construct new path with current folder to be created
|
||||
const normalizedCurrentPath = normalizePath(currentFolderPath)
|
||||
const normalizedParentPath = getParentPath(normalizedCurrentPath)
|
||||
|
||||
const existingFolder = await Folder.findOne({
|
||||
name: folderName,
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment: environment,
|
||||
parent: parentFolderId,
|
||||
path: normalizedCurrentPath
|
||||
environment,
|
||||
}).lean();
|
||||
// space has no folders initialized
|
||||
if (!folders) {
|
||||
const id = generateFolderId();
|
||||
const folder = new Folder({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: {
|
||||
id: "root",
|
||||
name: "root",
|
||||
version: 1,
|
||||
children: [{ id, name: folderName, children: [], version: 1 }],
|
||||
},
|
||||
});
|
||||
await folder.save();
|
||||
const folderVersion = new FolderVersion({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: folder.nodes,
|
||||
});
|
||||
await folderVersion.save();
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId,
|
||||
environment,
|
||||
});
|
||||
return res.json({ folder: { id, name: folderName } });
|
||||
}
|
||||
|
||||
const folder = appendFolder(folders.nodes, { folderName, parentFolderId });
|
||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||
|
||||
const parentFolder = searchByFolderId(folders.nodes, parentFolderId);
|
||||
const folderVersion = new FolderVersion({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: parentFolder,
|
||||
});
|
||||
await folderVersion.save();
|
||||
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId: parentFolderId,
|
||||
});
|
||||
|
||||
if (existingFolder) {
|
||||
return res.json(existingFolder)
|
||||
}
|
||||
return res.json({ folder });
|
||||
};
|
||||
|
||||
const newFolder = new Folder({
|
||||
name: folderName,
|
||||
workspace: workspaceId,
|
||||
environment: environment,
|
||||
parent: parentFolderId,
|
||||
path: normalizedCurrentPath,
|
||||
parentPath: normalizedParentPath
|
||||
});
|
||||
export const updateFolderById = async (req: Request, res: Response) => {
|
||||
const { folderId } = req.params;
|
||||
const { name, workspaceId, environment } = req.body;
|
||||
|
||||
await newFolder.save();
|
||||
|
||||
return res.json(newFolder)
|
||||
}
|
||||
|
||||
export const deleteFolder = async (req: Request, res: Response) => {
|
||||
const { folderId } = req.params
|
||||
const queue: any[] = [folderId];
|
||||
|
||||
const folder = await Folder.findById(folderId);
|
||||
if (!folder) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" })
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
// check that user is a member of the workspace
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: folder.workspace as any,
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
workspaceId,
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
});
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentFolderId = queue.shift();
|
||||
|
||||
const childFolders = await Folder.find({ parent: currentFolderId });
|
||||
for (const childFolder of childFolders) {
|
||||
queue.push(childFolder._id);
|
||||
}
|
||||
|
||||
await Secret.deleteMany({ folder: currentFolderId });
|
||||
|
||||
await Folder.deleteOne({ _id: currentFolderId });
|
||||
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" });
|
||||
}
|
||||
|
||||
res.send()
|
||||
}
|
||||
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({
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { User } from '../../models';
|
||||
import {
|
||||
sendEmailVerification,
|
||||
@ -8,6 +7,7 @@ import {
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
|
||||
import { validateUserEmail } from '../../validation';
|
||||
|
||||
/**
|
||||
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
||||
@ -18,35 +18,22 @@ import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpC
|
||||
*/
|
||||
export const beginEmailSignup = async (req: Request, res: Response) => {
|
||||
let email: string;
|
||||
try {
|
||||
email = req.body.email;
|
||||
email = req.body.email;
|
||||
|
||||
// validate that email is not disposable
|
||||
validateUserEmail(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
|
||||
|
||||
const user = await User.findOne({ email }).select('+publicKey');
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
return res.status(403).send({
|
||||
error: 'Failed to send email verification code for complete account'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(403).send({
|
||||
error: 'Failed to send email verification code for complete account'
|
||||
});
|
||||
}
|
||||
|
||||
// send send verification email
|
||||
await sendEmailVerification({ email });
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
error: 'Failed to send email verification code'
|
||||
});
|
||||
}
|
||||
// send send verification email
|
||||
await sendEmailVerification({ email });
|
||||
|
||||
return res.status(200).send({
|
||||
message: `Sent an email verification code to ${email}`
|
||||
@ -62,47 +49,47 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const verifyEmailSignup = async (req: Request, res: Response) => {
|
||||
let user, token;
|
||||
try {
|
||||
const { email, code } = req.body;
|
||||
const { email, code } = req.body;
|
||||
|
||||
// initialize user account
|
||||
user = await User.findOne({ email }).select('+publicKey');
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
return res.status(403).send({
|
||||
error: 'Failed email verification for complete user'
|
||||
});
|
||||
}
|
||||
// initialize user account
|
||||
user = await User.findOne({ email }).select('+publicKey');
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
return res.status(403).send({
|
||||
error: 'Failed email verification for complete user'
|
||||
});
|
||||
}
|
||||
|
||||
// verify email
|
||||
if (getSmtpConfigured()) {
|
||||
await checkEmailVerification({
|
||||
email,
|
||||
code
|
||||
});
|
||||
}
|
||||
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." })
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
user = await new User({
|
||||
email
|
||||
}).save();
|
||||
}
|
||||
// verify email
|
||||
if (await getSmtpConfigured()) {
|
||||
await checkEmailVerification({
|
||||
email,
|
||||
code
|
||||
});
|
||||
}
|
||||
|
||||
// generate temporary signup token
|
||||
token = createToken({
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: getJwtSignupLifetime(),
|
||||
secret: getJwtSignupSecret()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
error: 'Failed email verification'
|
||||
});
|
||||
}
|
||||
if (!user) {
|
||||
user = await new User({
|
||||
email
|
||||
}).save();
|
||||
}
|
||||
|
||||
// generate temporary signup token
|
||||
token = createToken({
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: await getJwtSignupLifetime(),
|
||||
secret: await getJwtSignupSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfuly verified email',
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import Stripe from 'stripe';
|
||||
import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
|
||||
|
||||
@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
|
||||
* @returns
|
||||
*/
|
||||
export const handleWebhook = async (req: Request, res: Response) => {
|
||||
let event;
|
||||
try {
|
||||
// check request for valid stripe signature
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
// check request for valid stripe signature
|
||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
|
||||
const sig = req.headers['stripe-signature'] as string;
|
||||
event = stripe.webhooks.constructEvent(
|
||||
req.body,
|
||||
sig,
|
||||
getStripeWebhookSecret()
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
error: 'Failed to process webhook'
|
||||
});
|
||||
}
|
||||
const sig = req.headers['stripe-signature'] as string;
|
||||
const event = stripe.webhooks.constructEvent(
|
||||
req.body,
|
||||
sig,
|
||||
await getStripeWebhookSecret()
|
||||
);
|
||||
|
||||
switch (event.type) {
|
||||
case '':
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { UserAction } from '../../models';
|
||||
|
||||
/**
|
||||
@ -11,28 +10,19 @@ import { UserAction } from '../../models';
|
||||
export const addUserAction = async (req: Request, res: Response) => {
|
||||
// add/record new action [action] for user with id [req.user._id]
|
||||
|
||||
let userAction;
|
||||
try {
|
||||
const { action } = req.body;
|
||||
const { action } = req.body;
|
||||
|
||||
userAction = await UserAction.findOneAndUpdate(
|
||||
{
|
||||
user: req.user._id,
|
||||
action
|
||||
},
|
||||
{ user: req.user._id, action },
|
||||
{
|
||||
new: true,
|
||||
upsert: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to record user action'
|
||||
});
|
||||
}
|
||||
const userAction = await UserAction.findOneAndUpdate(
|
||||
{
|
||||
user: req.user._id,
|
||||
action
|
||||
},
|
||||
{ user: req.user._id, action },
|
||||
{
|
||||
new: true,
|
||||
upsert: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully recorded user action',
|
||||
@ -48,21 +38,12 @@ export const addUserAction = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const getUserAction = async (req: Request, res: Response) => {
|
||||
// get user action [action] for user with id [req.user._id]
|
||||
let userAction;
|
||||
try {
|
||||
const action: string = req.query.action as string;
|
||||
const action: string = req.query.action as string;
|
||||
|
||||
userAction = await UserAction.findOne({
|
||||
user: req.user._id,
|
||||
action
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get user action'
|
||||
});
|
||||
}
|
||||
const userAction = await UserAction.findOne({
|
||||
user: req.user._id,
|
||||
action
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
userAction
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Request, Response } from "express";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import {
|
||||
Workspace,
|
||||
Membership,
|
||||
@ -14,6 +13,7 @@ import {
|
||||
createWorkspace as create,
|
||||
deleteWorkspace as deleteWork,
|
||||
} from "../../helpers/workspace";
|
||||
import { EELicenseService } from '../../ee/services';
|
||||
import { addMemberships } from "../../helpers/membership";
|
||||
import { ADMIN } from "../../variables";
|
||||
|
||||
@ -24,27 +24,18 @@ import { ADMIN } from "../../variables";
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
||||
let publicKeys;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
publicKeys = (
|
||||
await Membership.find({
|
||||
workspace: workspaceId,
|
||||
}).populate<{ user: IUser }>("user", "publicKey")
|
||||
).map((member) => {
|
||||
return {
|
||||
publicKey: member.user.publicKey,
|
||||
userId: member.user._id,
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to get workspace member public keys",
|
||||
});
|
||||
}
|
||||
const publicKeys = (
|
||||
await Membership.find({
|
||||
workspace: workspaceId,
|
||||
}).populate<{ user: IUser }>("user", "publicKey")
|
||||
).map((member) => {
|
||||
return {
|
||||
publicKey: member.user.publicKey,
|
||||
userId: member.user._id,
|
||||
};
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
publicKeys,
|
||||
@ -58,20 +49,11 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||
let users;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
users = await Membership.find({
|
||||
workspace: workspaceId,
|
||||
}).populate("user", "+publicKey");
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to get workspace members",
|
||||
});
|
||||
}
|
||||
const users = await Membership.find({
|
||||
workspace: workspaceId,
|
||||
}).populate("user", "+publicKey");
|
||||
|
||||
return res.status(200).send({
|
||||
users,
|
||||
@ -85,20 +67,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaces = async (req: Request, res: Response) => {
|
||||
let workspaces;
|
||||
try {
|
||||
workspaces = (
|
||||
await Membership.find({
|
||||
user: req.user._id,
|
||||
}).populate("workspace")
|
||||
).map((m) => m.workspace);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to get workspaces",
|
||||
});
|
||||
}
|
||||
const workspaces = (
|
||||
await Membership.find({
|
||||
user: req.user._id,
|
||||
}).populate("workspace")
|
||||
).map((m) => m.workspace);
|
||||
|
||||
return res.status(200).send({
|
||||
workspaces,
|
||||
@ -112,20 +85,11 @@ export const getWorkspaces = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspace = async (req: Request, res: Response) => {
|
||||
let workspace;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
workspace = await Workspace.findOne({
|
||||
_id: workspaceId,
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to get workspace",
|
||||
});
|
||||
}
|
||||
const workspace = await Workspace.findOne({
|
||||
_id: workspaceId,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
workspace,
|
||||
@ -140,43 +104,46 @@ export const getWorkspace = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const createWorkspace = async (req: Request, res: Response) => {
|
||||
let workspace;
|
||||
try {
|
||||
const { workspaceName, organizationId } = req.body;
|
||||
const { workspaceName, organizationId } = req.body;
|
||||
|
||||
// validate organization membership
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: req.user._id,
|
||||
organization: organizationId,
|
||||
});
|
||||
// validate organization membership
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: req.user._id,
|
||||
organization: organizationId,
|
||||
});
|
||||
|
||||
if (!membershipOrg) {
|
||||
throw new Error("Failed to validate organization membership");
|
||||
}
|
||||
|
||||
if (workspaceName.length < 1) {
|
||||
throw new Error("Workspace names must be at least 1-character long");
|
||||
}
|
||||
|
||||
// create workspace and add user as member
|
||||
workspace = await create({
|
||||
name: workspaceName,
|
||||
organizationId,
|
||||
});
|
||||
|
||||
await addMemberships({
|
||||
userIds: [req.user._id],
|
||||
workspaceId: workspace._id.toString(),
|
||||
roles: [ADMIN],
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to create workspace",
|
||||
});
|
||||
if (!membershipOrg) {
|
||||
throw new Error("Failed to validate organization membership");
|
||||
}
|
||||
|
||||
const plan = await EELicenseService.getOrganizationPlan(organizationId);
|
||||
|
||||
if (plan.workspaceLimit !== null) {
|
||||
// case: limit imposed on number of workspaces allowed
|
||||
if (plan.workspacesUsed >= plan.workspaceLimit) {
|
||||
// case: number of workspaces used exceeds the number of workspaces allowed
|
||||
return res.status(400).send({
|
||||
message: 'Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (workspaceName.length < 1) {
|
||||
throw new Error("Workspace names must be at least 1-character long");
|
||||
}
|
||||
|
||||
// create workspace and add user as member
|
||||
const workspace = await create({
|
||||
name: workspaceName,
|
||||
organizationId,
|
||||
});
|
||||
|
||||
await addMemberships({
|
||||
userIds: [req.user._id],
|
||||
workspaceId: workspace._id.toString(),
|
||||
roles: [ADMIN],
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
workspace,
|
||||
});
|
||||
@ -189,20 +156,12 @@ export const createWorkspace = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const deleteWorkspace = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// delete workspace
|
||||
await deleteWork({
|
||||
id: workspaceId,
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to delete workspace",
|
||||
});
|
||||
}
|
||||
// delete workspace
|
||||
await deleteWork({
|
||||
id: workspaceId,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully deleted workspace",
|
||||
@ -216,29 +175,20 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const changeWorkspaceName = async (req: Request, res: Response) => {
|
||||
let workspace;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { name } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
const { name } = req.body;
|
||||
|
||||
workspace = await Workspace.findOneAndUpdate(
|
||||
{
|
||||
_id: workspaceId,
|
||||
},
|
||||
{
|
||||
name,
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to change workspace name",
|
||||
});
|
||||
}
|
||||
const workspace = await Workspace.findOneAndUpdate(
|
||||
{
|
||||
_id: workspaceId,
|
||||
},
|
||||
{
|
||||
name,
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully changed workspace name",
|
||||
@ -253,20 +203,11 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
||||
let integrations;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
integrations = await Integration.find({
|
||||
workspace: workspaceId,
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to get workspace integrations",
|
||||
});
|
||||
}
|
||||
const integrations = await Integration.find({
|
||||
workspace: workspaceId,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
integrations,
|
||||
@ -283,20 +224,11 @@ export const getWorkspaceIntegrationAuthorizations = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
let authorizations;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
authorizations = await IntegrationAuth.find({
|
||||
workspace: workspaceId,
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to get workspace integration authorizations",
|
||||
});
|
||||
}
|
||||
const authorizations = await IntegrationAuth.find({
|
||||
workspace: workspaceId,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
authorizations,
|
||||
@ -313,21 +245,12 @@ export const getWorkspaceServiceTokens = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
let serviceTokens;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
// ?? FIX.
|
||||
serviceTokens = await ServiceToken.find({
|
||||
user: req.user._id,
|
||||
workspace: workspaceId,
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to get workspace service tokens",
|
||||
});
|
||||
}
|
||||
const { workspaceId } = req.params;
|
||||
// ?? FIX.
|
||||
const serviceTokens = await ServiceToken.find({
|
||||
user: req.user._id,
|
||||
workspace: workspaceId,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokens,
|
||||
|
@ -1,4 +1,3 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response } from 'express';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
@ -14,18 +13,9 @@ import { getSaltRounds } from '../../config';
|
||||
* @returns
|
||||
*/
|
||||
export const getAPIKeyData = async (req: Request, res: Response) => {
|
||||
let apiKeyData;
|
||||
try {
|
||||
apiKeyData = await APIKeyData.find({
|
||||
user: req.user._id
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get API key data'
|
||||
});
|
||||
}
|
||||
const apiKeyData = await APIKeyData.find({
|
||||
user: req.user._id
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
apiKeyData
|
||||
@ -38,38 +28,30 @@ export const getAPIKeyData = async (req: Request, res: Response) => {
|
||||
* @param res
|
||||
*/
|
||||
export const createAPIKeyData = async (req: Request, res: Response) => {
|
||||
let apiKey, apiKeyData;
|
||||
try {
|
||||
const { name, expiresIn } = req.body;
|
||||
|
||||
const secret = crypto.randomBytes(16).toString('hex');
|
||||
const secretHash = await bcrypt.hash(secret, getSaltRounds());
|
||||
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
|
||||
apiKeyData = await new APIKeyData({
|
||||
name,
|
||||
expiresAt,
|
||||
user: req.user._id,
|
||||
secretHash
|
||||
}).save();
|
||||
|
||||
// return api key data without sensitive data
|
||||
apiKeyData = await APIKeyData.findById(apiKeyData._id);
|
||||
|
||||
if (!apiKeyData) throw new Error('Failed to find API key data');
|
||||
|
||||
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to API key data'
|
||||
});
|
||||
}
|
||||
const { name, expiresIn } = req.body;
|
||||
|
||||
const secret = crypto.randomBytes(16).toString('hex');
|
||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
|
||||
let apiKeyData = await new APIKeyData({
|
||||
name,
|
||||
lastUsed: new Date(),
|
||||
expiresAt,
|
||||
user: req.user._id,
|
||||
secretHash
|
||||
}).save();
|
||||
|
||||
// return api key data without sensitive data
|
||||
// FIX: fix this any
|
||||
apiKeyData = await APIKeyData.findById(apiKeyData._id) as any
|
||||
|
||||
if (!apiKeyData) throw new Error('Failed to find API key data');
|
||||
|
||||
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
||||
|
||||
return res.status(200).send({
|
||||
apiKey,
|
||||
apiKeyData
|
||||
@ -83,21 +65,10 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const deleteAPIKeyData = async (req: Request, res: Response) => {
|
||||
let apiKeyData;
|
||||
try {
|
||||
const { apiKeyDataId } = req.params;
|
||||
|
||||
apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete API key data'
|
||||
});
|
||||
}
|
||||
const { apiKeyDataId } = req.params;
|
||||
const apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
|
||||
|
||||
return res.status(200).send({
|
||||
apiKeyData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { Request, Response } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import * as bigintConversion from 'bigint-conversion';
|
||||
const jsrp = require('jsrp');
|
||||
import { User, LoginSRPDetail } from '../../models';
|
||||
@ -17,9 +16,9 @@ import {
|
||||
} from '../../variables';
|
||||
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
|
||||
import {
|
||||
getNodeEnv,
|
||||
getJwtMfaLifetime,
|
||||
getJwtMfaSecret
|
||||
getJwtMfaSecret,
|
||||
getHttpsEnabled
|
||||
} from '../../config';
|
||||
|
||||
declare module 'jsonwebtoken' {
|
||||
@ -35,47 +34,40 @@ declare module 'jsonwebtoken' {
|
||||
* @returns
|
||||
*/
|
||||
export const login1 = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
email,
|
||||
clientPublicKey
|
||||
}: { email: string; clientPublicKey: string } = req.body;
|
||||
const {
|
||||
email,
|
||||
clientPublicKey
|
||||
}: { email: string; clientPublicKey: string } = req.body;
|
||||
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select('+salt +verifier');
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select('+salt +verifier');
|
||||
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier
|
||||
},
|
||||
async () => {
|
||||
// generate server-side public key
|
||||
const serverPublicKey = server.getPublicKey();
|
||||
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: email,
|
||||
clientPublicKey: clientPublicKey,
|
||||
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||
}, { upsert: true, returnNewDocument: false });
|
||||
await LoginSRPDetail.findOneAndReplace({ email: email }, {
|
||||
email: email,
|
||||
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'
|
||||
});
|
||||
}
|
||||
return res.status(200).send({
|
||||
serverPublicKey,
|
||||
salt: user.salt
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
@ -86,149 +78,143 @@ export const login1 = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const login2 = async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
|
||||
|
||||
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
|
||||
const { email, clientProof } = req.body;
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
|
||||
|
||||
const { email, clientProof } = req.body;
|
||||
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) throw new Error('Failed to find user');
|
||||
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
|
||||
|
||||
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
|
||||
if (!loginSRPDetail) {
|
||||
return BadRequestError(Error("Failed to find login details for SRP"))
|
||||
}
|
||||
|
||||
if (!loginSRPDetail) {
|
||||
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);
|
||||
|
||||
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
|
||||
|
||||
// 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: getJwtMfaLifetime(),
|
||||
secret: getJwtMfaSecret()
|
||||
});
|
||||
|
||||
const code = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email
|
||||
});
|
||||
|
||||
// send MFA code [code] to [email]
|
||||
await sendMail({
|
||||
template: 'emailMfa.handlebars',
|
||||
subjectLine: 'Infisical MFA code',
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
code
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
mfaEnabled: true,
|
||||
token
|
||||
});
|
||||
}
|
||||
|
||||
await checkUserDevice({
|
||||
user,
|
||||
ip: req.ip,
|
||||
userAgent: req.headers['user-agent'] ?? ''
|
||||
// generate temporary MFA token
|
||||
const token = createToken({
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: await getJwtMfaLifetime(),
|
||||
secret: await getJwtMfaSecret()
|
||||
});
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({ userId: user._id.toString() });
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: getNodeEnv() === 'production' ? true : false
|
||||
const code = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email
|
||||
});
|
||||
|
||||
// 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
|
||||
// send MFA code [code] to [email]
|
||||
await sendMail({
|
||||
template: 'emailMfa.handlebars',
|
||||
subjectLine: 'Infisical MFA code',
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
code
|
||||
}
|
||||
});
|
||||
|
||||
loginAction && await EELogService.createLog({
|
||||
userId: user._id,
|
||||
actions: [loginAction],
|
||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||
ipAddress: req.ip
|
||||
return res.status(200).send({
|
||||
mfaEnabled: true,
|
||||
token
|
||||
});
|
||||
|
||||
return res.status(200).send(response);
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: 'Failed to authenticate. Try again?'
|
||||
await checkUserDevice({
|
||||
user,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers['user-agent'] ?? ''
|
||||
});
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers['user-agent'] ?? ''
|
||||
});
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
|
||||
// case: user does not have MFA enabled
|
||||
// return (access) token in response
|
||||
|
||||
interface ResponseData {
|
||||
mfaEnabled: boolean;
|
||||
encryptionVersion: any;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
protectedKeyTag?: string;
|
||||
token: string;
|
||||
publicKey?: string;
|
||||
encryptedPrivateKey?: string;
|
||||
iv?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
const response: ResponseData = {
|
||||
mfaEnabled: false,
|
||||
encryptionVersion: user.encryptionVersion,
|
||||
token: tokens.token,
|
||||
publicKey: user.publicKey,
|
||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
tag: user.tag
|
||||
}
|
||||
|
||||
if (
|
||||
user?.protectedKey &&
|
||||
user?.protectedKeyIV &&
|
||||
user?.protectedKeyTag
|
||||
) {
|
||||
response.protectedKey = user.protectedKey;
|
||||
response.protectedKeyIV = user.protectedKeyIV
|
||||
response.protectedKeyTag = user.protectedKeyTag;
|
||||
}
|
||||
|
||||
const loginAction = await EELogService.createAction({
|
||||
name: ACTION_LOGIN,
|
||||
userId: user._id
|
||||
});
|
||||
|
||||
loginAction && await EELogService.createLog({
|
||||
userId: user._id,
|
||||
actions: [loginAction],
|
||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
return res.status(200).send(response);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to authenticate. Try again?'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: 'Failed to authenticate. Try again?'
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -237,30 +223,22 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
* @param res
|
||||
*/
|
||||
export const sendMfaToken = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
const { email } = req.body;
|
||||
|
||||
const code = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email
|
||||
});
|
||||
const code = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email
|
||||
});
|
||||
|
||||
// send MFA code [code] to [email]
|
||||
await sendMail({
|
||||
template: 'emailMfa.handlebars',
|
||||
subjectLine: 'Infisical MFA code',
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
code
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to send MFA code'
|
||||
});
|
||||
}
|
||||
// send MFA code [code] to [email]
|
||||
await sendMail({
|
||||
template: 'emailMfa.handlebars',
|
||||
subjectLine: 'Infisical MFA code',
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
code
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully sent new MFA code'
|
||||
@ -284,25 +262,31 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
|
||||
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
|
||||
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
|
||||
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
|
||||
await LoginSRPDetail.deleteOne({ userId: user.id })
|
||||
|
||||
await checkUserDevice({
|
||||
user,
|
||||
ip: req.ip,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers['user-agent'] ?? ''
|
||||
});
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({ userId: user._id.toString() });
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers['user-agent'] ?? ''
|
||||
});
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: getNodeEnv() === 'production' ? true : false
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
|
||||
interface VerifyMfaTokenRes {
|
||||
@ -353,7 +337,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
|
||||
userId: user._id,
|
||||
actions: [loginAction],
|
||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||
ipAddress: req.ip
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
return res.status(200).send(resObj);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Secret,
|
||||
ServiceToken,
|
||||
@ -11,7 +10,7 @@ import {
|
||||
import { SecretVersion } from '../../ee/models';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
import _ from 'lodash';
|
||||
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
|
||||
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
|
||||
|
||||
/**
|
||||
* Create new workspace environment named [environmentName] under workspace with id
|
||||
@ -25,30 +24,22 @@ export const createWorkspaceEnvironment = async (
|
||||
) => {
|
||||
const { workspaceId } = req.params;
|
||||
const { environmentName, environmentSlug } = req.body;
|
||||
try {
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
if (
|
||||
!workspace ||
|
||||
workspace?.environments.find(
|
||||
({ name, slug }) => slug === environmentSlug || environmentName === name
|
||||
)
|
||||
) {
|
||||
throw new Error('Failed to create workspace environment');
|
||||
}
|
||||
|
||||
workspace?.environments.push({
|
||||
name: environmentName,
|
||||
slug: environmentSlug.toLowerCase(),
|
||||
});
|
||||
await workspace.save();
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to create new workspace environment',
|
||||
});
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
if (
|
||||
!workspace ||
|
||||
workspace?.environments.find(
|
||||
({ name, slug }) => slug === environmentSlug || environmentName === name
|
||||
)
|
||||
) {
|
||||
throw new Error('Failed to create workspace environment');
|
||||
}
|
||||
|
||||
workspace?.environments.push({
|
||||
name: environmentName,
|
||||
slug: environmentSlug.toLowerCase(),
|
||||
});
|
||||
await workspace.save();
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully created new environment',
|
||||
workspace: workspaceId,
|
||||
@ -72,75 +63,67 @@ export const renameWorkspaceEnvironment = async (
|
||||
) => {
|
||||
const { workspaceId } = req.params;
|
||||
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
|
||||
try {
|
||||
// user should pass both new slug and env name
|
||||
if (!environmentSlug || !environmentName) {
|
||||
throw new Error('Invalid environment given.');
|
||||
}
|
||||
|
||||
// atomic update the env to avoid conflict
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
if (!workspace) {
|
||||
throw new Error('Failed to create workspace environment');
|
||||
}
|
||||
|
||||
const isEnvExist = workspace.environments.some(
|
||||
({ name, slug }) =>
|
||||
slug !== oldEnvironmentSlug &&
|
||||
(name === environmentName || slug === environmentSlug)
|
||||
);
|
||||
if (isEnvExist) {
|
||||
throw new Error('Invalid environment given');
|
||||
}
|
||||
|
||||
const envIndex = workspace?.environments.findIndex(
|
||||
({ slug }) => slug === oldEnvironmentSlug
|
||||
);
|
||||
if (envIndex === -1) {
|
||||
throw new Error('Invalid environment given');
|
||||
}
|
||||
|
||||
workspace.environments[envIndex].name = environmentName;
|
||||
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
|
||||
|
||||
await workspace.save();
|
||||
await Secret.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await SecretVersion.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await ServiceToken.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await ServiceTokenData.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await Integration.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await Membership.updateMany(
|
||||
{
|
||||
workspace: workspaceId,
|
||||
"deniedPermissions.environmentSlug": oldEnvironmentSlug
|
||||
},
|
||||
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
|
||||
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
|
||||
)
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to update workspace environment',
|
||||
});
|
||||
// user should pass both new slug and env name
|
||||
if (!environmentSlug || !environmentName) {
|
||||
throw new Error('Invalid environment given.');
|
||||
}
|
||||
|
||||
// atomic update the env to avoid conflict
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
if (!workspace) {
|
||||
throw new Error('Failed to create workspace environment');
|
||||
}
|
||||
|
||||
const isEnvExist = workspace.environments.some(
|
||||
({ name, slug }) =>
|
||||
slug !== oldEnvironmentSlug &&
|
||||
(name === environmentName || slug === environmentSlug)
|
||||
);
|
||||
if (isEnvExist) {
|
||||
throw new Error('Invalid environment given');
|
||||
}
|
||||
|
||||
const envIndex = workspace?.environments.findIndex(
|
||||
({ slug }) => slug === oldEnvironmentSlug
|
||||
);
|
||||
if (envIndex === -1) {
|
||||
throw new Error('Invalid environment given');
|
||||
}
|
||||
|
||||
workspace.environments[envIndex].name = environmentName;
|
||||
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
|
||||
|
||||
await workspace.save();
|
||||
await Secret.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await SecretVersion.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await ServiceToken.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await ServiceTokenData.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await Integration.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await Membership.updateMany(
|
||||
{
|
||||
workspace: workspaceId,
|
||||
"deniedPermissions.environmentSlug": oldEnvironmentSlug
|
||||
},
|
||||
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
|
||||
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
|
||||
)
|
||||
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully update environment',
|
||||
workspace: workspaceId,
|
||||
@ -163,57 +146,48 @@ export const deleteWorkspaceEnvironment = async (
|
||||
) => {
|
||||
const { workspaceId } = req.params;
|
||||
const { environmentSlug } = req.body;
|
||||
try {
|
||||
// atomic update the env to avoid conflict
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
if (!workspace) {
|
||||
throw new Error('Failed to create workspace environment');
|
||||
}
|
||||
|
||||
const envIndex = workspace?.environments.findIndex(
|
||||
({ slug }) => slug === environmentSlug
|
||||
);
|
||||
if (envIndex === -1) {
|
||||
throw new Error('Invalid environment given');
|
||||
}
|
||||
|
||||
workspace.environments.splice(envIndex, 1);
|
||||
await workspace.save();
|
||||
|
||||
// clean up
|
||||
await Secret.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
await SecretVersion.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
await ServiceToken.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
await ServiceTokenData.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
await Integration.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
await Membership.updateMany(
|
||||
{ workspace: workspaceId },
|
||||
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
|
||||
)
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete workspace environment',
|
||||
});
|
||||
// atomic update the env to avoid conflict
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
if (!workspace) {
|
||||
throw new Error('Failed to create workspace environment');
|
||||
}
|
||||
|
||||
const envIndex = workspace?.environments.findIndex(
|
||||
({ slug }) => slug === environmentSlug
|
||||
);
|
||||
if (envIndex === -1) {
|
||||
throw new Error('Invalid environment given');
|
||||
}
|
||||
|
||||
workspace.environments.splice(envIndex, 1);
|
||||
await workspace.save();
|
||||
|
||||
// clean up
|
||||
await Secret.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
await SecretVersion.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
await ServiceToken.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
await ServiceTokenData.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
await Integration.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
await Membership.updateMany(
|
||||
{ workspace: workspaceId },
|
||||
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
|
||||
)
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully deleted environment',
|
||||
workspace: workspaceId,
|
||||
@ -244,8 +218,8 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (
|
||||
throw BadRequestError()
|
||||
}
|
||||
relatedWorkspace.environments.forEach(environment => {
|
||||
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ })
|
||||
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE })
|
||||
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS })
|
||||
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS })
|
||||
if (isReadBlocked && isWriteBlocked) {
|
||||
return
|
||||
} else {
|
||||
|
@ -7,6 +7,7 @@ import * as serviceTokenDataController from './serviceTokenDataController';
|
||||
import * as apiKeyDataController from './apiKeyDataController';
|
||||
import * as secretController from './secretController';
|
||||
import * as secretsController from './secretsController';
|
||||
import * as serviceAccountsController from './serviceAccountsController';
|
||||
import * as environmentController from './environmentController';
|
||||
import * as tagController from './tagController';
|
||||
|
||||
@ -20,6 +21,7 @@ export {
|
||||
apiKeyDataController,
|
||||
secretController,
|
||||
secretsController,
|
||||
serviceAccountsController,
|
||||
environmentController,
|
||||
tagController
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
MembershipOrg,
|
||||
Membership,
|
||||
Workspace
|
||||
Workspace,
|
||||
ServiceAccount
|
||||
} from '../../models';
|
||||
import { deleteMembershipOrg } from '../../helpers/membershipOrg';
|
||||
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
||||
@ -47,20 +48,11 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
|
||||
}
|
||||
}
|
||||
*/
|
||||
let memberships;
|
||||
try {
|
||||
const { organizationId } = req.params;
|
||||
const { organizationId } = req.params;
|
||||
|
||||
memberships = await MembershipOrg.find({
|
||||
const memberships = await MembershipOrg.find({
|
||||
organization: organizationId
|
||||
}).populate('user', '+publicKey');
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get organization memberships'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
memberships
|
||||
@ -126,26 +118,17 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
|
||||
}
|
||||
}
|
||||
*/
|
||||
let membership;
|
||||
try {
|
||||
const { membershipId } = req.params;
|
||||
const { role } = req.body;
|
||||
|
||||
membership = await MembershipOrg.findByIdAndUpdate(
|
||||
membershipId,
|
||||
{
|
||||
role
|
||||
}, {
|
||||
new: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to update organization membership'
|
||||
});
|
||||
}
|
||||
const { membershipId } = req.params;
|
||||
const { role } = req.body;
|
||||
|
||||
const membership = await MembershipOrg.findByIdAndUpdate(
|
||||
membershipId,
|
||||
{
|
||||
role
|
||||
}, {
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
membership
|
||||
@ -195,25 +178,16 @@ export const deleteOrganizationMembership = async (req: Request, res: Response)
|
||||
}
|
||||
}
|
||||
*/
|
||||
let membership;
|
||||
try {
|
||||
const { membershipId } = req.params;
|
||||
|
||||
// delete organization membership
|
||||
membership = await deleteMembershipOrg({
|
||||
membershipOrgId: membershipId
|
||||
});
|
||||
const { membershipId } = req.params;
|
||||
|
||||
// delete organization membership
|
||||
const membership = await deleteMembershipOrg({
|
||||
membershipOrgId: membershipId
|
||||
});
|
||||
|
||||
await updateSubscriptionOrgQuantity({
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId: membership.organization.toString()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete organization membership'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
membership
|
||||
@ -260,37 +234,45 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
|
||||
}
|
||||
}
|
||||
*/
|
||||
let workspaces;
|
||||
try {
|
||||
const { organizationId } = req.params;
|
||||
const { organizationId } = req.params;
|
||||
|
||||
const workspacesSet = new Set(
|
||||
(
|
||||
await Workspace.find(
|
||||
{
|
||||
organization: organizationId
|
||||
},
|
||||
'_id'
|
||||
)
|
||||
).map((w) => w._id.toString())
|
||||
);
|
||||
const workspacesSet = new Set(
|
||||
(
|
||||
await Workspace.find(
|
||||
{
|
||||
organization: organizationId
|
||||
},
|
||||
'_id'
|
||||
)
|
||||
).map((w) => w._id.toString())
|
||||
);
|
||||
|
||||
workspaces = (
|
||||
await Membership.find({
|
||||
user: req.user._id
|
||||
}).populate('workspace')
|
||||
)
|
||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||
.map((m) => m.workspace);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get organization workspaces'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
const workspaces = (
|
||||
await Membership.find({
|
||||
user: req.user._id
|
||||
}).populate('workspace')
|
||||
)
|
||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||
.map((m) => m.workspace);
|
||||
|
||||
return res.status(200).send({
|
||||
workspaces
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return service accounts for organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getOrganizationServiceAccounts = async (req: Request, res: Response) => {
|
||||
const { organizationId } = req.params;
|
||||
|
||||
const serviceAccounts = await ServiceAccount.find({
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccounts
|
||||
});
|
||||
}
|
||||
|
@ -6,8 +6,10 @@ 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 { getPostHogClient } from '../../services';
|
||||
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';
|
||||
|
||||
/**
|
||||
* Create secret for workspace with id [workspaceId] and environment [environment]
|
||||
@ -15,7 +17,7 @@ import { getPostHogClient } from '../../services';
|
||||
* @param res
|
||||
*/
|
||||
export const createSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const secretToCreate: CreateSecretRequestBody = req.body.secret;
|
||||
const { workspaceId, environment } = req.params
|
||||
const sanitizedSecret: SanitizedSecretForCreate = {
|
||||
@ -34,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
|
||||
}
|
||||
|
||||
|
||||
@ -68,7 +72,7 @@ export const createSecret = async (req: Request, res: Response) => {
|
||||
* @param res
|
||||
*/
|
||||
export const createSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
|
||||
const { workspaceId, environment } = req.params
|
||||
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
|
||||
@ -90,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)
|
||||
@ -130,7 +136,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const { workspaceId, environmentName } = req.params
|
||||
const secretIdsToDelete: string[] = req.body.secretIds
|
||||
|
||||
@ -184,7 +190,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
await Secret.findByIdAndDelete(req._secret._id)
|
||||
|
||||
if (postHogClient) {
|
||||
@ -213,7 +219,7 @@ export const deleteSecret = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const updateSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = 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())
|
||||
@ -281,7 +287,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const updateSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const { workspaceId, environmentName } = req.params
|
||||
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
|
||||
|
||||
@ -335,20 +341,23 @@ export const updateSecret = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const getSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const { environment } = req.query;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
|
||||
let userEmail: Types.ObjectId | undefined = undefined // used for posthog
|
||||
let userEmail: string | undefined = undefined // used for posthog
|
||||
if (req.user) {
|
||||
userId = req.user._id;
|
||||
userEmail = req.user.email;
|
||||
}
|
||||
|
||||
if (req.serviceTokenData) {
|
||||
userId = req.serviceTokenData.user._id
|
||||
userEmail = req.serviceTokenData.user.email;
|
||||
userId = req.serviceTokenData.user;
|
||||
|
||||
const user = await User.findById(req.serviceTokenData.user, 'email');
|
||||
if (!user) throw AccountNotFoundError();
|
||||
userEmail = user.email;
|
||||
}
|
||||
|
||||
const [err, secrets] = await to(Secret.find(
|
||||
|
File diff suppressed because it is too large
Load Diff
306
backend/src/controllers/v2/serviceAccountsController.ts
Normal file
306
backend/src/controllers/v2/serviceAccountsController.ts
Normal file
@ -0,0 +1,306 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import {
|
||||
ServiceAccount,
|
||||
ServiceAccountKey,
|
||||
ServiceAccountOrganizationPermission,
|
||||
ServiceAccountWorkspacePermission
|
||||
} from '../../models';
|
||||
import {
|
||||
CreateServiceAccountDto
|
||||
} from '../../interfaces/serviceAccounts/dto';
|
||||
import { BadRequestError, ServiceAccountNotFoundError } from '../../utils/errors';
|
||||
import { getSaltRounds } from '../../config';
|
||||
|
||||
/**
|
||||
* Return service account tied to the request (service account) client
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getCurrentServiceAccount = async (req: Request, res: Response) => {
|
||||
const serviceAccount = await ServiceAccount.findById(req.serviceAccount._id);
|
||||
|
||||
if (!serviceAccount) {
|
||||
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccount
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getServiceAccountById = async (req: Request, res: Response) => {
|
||||
const { serviceAccountId } = req.params;
|
||||
|
||||
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
|
||||
|
||||
if (!serviceAccount) {
|
||||
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccount
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new service account under organization with id [organizationId]
|
||||
* that has access to workspaces [workspaces]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createServiceAccount = async (req: Request, res: Response) => {
|
||||
const {
|
||||
name,
|
||||
organizationId,
|
||||
publicKey,
|
||||
expiresIn,
|
||||
}: CreateServiceAccountDto = req.body;
|
||||
|
||||
let expiresAt;
|
||||
if (expiresIn) {
|
||||
expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
}
|
||||
|
||||
const secret = crypto.randomBytes(16).toString('base64');
|
||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||
|
||||
// create service account
|
||||
const serviceAccount = await new ServiceAccount({
|
||||
name,
|
||||
organization: new Types.ObjectId(organizationId),
|
||||
user: req.user,
|
||||
publicKey,
|
||||
lastUsed: new Date(),
|
||||
expiresAt,
|
||||
secretHash
|
||||
}).save();
|
||||
|
||||
const serviceAccountObj = serviceAccount.toObject();
|
||||
|
||||
delete serviceAccountObj.secretHash;
|
||||
|
||||
// provision default org-level permission for service account
|
||||
await new ServiceAccountOrganizationPermission({
|
||||
serviceAccount: serviceAccount._id
|
||||
}).save();
|
||||
|
||||
const secretId = Buffer.from(serviceAccount._id.toString(), 'hex').toString('base64');
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccountAccessKey: `sa.${secretId}.${secret}`,
|
||||
serviceAccount: serviceAccountObj
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change name of service account with id [serviceAccountId] to [name]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const changeServiceAccountName = async (req: Request, res: Response) => {
|
||||
const { serviceAccountId } = req.params;
|
||||
const { name } = req.body;
|
||||
|
||||
const serviceAccount = await ServiceAccount.findOneAndUpdate(
|
||||
{
|
||||
_id: new Types.ObjectId(serviceAccountId)
|
||||
},
|
||||
{
|
||||
name
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccount
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a service account key to service account with id [serviceAccountId]
|
||||
* for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const addServiceAccountKey = async (req: Request, res: Response) => {
|
||||
const {
|
||||
workspaceId,
|
||||
encryptedKey,
|
||||
nonce
|
||||
} = req.body;
|
||||
|
||||
const serviceAccountKey = await new ServiceAccountKey({
|
||||
encryptedKey,
|
||||
nonce,
|
||||
sender: req.user._id,
|
||||
serviceAccount: req.serviceAccount._d,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
}).save();
|
||||
|
||||
return serviceAccountKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return workspace-level permission for service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getServiceAccountWorkspacePermissions = async (req: Request, res: Response) => {
|
||||
const serviceAccountWorkspacePermissions = await ServiceAccountWorkspacePermission.find({
|
||||
serviceAccount: req.serviceAccount._id
|
||||
}).populate('workspace');
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccountWorkspacePermissions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a workspace permission to service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const addServiceAccountWorkspacePermission = async (req: Request, res: Response) => {
|
||||
const { serviceAccountId } = req.params;
|
||||
const {
|
||||
environment,
|
||||
workspaceId,
|
||||
read = false,
|
||||
write = false,
|
||||
encryptedKey,
|
||||
nonce
|
||||
} = req.body;
|
||||
|
||||
if (!req.membership.workspace.environments.some((e: { name: string; slug: string }) => e.slug === environment)) {
|
||||
return res.status(400).send({
|
||||
message: 'Failed to validate workspace environment'
|
||||
});
|
||||
}
|
||||
|
||||
const existingPermission = await ServiceAccountWorkspacePermission.findOne({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId),
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment
|
||||
});
|
||||
|
||||
if (existingPermission) throw BadRequestError({ message: 'Failed to add workspace permission to service account due to already-existing ' });
|
||||
|
||||
const serviceAccountWorkspacePermission = await new ServiceAccountWorkspacePermission({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId),
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
read,
|
||||
write
|
||||
}).save();
|
||||
|
||||
const existingServiceAccountKey = await ServiceAccountKey.findOne({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId),
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!existingServiceAccountKey) {
|
||||
await new ServiceAccountKey({
|
||||
encryptedKey,
|
||||
nonce,
|
||||
sender: req.user._id,
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId),
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
}).save();
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccountWorkspacePermission
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete workspace permission from service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteServiceAccountWorkspacePermission = async (req: Request, res: Response) => {
|
||||
const { serviceAccountWorkspacePermissionId } = req.params;
|
||||
const serviceAccountWorkspacePermission = await ServiceAccountWorkspacePermission.findByIdAndDelete(serviceAccountWorkspacePermissionId);
|
||||
|
||||
if (serviceAccountWorkspacePermission) {
|
||||
const { serviceAccount, workspace } = serviceAccountWorkspacePermission;
|
||||
const count = await ServiceAccountWorkspacePermission.countDocuments({
|
||||
serviceAccount,
|
||||
workspace
|
||||
});
|
||||
|
||||
if (count === 0) {
|
||||
await ServiceAccountKey.findOneAndDelete({
|
||||
serviceAccount,
|
||||
workspace
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccountWorkspacePermission
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteServiceAccount = async (req: Request, res: Response) => {
|
||||
const { serviceAccountId } = req.params;
|
||||
|
||||
const serviceAccount = await ServiceAccount.findByIdAndDelete(serviceAccountId);
|
||||
|
||||
if (serviceAccount) {
|
||||
await ServiceAccountKey.deleteMany({
|
||||
serviceAccount: serviceAccount._id
|
||||
});
|
||||
|
||||
await ServiceAccountOrganizationPermission.deleteMany({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId)
|
||||
});
|
||||
|
||||
await ServiceAccountWorkspacePermission.deleteMany({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId)
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccount
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return service account keys for service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getServiceAccountKeys = async (req: Request, res: Response) => {
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
|
||||
const serviceAccountKeys = await ServiceAccountKey.find({
|
||||
serviceAccount: req.serviceAccount._id,
|
||||
...(workspaceId ? { workspace: new Types.ObjectId(workspaceId) } : {})
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccountKeys
|
||||
});
|
||||
}
|
@ -1,22 +1,27 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response } from 'express';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { Request, Response } from "express";
|
||||
import crypto from "crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import { User, ServiceAccount, ServiceTokenData } from "../../models";
|
||||
import { userHasWorkspaceAccess } from "../../ee/helpers/checkMembershipPermissions";
|
||||
import {
|
||||
ServiceTokenData
|
||||
} from '../../models';
|
||||
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
|
||||
import { ABILITY_READ } from '../../variables/organization';
|
||||
import { getSaltRounds } from '../../config';
|
||||
PERMISSION_READ_SECRETS,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
} from "../../variables";
|
||||
import { getSaltRounds } from "../../config";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import Folder from "../../models/folder";
|
||||
import { getFolderByPath } from "../../services/FolderService";
|
||||
|
||||
/**
|
||||
* Return service token data associated with service token on request
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getServiceTokenData = async (req: Request, res: Response) => {
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Return Infisical Token data'
|
||||
#swagger.description = 'Return Infisical Token data'
|
||||
|
||||
@ -29,115 +34,135 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"serviceTokenData": {
|
||||
"type": "object",
|
||||
$ref: "#/components/schemas/ServiceTokenData",
|
||||
"description": "Details of service token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return res.status(200).json(req.serviceTokenData);
|
||||
}
|
||||
if (!(req.authData.authPayload instanceof ServiceTokenData))
|
||||
throw BadRequestError({
|
||||
message: "Failed accepted client validation for service token data",
|
||||
});
|
||||
|
||||
const serviceTokenData = await ServiceTokenData.findById(
|
||||
req.authData.authPayload._id
|
||||
)
|
||||
.select("+encryptedKey +iv +tag")
|
||||
.populate("user");
|
||||
|
||||
return res.status(200).json(serviceTokenData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new service token data for workspace with id [workspaceId] and
|
||||
* environment [environment].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
let serviceToken, serviceTokenData;
|
||||
let serviceTokenData;
|
||||
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
workspaceId,
|
||||
environment,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
expiresIn,
|
||||
permissions
|
||||
} = req.body;
|
||||
const {
|
||||
name,
|
||||
workspaceId,
|
||||
environment,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
expiresIn,
|
||||
secretPath,
|
||||
permissions,
|
||||
} = req.body;
|
||||
|
||||
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ)
|
||||
if (!hasAccess) {
|
||||
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
||||
}
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
});
|
||||
|
||||
const secret = crypto.randomBytes(16).toString('hex');
|
||||
const secretHash = await bcrypt.hash(secret, getSaltRounds());
|
||||
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
|
||||
serviceTokenData = await new ServiceTokenData({
|
||||
name,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
user: req.user._id,
|
||||
expiresAt,
|
||||
secretHash,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
permissions
|
||||
}).save();
|
||||
|
||||
// return service token data without sensitive data
|
||||
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
|
||||
|
||||
if (!serviceTokenData) throw new Error('Failed to find service token data');
|
||||
|
||||
serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to create service token data'
|
||||
});
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (folder == undefined) {
|
||||
throw BadRequestError({ message: "Path for service token does not exist" })
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceToken,
|
||||
serviceTokenData
|
||||
});
|
||||
}
|
||||
const secret = crypto.randomBytes(16).toString("hex");
|
||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||
|
||||
let expiresAt;
|
||||
if (expiresIn) {
|
||||
expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
}
|
||||
|
||||
let user, serviceAccount;
|
||||
|
||||
if (
|
||||
req.authData.authMode === AUTH_MODE_JWT &&
|
||||
req.authData.authPayload instanceof User
|
||||
) {
|
||||
user = req.authData.authPayload._id;
|
||||
}
|
||||
|
||||
if (
|
||||
req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
|
||||
req.authData.authPayload instanceof ServiceAccount
|
||||
) {
|
||||
serviceAccount = req.authData.authPayload._id;
|
||||
}
|
||||
|
||||
serviceTokenData = await new ServiceTokenData({
|
||||
name,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
user,
|
||||
serviceAccount,
|
||||
lastUsed: new Date(),
|
||||
expiresAt,
|
||||
secretHash,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
secretPath,
|
||||
permissions,
|
||||
}).save();
|
||||
|
||||
// return service token data without sensitive data
|
||||
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
|
||||
|
||||
if (!serviceTokenData) throw new Error("Failed to find service token data");
|
||||
|
||||
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
|
||||
|
||||
return res.status(200).send({
|
||||
serviceToken,
|
||||
serviceTokenData,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete service token data with id [serviceTokenDataId].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||
let serviceTokenData;
|
||||
try {
|
||||
const { serviceTokenDataId } = req.params;
|
||||
const { serviceTokenDataId } = req.params;
|
||||
|
||||
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
|
||||
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(
|
||||
serviceTokenDataId
|
||||
);
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete service token data'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
}
|
||||
|
||||
function UnauthorizedRequestError(arg0: { message: string; }) {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
return res.status(200).send({
|
||||
serviceTokenData,
|
||||
});
|
||||
};
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { User, MembershipOrg } from '../../models';
|
||||
import { completeAccount } from '../../helpers/user';
|
||||
import {
|
||||
@ -7,8 +6,9 @@ import {
|
||||
} from '../../helpers/signup';
|
||||
import { issueAuthTokens } from '../../helpers/auth';
|
||||
import { INVITED, ACCEPTED } from '../../variables';
|
||||
import request from '../../config/request';
|
||||
import { getNodeEnv, getLoopsApiKey } from '../../config';
|
||||
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
|
||||
@ -19,123 +19,130 @@ import { getNodeEnv, getLoopsApiKey } from '../../config';
|
||||
*/
|
||||
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
|
||||
}: {
|
||||
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;
|
||||
} = req.body;
|
||||
const {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
organizationName
|
||||
}: {
|
||||
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;
|
||||
} = req.body;
|
||||
|
||||
// get user
|
||||
user = await User.findOne({ email });
|
||||
|
||||
if (!user || (user && user?.publicKey)) {
|
||||
// case 1: user doesn't exist.
|
||||
// case 2: user has already completed account
|
||||
return res.status(403).send({
|
||||
error: 'Failed to complete account for complete user'
|
||||
});
|
||||
}
|
||||
// get user
|
||||
user = await User.findOne({ email });
|
||||
|
||||
// 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 || (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 (!user)
|
||||
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
|
||||
// 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
|
||||
});
|
||||
|
||||
// initialize default organization and workspace
|
||||
await initializeDefaultOrg({
|
||||
organizationName,
|
||||
user
|
||||
});
|
||||
if (!user)
|
||||
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
|
||||
|
||||
// update organization membership statuses that are
|
||||
// invited to completed with user attached
|
||||
await MembershipOrg.updateMany(
|
||||
{
|
||||
inviteEmail: email,
|
||||
status: INVITED
|
||||
},
|
||||
{
|
||||
user,
|
||||
status: ACCEPTED
|
||||
}
|
||||
);
|
||||
// initialize default organization and workspace
|
||||
await initializeDefaultOrg({
|
||||
organizationName,
|
||||
user
|
||||
});
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id.toString()
|
||||
});
|
||||
// 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()
|
||||
});
|
||||
});
|
||||
|
||||
token = tokens.token;
|
||||
// update organization membership statuses that are
|
||||
// invited to completed with user attached
|
||||
await MembershipOrg.updateMany(
|
||||
{
|
||||
inviteEmail: email,
|
||||
status: INVITED
|
||||
},
|
||||
{
|
||||
user,
|
||||
status: ACCEPTED
|
||||
}
|
||||
);
|
||||
|
||||
// sending a welcome email to new users
|
||||
if (getLoopsApiKey()) {
|
||||
await request.post("https://app.loops.so/api/v1/events/send", {
|
||||
"email": email,
|
||||
"eventName": "Sign Up",
|
||||
"firstName": firstName,
|
||||
"lastName": lastName
|
||||
}, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Authorization": "Bearer " + getLoopsApiKey()
|
||||
},
|
||||
});
|
||||
}
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers['user-agent'] ?? ''
|
||||
});
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: getNodeEnv() === 'production' ? true : false
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to complete account setup'
|
||||
});
|
||||
}
|
||||
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()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully set up account',
|
||||
@ -153,98 +160,103 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const completeAccountInvite = async (req: Request, res: Response) => {
|
||||
let user, token, refreshToken;
|
||||
try {
|
||||
const {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
} = req.body;
|
||||
const {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
} = req.body;
|
||||
|
||||
// get user
|
||||
user = await User.findOne({ email });
|
||||
// get user
|
||||
user = await User.findOne({ email });
|
||||
|
||||
if (!user || (user && user?.publicKey)) {
|
||||
// case 1: user doesn't exist.
|
||||
// case 2: user has already completed account
|
||||
return res.status(403).send({
|
||||
error: 'Failed to complete account for complete user'
|
||||
});
|
||||
}
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: email,
|
||||
status: INVITED
|
||||
});
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: email,
|
||||
status: INVITED
|
||||
});
|
||||
|
||||
if (!membershipOrg) throw new Error('Failed to find invitations for email');
|
||||
if (!membershipOrg) throw new Error('Failed to find invitations for email');
|
||||
|
||||
// complete setting up user's account
|
||||
user = await completeAccount({
|
||||
userId: user._id.toString(),
|
||||
firstName,
|
||||
lastName,
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
});
|
||||
// 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');
|
||||
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()
|
||||
});
|
||||
});
|
||||
|
||||
// update organization membership statuses that are
|
||||
// invited to completed with user attached
|
||||
await MembershipOrg.updateMany(
|
||||
{
|
||||
inviteEmail: email,
|
||||
status: INVITED
|
||||
},
|
||||
{
|
||||
user,
|
||||
status: ACCEPTED
|
||||
}
|
||||
);
|
||||
await MembershipOrg.updateMany(
|
||||
{
|
||||
inviteEmail: email,
|
||||
status: INVITED
|
||||
},
|
||||
{
|
||||
user,
|
||||
status: ACCEPTED
|
||||
}
|
||||
);
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id.toString()
|
||||
});
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers['user-agent'] ?? ''
|
||||
});
|
||||
|
||||
token = tokens.token;
|
||||
token = tokens.token;
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: getNodeEnv() === 'production' ? true : false
|
||||
});
|
||||
} 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
|
||||
});
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Membership, Secret,
|
||||
@ -69,4 +68,4 @@ export const getWorkspaceTags = async (req: Request, res: Response) => {
|
||||
return res.json({
|
||||
workspaceTags
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
User,
|
||||
MembershipOrg
|
||||
@ -37,18 +36,9 @@ export const getMe = async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
*/
|
||||
let user;
|
||||
try {
|
||||
user = await User
|
||||
.findById(req.user._id)
|
||||
.select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get current user'
|
||||
});
|
||||
}
|
||||
const user = await User
|
||||
.findById(req.user._id)
|
||||
.select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
@ -64,29 +54,20 @@ export const getMe = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
|
||||
let user;
|
||||
try {
|
||||
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
|
||||
req.user.isMfaEnabled = isMfaEnabled;
|
||||
|
||||
if (isMfaEnabled) {
|
||||
// TODO: adapt this route/controller
|
||||
// to work for different forms of MFA
|
||||
req.user.mfaMethods = ['email'];
|
||||
} else {
|
||||
req.user.mfaMethods = [];
|
||||
}
|
||||
|
||||
await req.user.save();
|
||||
|
||||
user = req.user;
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to update current user's MFA status"
|
||||
});
|
||||
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
|
||||
req.user.isMfaEnabled = isMfaEnabled;
|
||||
|
||||
if (isMfaEnabled) {
|
||||
// TODO: adapt this route/controller
|
||||
// to work for different forms of MFA
|
||||
req.user.mfaMethods = ['email'];
|
||||
} else {
|
||||
req.user.mfaMethods = [];
|
||||
}
|
||||
|
||||
await req.user.save();
|
||||
|
||||
const user = req.user;
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
@ -126,22 +107,13 @@ export const getMyOrganizations = async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
*/
|
||||
let organizations;
|
||||
try {
|
||||
organizations = (
|
||||
await MembershipOrg.find({
|
||||
user: req.user._id
|
||||
}).populate('organization')
|
||||
).map((m) => m.organization);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to get current user's organizations"
|
||||
});
|
||||
}
|
||||
const organizations = (
|
||||
await MembershipOrg.find({
|
||||
user: req.user._id
|
||||
}).populate('organization')
|
||||
).map((m) => m.organization);
|
||||
|
||||
return res.status(200).send({
|
||||
organizations
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Workspace,
|
||||
@ -19,7 +18,7 @@ import {
|
||||
reformatPullSecrets
|
||||
} from '../../helpers/secret';
|
||||
import { pushKeys } from '../../helpers/key';
|
||||
import { getPostHogClient, EventService } from '../../services';
|
||||
import { TelemetryService, EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
|
||||
interface V2PushSecret {
|
||||
@ -47,65 +46,57 @@ interface V2PushSecret {
|
||||
*/
|
||||
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
try {
|
||||
const postHogClient = getPostHogClient();
|
||||
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
// sanitize secrets
|
||||
secrets = secrets.filter(
|
||||
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
|
||||
);
|
||||
// sanitize secrets
|
||||
secrets = secrets.filter(
|
||||
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
|
||||
);
|
||||
|
||||
await push({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets,
|
||||
channel: channel ? channel : 'cli',
|
||||
ipAddress: req.ip
|
||||
});
|
||||
await push({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets,
|
||||
channel: channel ? channel : 'cli',
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
await pushKeys({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
keys
|
||||
});
|
||||
await pushKeys({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
keys
|
||||
});
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets pushed',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets pushed',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId
|
||||
})
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to upload workspace secrets'
|
||||
});
|
||||
}
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment
|
||||
})
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully uploaded workspace secrets'
|
||||
@ -121,56 +112,48 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const pullSecrets = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
try {
|
||||
const postHogClient = getPostHogClient();
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
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;
|
||||
|
||||
let userId;
|
||||
if (req.user) {
|
||||
userId = req.user._id.toString();
|
||||
} else if (req.serviceTokenData) {
|
||||
userId = req.serviceTokenData.user._id
|
||||
}
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
let userId;
|
||||
if (req.user) {
|
||||
userId = req.user._id.toString();
|
||||
} else if (req.serviceTokenData) {
|
||||
userId = req.serviceTokenData.user.toString();
|
||||
}
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
secrets = await pull({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: channel ? channel : 'cli',
|
||||
ipAddress: req.ip
|
||||
});
|
||||
secrets = await pull({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: channel ? channel : 'cli',
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
if (channel !== 'cli') {
|
||||
secrets = reformatPullSecrets({ secrets });
|
||||
}
|
||||
if (channel !== 'cli') {
|
||||
secrets = reformatPullSecrets({ secrets });
|
||||
}
|
||||
|
||||
if (postHogClient) {
|
||||
// capture secrets pushed event in production
|
||||
postHogClient.capture({
|
||||
distinctId: req.user.email,
|
||||
event: 'secrets pulled',
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to pull workspace secrets'
|
||||
});
|
||||
}
|
||||
if (postHogClient) {
|
||||
// capture secrets pushed event in production
|
||||
postHogClient.capture({
|
||||
distinctId: req.user.email,
|
||||
event: 'secrets pulled',
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets
|
||||
@ -207,22 +190,14 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
|
||||
}
|
||||
*/
|
||||
let key;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
key = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
}).populate('sender', '+publicKey');
|
||||
key = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
}).populate('sender', '+publicKey');
|
||||
|
||||
if (!key) throw new Error('Failed to find workspace key');
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get workspace key'
|
||||
});
|
||||
}
|
||||
if (!key) throw new Error('Failed to find workspace key');
|
||||
|
||||
return res.status(200).json(key);
|
||||
}
|
||||
@ -230,23 +205,13 @@ export const getWorkspaceServiceTokenData = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
let serviceTokenData;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
serviceTokenData = await ServiceTokenData
|
||||
.find({
|
||||
workspace: workspaceId
|
||||
})
|
||||
.select('+encryptedKey +iv +tag');
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get workspace service token data'
|
||||
});
|
||||
}
|
||||
const serviceTokenData = await ServiceTokenData
|
||||
.find({
|
||||
workspace: workspaceId
|
||||
})
|
||||
.select('+encryptedKey +iv +tag');
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
@ -293,20 +258,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
*/
|
||||
let memberships;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
memberships = await Membership.find({
|
||||
workspace: workspaceId
|
||||
}).populate('user', '+publicKey');
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get workspace memberships'
|
||||
});
|
||||
}
|
||||
const memberships = await Membership.find({
|
||||
workspace: workspaceId
|
||||
}).populate('user', '+publicKey');
|
||||
|
||||
return res.status(200).send({
|
||||
memberships
|
||||
@ -373,29 +329,20 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
|
||||
}
|
||||
}
|
||||
*/
|
||||
let membership;
|
||||
try {
|
||||
const {
|
||||
membershipId
|
||||
} = req.params;
|
||||
const { role } = req.body;
|
||||
|
||||
membership = await Membership.findByIdAndUpdate(
|
||||
membershipId,
|
||||
{
|
||||
role
|
||||
}, {
|
||||
new: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to update workspace membership'
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
membershipId
|
||||
} = req.params;
|
||||
const { role } = req.body;
|
||||
|
||||
const membership = await Membership.findByIdAndUpdate(
|
||||
membershipId,
|
||||
{
|
||||
role
|
||||
}, {
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
@ -444,27 +391,18 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
|
||||
}
|
||||
}
|
||||
*/
|
||||
let membership;
|
||||
try {
|
||||
const {
|
||||
membershipId
|
||||
} = req.params;
|
||||
|
||||
membership = await Membership.findByIdAndDelete(membershipId);
|
||||
|
||||
if (!membership) throw new Error('Failed to delete workspace membership');
|
||||
|
||||
await Key.deleteMany({
|
||||
receiver: membership.user,
|
||||
workspace: membership.workspace
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete workspace membership'
|
||||
});
|
||||
}
|
||||
const {
|
||||
membershipId
|
||||
} = req.params;
|
||||
|
||||
const membership = await Membership.findByIdAndDelete(membershipId);
|
||||
|
||||
if (!membership) throw new Error('Failed to delete workspace membership');
|
||||
|
||||
await Key.deleteMany({
|
||||
receiver: membership.user,
|
||||
workspace: membership.workspace
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
membership
|
||||
@ -478,33 +416,23 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
|
||||
* @returns
|
||||
*/
|
||||
export const toggleAutoCapitalization = async (req: Request, res: Response) => {
|
||||
let workspace;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { autoCapitalization } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
const { autoCapitalization } = req.body;
|
||||
|
||||
workspace = await Workspace.findOneAndUpdate(
|
||||
{
|
||||
_id: workspaceId
|
||||
},
|
||||
{
|
||||
autoCapitalization
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to change autoCapitalization setting'
|
||||
});
|
||||
}
|
||||
const workspace = await Workspace.findOneAndUpdate(
|
||||
{
|
||||
_id: workspaceId
|
||||
},
|
||||
{
|
||||
autoCapitalization
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully changed autoCapitalization setting',
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
|
264
backend/src/controllers/v3/authController.ts
Normal file
264
backend/src/controllers/v3/authController.ts
Normal file
@ -0,0 +1,264 @@
|
||||
/* 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 +devices');
|
||||
|
||||
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.realIP,
|
||||
userAgent: req.headers['user-agent'] ?? ''
|
||||
});
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers['user-agent'] ?? ''
|
||||
});
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
|
||||
// case: user does not have MFA enablgged
|
||||
// return (access) token in response
|
||||
|
||||
interface ResponseData {
|
||||
mfaEnabled: boolean;
|
||||
encryptionVersion: any;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
protectedKeyTag?: string;
|
||||
token: string;
|
||||
publicKey?: string;
|
||||
encryptedPrivateKey?: string;
|
||||
iv?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
const response: ResponseData = {
|
||||
mfaEnabled: false,
|
||||
encryptionVersion: user.encryptionVersion,
|
||||
token: tokens.token,
|
||||
publicKey: user.publicKey,
|
||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
tag: user.tag
|
||||
}
|
||||
|
||||
if (
|
||||
user?.protectedKey &&
|
||||
user?.protectedKeyIV &&
|
||||
user?.protectedKeyTag
|
||||
) {
|
||||
response.protectedKey = user.protectedKey;
|
||||
response.protectedKeyIV = user.protectedKeyIV
|
||||
response.protectedKeyTag = user.protectedKeyTag;
|
||||
}
|
||||
|
||||
const loginAction = await EELogService.createAction({
|
||||
name: ACTION_LOGIN,
|
||||
userId: user._id
|
||||
});
|
||||
|
||||
loginAction && await EELogService.createLog({
|
||||
userId: user._id,
|
||||
actions: [loginAction],
|
||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
return res.status(200).send(response);
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: 'Failed to authenticate. Try again?'
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to authenticate. Try again?'
|
||||
});
|
||||
}
|
||||
};
|
11
backend/src/controllers/v3/index.ts
Normal file
11
backend/src/controllers/v3/index.ts
Normal file
@ -0,0 +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,
|
||||
signupController,
|
||||
workspacesController,
|
||||
}
|
420
backend/src/controllers/v3/secretsController.ts
Normal file
420
backend/src/controllers/v3/secretsController.ts
Normal file
@ -0,0 +1,420 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { SecretService, EventService } from "../../services";
|
||||
import { eventPushSecrets } from "../../events";
|
||||
import { BotService } from "../../services";
|
||||
import { repackageSecretToRaw } from "../../helpers/secrets";
|
||||
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
|
||||
|
||||
/**
|
||||
* Return secrets for workspace with id [workspaceId] and environment
|
||||
* [environment] in plaintext
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getSecretsRaw = async (req: Request, res: Response) => {
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
const secretPath = req.query.secretPath as string;
|
||||
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
authData: req.authData,
|
||||
});
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: secrets.map((secret) => {
|
||||
const rep = repackageSecretToRaw({
|
||||
secret,
|
||||
key
|
||||
});
|
||||
|
||||
return rep;
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return secret with name [secretName] in plaintext
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
const secretPath = req.query.secretPath as string;
|
||||
const type = req.query.type as "shared" | "personal" | undefined;
|
||||
|
||||
const secret = await SecretService.getSecret({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
secretPath,
|
||||
authData: req.authData,
|
||||
});
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret: repackageSecretToRaw({
|
||||
secret,
|
||||
key
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create secret with name [secretName] in plaintext
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const createSecretRaw = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretValue,
|
||||
secretComment,
|
||||
secretPath = "/"
|
||||
} = req.body;
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: secretName,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: secretValue,
|
||||
key
|
||||
});
|
||||
|
||||
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: secretComment,
|
||||
key
|
||||
});
|
||||
|
||||
const secret = await SecretService.createSecret({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
||||
secretKeyIV: secretKeyEncrypted.iv,
|
||||
secretKeyTag: secretKeyEncrypted.tag,
|
||||
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||
secretValueIV: secretValueEncrypted.iv,
|
||||
secretValueTag: secretValueEncrypted.tag,
|
||||
secretPath,
|
||||
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||
secretCommentIV: secretCommentEncrypted.iv,
|
||||
secretCommentTag: secretCommentEncrypted.tag
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
});
|
||||
|
||||
const secretWithoutBlindIndex = secret.toObject();
|
||||
delete secretWithoutBlindIndex.secretBlindIndex;
|
||||
|
||||
return res.status(200).send({
|
||||
secret: repackageSecretToRaw({
|
||||
secret: secretWithoutBlindIndex,
|
||||
key
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update secret with name [secretName]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const updateSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretValue,
|
||||
secretPath = "/",
|
||||
} = req.body;
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: secretValue,
|
||||
key
|
||||
});
|
||||
|
||||
const secret = await SecretService.updateSecret({
|
||||
secretName,
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||
secretValueIV: secretValueEncrypted.iv,
|
||||
secretValueTag: secretValueEncrypted.tag,
|
||||
secretPath,
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret: repackageSecretToRaw({
|
||||
secret,
|
||||
key
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete secret with name [secretName]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretPath = "/"
|
||||
} = req.body;
|
||||
|
||||
const { secret } = await SecretService.deleteSecret({
|
||||
secretName,
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretPath,
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
});
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret: repackageSecretToRaw({
|
||||
secret,
|
||||
key
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get secrets for workspace with id [workspaceId] and environment
|
||||
* [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getSecrets = async (req: Request, res: Response) => {
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
const secretPath = req.query.secretPath as string;
|
||||
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
authData: req.authData,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return secret with name [secretName]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getSecretByName = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
const secretPath = req.query.secretPath as string;
|
||||
const type = req.query.type as "shared" | "personal" | undefined;
|
||||
|
||||
const secret = await SecretService.getSecret({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
secretPath,
|
||||
authData: req.authData,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create secret with name [secretName]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const createSecret = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretPath = "/",
|
||||
} = req.body;
|
||||
|
||||
const secret = await SecretService.createSecret({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
});
|
||||
|
||||
const secretWithoutBlindIndex = secret.toObject();
|
||||
delete secretWithoutBlindIndex.secretBlindIndex;
|
||||
|
||||
return res.status(200).send({
|
||||
secret: secretWithoutBlindIndex,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Update secret with name [secretName]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const updateSecretByName = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath = "/",
|
||||
} = req.body;
|
||||
|
||||
const secret = await SecretService.updateSecret({
|
||||
secretName,
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath,
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete secret with name [secretName]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecretByName = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretPath = "/"
|
||||
} = req.body;
|
||||
|
||||
const { secret } = await SecretService.deleteSecret({
|
||||
secretName,
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretPath,
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret,
|
||||
});
|
||||
};
|
194
backend/src/controllers/v3/signupController.ts
Normal file
194
backend/src/controllers/v3/signupController.ts
Normal file
@ -0,0 +1,194 @@
|
||||
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,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers['user-agent'] ?? ''
|
||||
});
|
||||
|
||||
token = tokens.token;
|
||||
|
||||
// sending a welcome email to new users
|
||||
if (await getLoopsApiKey()) {
|
||||
await standardRequest.post("https://app.loops.so/api/v1/events/send", {
|
||||
"email": email,
|
||||
"eventName": "Sign Up",
|
||||
"firstName": firstName,
|
||||
"lastName": lastName
|
||||
}, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Authorization": "Bearer " + (await getLoopsApiKey())
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'User Signed Up',
|
||||
distinctId: email,
|
||||
properties: {
|
||||
email,
|
||||
attributionSource
|
||||
}
|
||||
});
|
||||
}
|
||||
} 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
|
||||
});
|
||||
};
|
90
backend/src/controllers/v3/workspacesController.ts
Normal file
90
backend/src/controllers/v3/workspacesController.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import { Secret } from '../../models';
|
||||
import { SecretService } from'../../services';
|
||||
|
||||
/**
|
||||
* Return whether or not all secrets in workspace with id [workspaceId]
|
||||
* are blind-indexed
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
const secretsWithoutBlindIndex = await Secret.countDocuments({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
secretBlindIndex: {
|
||||
$exists: false
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send(secretsWithoutBlindIndex === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all secrets for workspace with id [workspaceId]
|
||||
*/
|
||||
export const getWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
const secrets = await Secret.find({
|
||||
workspace: new Types.ObjectId (workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update blind indices for secrets in workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
interface SecretToUpdate {
|
||||
secretName: string;
|
||||
_id: string;
|
||||
}
|
||||
|
||||
const { workspaceId } = req.params;
|
||||
const {
|
||||
secretsToUpdate
|
||||
}: {
|
||||
secretsToUpdate: SecretToUpdate[];
|
||||
} = req.body;
|
||||
|
||||
// get secret blind index salt
|
||||
const salt = await SecretService.getSecretBlindIndexSalt({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
// update secret blind indices
|
||||
const operations = await Promise.all(
|
||||
secretsToUpdate.map(async (secretToUpdate: SecretToUpdate) => {
|
||||
const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
|
||||
secretName: secretToUpdate.secretName,
|
||||
salt
|
||||
});
|
||||
|
||||
return ({
|
||||
updateOne: {
|
||||
filter: {
|
||||
_id: new Types.ObjectId(secretToUpdate._id)
|
||||
},
|
||||
update: {
|
||||
secretBlindIndex
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
await Secret.bulkWrite(operations);
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully named workspace secrets'
|
||||
});
|
||||
}
|
1497
backend/src/data/common_passwords.txt
Normal file
1497
backend/src/data/common_passwords.txt
Normal file
File diff suppressed because it is too large
Load Diff
3519
backend/src/data/disposable_emails.txt
Normal file
3519
backend/src/data/disposable_emails.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,4 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Action, SecretVersion } from '../../models';
|
||||
import { ActionNotFoundError } from '../../../utils/errors';
|
||||
|
||||
@ -28,4 +27,4 @@ export const getAction = async (req: Request, res: Response) => {
|
||||
return res.status(200).send({
|
||||
action
|
||||
});
|
||||
}
|
||||
}
|
||||
|
28
backend/src/ee/controllers/v1/cloudProductsController.ts
Normal file
28
backend/src/ee/controllers/v1/cloudProductsController.ts
Normal file
@ -0,0 +1,28 @@
|
||||
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) => {
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
@ -2,7 +2,8 @@ import { Request, Response } from "express";
|
||||
import { Membership, Workspace } from "../../../models";
|
||||
import { IMembershipPermission } from "../../../models/membership";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
|
||||
import { ABILITY_READ, ABILITY_WRITE, ADMIN, MEMBER } from "../../../variables/organization";
|
||||
import { ADMIN, MEMBER } from "../../../variables/organization";
|
||||
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../../variables';
|
||||
import { Builder } from "builder-pattern"
|
||||
import _ from "lodash";
|
||||
|
||||
@ -10,7 +11,7 @@ export const denyMembershipPermissions = async (req: Request, res: Response) =>
|
||||
const { membershipId } = req.params;
|
||||
const { permissions } = req.body;
|
||||
const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => {
|
||||
if (!permission.ability || !permission.environmentSlug || ![ABILITY_READ, ABILITY_WRITE].includes(permission.ability)) {
|
||||
if (!permission.ability || !permission.environmentSlug || ![PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS].includes(permission.ability)) {
|
||||
throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" })
|
||||
}
|
||||
|
||||
|
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,15 @@
|
||||
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 { 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 +54,34 @@ import { EESecretService } from '../../services';
|
||||
}
|
||||
}
|
||||
*/
|
||||
let secretVersions;
|
||||
try {
|
||||
const { secretId } = req.params;
|
||||
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
|
||||
});
|
||||
}
|
||||
const secretVersions = await SecretVersion.find({
|
||||
secret: secretId,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folder: folderId,
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(offset)
|
||||
.limit(limit);
|
||||
|
||||
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,88 +129,92 @@ 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
|
||||
});
|
||||
|
||||
if (!oldSecretVersion) throw new Error('Failed to find secret version');
|
||||
|
||||
const {
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
} = oldSecretVersion;
|
||||
|
||||
// update secret
|
||||
secret = await Secret.findByIdAndUpdate(
|
||||
secretId,
|
||||
{
|
||||
$inc: {
|
||||
version: 1
|
||||
},
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (!secret) throw new Error('Failed to find and update secret');
|
||||
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,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag
|
||||
}).save();
|
||||
|
||||
// take secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: secret.workspace.toString()
|
||||
});
|
||||
|
||||
} 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,
|
||||
algorithm,
|
||||
folder,
|
||||
keyEncoding,
|
||||
} = oldSecretVersion;
|
||||
|
||||
// update secret
|
||||
const secret = await Secret.findByIdAndUpdate(
|
||||
secretId,
|
||||
{
|
||||
$inc: {
|
||||
version: 1,
|
||||
},
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
...(secretBlindIndex ? { secretBlindIndex } : {}),
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
folderId: folder,
|
||||
algorithm,
|
||||
keyEncoding,
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (!secret) throw new Error("Failed to find and update secret");
|
||||
|
||||
// add new secret version
|
||||
await new SecretVersion({
|
||||
secret: secretId,
|
||||
version: secret.version,
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
isDeleted: false,
|
||||
...(secretBlindIndex ? { secretBlindIndex } : {}),
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
folder,
|
||||
algorithm,
|
||||
keyEncoding,
|
||||
}).save();
|
||||
|
||||
// take secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: secret.workspace,
|
||||
environment,
|
||||
folderId: folder,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret,
|
||||
});
|
||||
};
|
||||
|
@ -1,39 +1,45 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { SecretSnapshot } from '../../models';
|
||||
import { Request, Response } from "express";
|
||||
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;
|
||||
const { secretSnapshotId } = req.params;
|
||||
|
||||
secretSnapshot = await SecretSnapshot
|
||||
.findById(secretSnapshotId)
|
||||
.populate({
|
||||
path: 'secretVersions',
|
||||
populate: {
|
||||
path: 'tags',
|
||||
model: 'Tag',
|
||||
}
|
||||
});
|
||||
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secretSnapshot
|
||||
});
|
||||
}
|
||||
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
|
||||
.lean()
|
||||
.populate<{ secretVersions: ISecretVersion[] }>({
|
||||
path: 'secretVersions',
|
||||
populate: {
|
||||
path: 'tags',
|
||||
model: 'Tag'
|
||||
}
|
||||
})
|
||||
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
|
||||
|
||||
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
|
||||
|
||||
const folderId = secretSnapshot.folderId;
|
||||
// to show only the folder required secrets
|
||||
secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter(
|
||||
({ folder }) => folder === folderId
|
||||
);
|
||||
|
||||
secretSnapshot.folderVersion =
|
||||
secretSnapshot?.folderVersion?.nodes?.children?.map(({ id, name }) => ({
|
||||
id,
|
||||
name,
|
||||
})) as any;
|
||||
|
||||
return res.status(200).send({
|
||||
secretSnapshot,
|
||||
});
|
||||
};
|
||||
|
@ -1,4 +1,3 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response } from 'express';
|
||||
import Stripe from 'stripe';
|
||||
import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
|
||||
@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
|
||||
* @returns
|
||||
*/
|
||||
export const handleWebhook = async (req: Request, res: Response) => {
|
||||
let event;
|
||||
try {
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
|
||||
// check request for valid stripe signature
|
||||
const sig = req.headers['stripe-signature'] as string;
|
||||
event = stripe.webhooks.constructEvent(
|
||||
req.body,
|
||||
sig,
|
||||
getStripeWebhookSecret()
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
error: 'Failed to process webhook'
|
||||
});
|
||||
}
|
||||
// check request for valid stripe signature
|
||||
const sig = req.headers['stripe-signature'] as string;
|
||||
const event = stripe.webhooks.constructEvent(
|
||||
req.body,
|
||||
sig,
|
||||
await getStripeWebhookSecret()
|
||||
);
|
||||
|
||||
switch (event.type) {
|
||||
case '':
|
||||
|
@ -1,25 +1,29 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { Request, Response } from "express";
|
||||
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 +68,60 @@ import { getLatestSecretVersionIds } from '../../helpers/secretVersion';
|
||||
}
|
||||
}
|
||||
*/
|
||||
let secretSnapshots;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
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
|
||||
});
|
||||
}
|
||||
const secretSnapshots = await SecretSnapshot.find({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId: folderId || "root",
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(offset)
|
||||
.limit(limit);
|
||||
|
||||
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
|
||||
) => {
|
||||
const { workspaceId } = req.params;
|
||||
const { environment, folderId } = req.query;
|
||||
|
||||
const count = await SecretSnapshot.countDocuments({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId: folderId || "root",
|
||||
});
|
||||
|
||||
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,160 +171,329 @@ 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[]}>('secretVersions');
|
||||
|
||||
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
|
||||
});
|
||||
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,
|
||||
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,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext: '',
|
||||
secretCommentIV: '',
|
||||
secretCommentTag: '',
|
||||
createdAt
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// add secret versions
|
||||
await SecretVersion.insertMany(
|
||||
secrets.map(({
|
||||
_id,
|
||||
version,
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}) => ({
|
||||
_id: new Types.ObjectId(),
|
||||
secret: _id,
|
||||
version,
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
isDeleted: false,
|
||||
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
|
||||
});
|
||||
} 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
|
||||
const 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,
|
||||
});
|
||||
|
||||
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'
|
||||
|
||||
@ -392,43 +559,32 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
*/
|
||||
let logs
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
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');
|
||||
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
|
||||
});
|
||||
}
|
||||
const 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");
|
||||
|
||||
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 {
|
||||
@ -24,39 +23,37 @@ import {
|
||||
const createActionUpdateSecret = async ({
|
||||
name,
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
}: {
|
||||
name: string;
|
||||
userId: Types.ObjectId;
|
||||
userId?: Types.ObjectId;
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
workspaceId: Types.ObjectId;
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
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,
|
||||
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;
|
||||
}
|
||||
@ -72,68 +69,66 @@ const createActionUpdateSecret = async ({
|
||||
const createActionSecret = async ({
|
||||
name,
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
}: {
|
||||
name: string;
|
||||
userId: Types.ObjectId;
|
||||
userId?: Types.ObjectId;
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
workspaceId: Types.ObjectId;
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
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,
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an (audit) action for user with id [userId]
|
||||
* Create an (audit) action for client with id [userId],
|
||||
* [serviceAccountId], or [serviceTokenDataId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.name - name of action
|
||||
* @param {String} obj.userId - id of user associated with action
|
||||
* @returns
|
||||
*/
|
||||
const createActionUser = ({
|
||||
const createActionClient = ({
|
||||
name,
|
||||
userId
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId
|
||||
}: {
|
||||
name: string;
|
||||
userId: Types.ObjectId;
|
||||
userId?: Types.ObjectId;
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
}) => {
|
||||
let action;
|
||||
try {
|
||||
action = new Action({
|
||||
name,
|
||||
user: userId
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create user action');
|
||||
}
|
||||
const action = new Action({
|
||||
name,
|
||||
user: userId,
|
||||
serviceAccount: serviceAccountId,
|
||||
serviceTokenData: serviceTokenDataId
|
||||
}).save();
|
||||
|
||||
return action;
|
||||
}
|
||||
@ -149,49 +144,47 @@ const createActionUser = ({
|
||||
const createActionHelper = async ({
|
||||
name,
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
secretIds,
|
||||
}: {
|
||||
name: string;
|
||||
userId: Types.ObjectId;
|
||||
userId?: Types.ObjectId;
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
workspaceId?: Types.ObjectId;
|
||||
secretIds?: Types.ObjectId[];
|
||||
}) => {
|
||||
let action;
|
||||
try {
|
||||
switch (name) {
|
||||
case ACTION_LOGIN:
|
||||
case ACTION_LOGOUT:
|
||||
action = await createActionUser({
|
||||
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;
|
||||
@ -199,4 +192,4 @@ const createActionHelper = async ({
|
||||
|
||||
export {
|
||||
createActionHelper
|
||||
};
|
||||
};
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Types } from 'mongoose';
|
||||
import _ from "lodash";
|
||||
import { Membership } from "../../models";
|
||||
import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization";
|
||||
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
|
||||
|
||||
export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => {
|
||||
export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => {
|
||||
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||
if (!membershipForWorkspace) {
|
||||
return false
|
||||
@ -18,15 +19,15 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi
|
||||
return true
|
||||
}
|
||||
|
||||
export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, environment: any) => {
|
||||
export const userHasWriteOnlyAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
|
||||
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||
if (!membershipForWorkspace) {
|
||||
return false
|
||||
}
|
||||
|
||||
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
||||
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE });
|
||||
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ });
|
||||
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
|
||||
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
|
||||
|
||||
// case: you have write only if read is blocked and write is not
|
||||
if (isReadDisallowed && !isWriteDisallowed) {
|
||||
@ -36,15 +37,15 @@ export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, env
|
||||
return false
|
||||
}
|
||||
|
||||
export const userHasNoAbility = async (userId: any, workspaceId: any, environment: any) => {
|
||||
export const userHasNoAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
|
||||
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||
if (!membershipForWorkspace) {
|
||||
return true
|
||||
}
|
||||
|
||||
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
||||
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE });
|
||||
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ });
|
||||
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
|
||||
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
|
||||
|
||||
if (isReadBlocked && isWriteDisallowed) {
|
||||
return true
|
||||
|
@ -1,9 +1,9 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Log,
|
||||
IAction
|
||||
} from '../models';
|
||||
|
||||
/**
|
||||
* Create an (audit) log
|
||||
* @param {Object} obj
|
||||
@ -16,36 +16,35 @@ import {
|
||||
*/
|
||||
const createLogHelper = async ({
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
}: {
|
||||
userId: Types.ObjectId;
|
||||
userId?: Types.ObjectId;
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
workspaceId?: Types.ObjectId;
|
||||
actions: IAction[];
|
||||
channel: string;
|
||||
ipAddress: string;
|
||||
}) => {
|
||||
let log;
|
||||
try {
|
||||
log = await new Log({
|
||||
user: userId,
|
||||
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: string;
|
||||
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) => ({
|
||||
...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 };
|
||||
|
@ -15,32 +15,28 @@ import {
|
||||
const requireSecretSnapshotAuth = ({
|
||||
acceptedRoles,
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
acceptedRoles: Array<'admin' | 'member'>;
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { secretSnapshotId } = req.params;
|
||||
|
||||
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
|
||||
|
||||
if (!secretSnapshot) {
|
||||
return next(SecretSnapshotNotFoundError({
|
||||
message: 'Failed to find secret snapshot'
|
||||
}));
|
||||
}
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: secretSnapshot.workspace.toString(),
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
req.secretSnapshot = secretSnapshot as any;
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret snapshot' }));
|
||||
const { secretSnapshotId } = req.params;
|
||||
|
||||
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
|
||||
|
||||
if (!secretSnapshot) {
|
||||
return next(SecretSnapshotNotFoundError({
|
||||
message: 'Failed to find secret snapshot'
|
||||
}));
|
||||
}
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id,
|
||||
workspaceId: secretSnapshot.workspace,
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
req.secretSnapshot = secretSnapshot as any;
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,8 @@ import {
|
||||
export interface IAction {
|
||||
name: string;
|
||||
user?: Types.ObjectId,
|
||||
serviceAccount?: Types.ObjectId,
|
||||
serviceTokenData?: Types.ObjectId,
|
||||
workspace?: Types.ObjectId,
|
||||
payload?: {
|
||||
secretVersions?: Types.ObjectId[]
|
||||
@ -33,8 +35,15 @@ const actionSchema = new Schema<IAction>(
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
ref: 'User'
|
||||
},
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceAccount'
|
||||
},
|
||||
serviceTokenData: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceTokenData'
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
|
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,
|
||||
};
|
||||
|
@ -11,6 +11,8 @@ import {
|
||||
export interface ILog {
|
||||
_id: Types.ObjectId;
|
||||
user?: Types.ObjectId;
|
||||
serviceAccount?: Types.ObjectId;
|
||||
serviceTokenData?: Types.ObjectId;
|
||||
workspace?: Types.ObjectId;
|
||||
actionNames: string[];
|
||||
actions: Types.ObjectId[];
|
||||
@ -24,6 +26,14 @@ const logSchema = new Schema<ILog>(
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceAccount'
|
||||
},
|
||||
serviceTokenData: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceTokenData'
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace'
|
||||
|
@ -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 {
|
||||
secret: Types.ObjectId;
|
||||
version: number;
|
||||
workspace: Types.ObjectId; // new
|
||||
type: string; // new
|
||||
user?: Types.ObjectId; // new
|
||||
environment: string; // new
|
||||
isDeleted: boolean;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
tags?: 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
|
||||
},
|
||||
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
|
||||
},
|
||||
tags: {
|
||||
ref: 'Tag',
|
||||
type: [Schema.Types.ObjectId],
|
||||
default: []
|
||||
},
|
||||
},
|
||||
{
|
||||
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,12 @@ import {
|
||||
} from '../../../middleware';
|
||||
import { query, param, body } from 'express-validator';
|
||||
import { secretController } from '../../controllers/v1';
|
||||
import { ADMIN, MEMBER } from '../../../variables';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
PERMISSION_READ_SECRETS,
|
||||
PERMISSION_WRITE_SECRETS
|
||||
} from '../../../variables';
|
||||
|
||||
router.get(
|
||||
'/:secretId/secret-versions',
|
||||
@ -15,7 +20,8 @@ router.get(
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireSecretAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS]
|
||||
}),
|
||||
param('secretId').exists().trim(),
|
||||
query('offset').exists().isInt(),
|
||||
@ -30,7 +36,8 @@ router.post(
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireSecretAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS]
|
||||
}),
|
||||
param('secretId').exists().trim(),
|
||||
body('version').exists().isInt(),
|
||||
|
@ -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,72 +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]
|
||||
}),
|
||||
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]
|
||||
}),
|
||||
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]
|
||||
}),
|
||||
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]
|
||||
}),
|
||||
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,142 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import NodeCache from 'node-cache';
|
||||
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;
|
||||
envLimit?: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
envLimit: null
|
||||
}
|
||||
|
||||
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 refreshOrganizationPlan(organizationId: string) {
|
||||
if (this.instanceType === 'cloud') {
|
||||
this.localFeatureSet.del(organizationId);
|
||||
await this.getOrganizationPlan(organizationId);
|
||||
}
|
||||
}
|
||||
|
||||
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 +144,4 @@ class EELicenseService {
|
||||
}
|
||||
}
|
||||
|
||||
export default new EELicenseService('N/A');
|
||||
export default new EELicenseService();
|
||||
|
@ -26,12 +26,16 @@ class EELogService {
|
||||
*/
|
||||
static async createLog({
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
}: {
|
||||
userId: Types.ObjectId;
|
||||
userId?: Types.ObjectId;
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
workspaceId?: Types.ObjectId;
|
||||
actions: IAction[];
|
||||
channel: string;
|
||||
@ -40,6 +44,8 @@ class EELogService {
|
||||
if (!EELicenseService.isLicenseValid) return null;
|
||||
return await createLogHelper({
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
actions,
|
||||
channel,
|
||||
@ -59,17 +65,23 @@ class EELogService {
|
||||
static async createAction({
|
||||
name,
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
}: {
|
||||
name: string;
|
||||
userId: Types.ObjectId;
|
||||
userId?: Types.ObjectId;
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
workspaceId?: Types.ObjectId;
|
||||
secretIds?: Types.ObjectId[];
|
||||
}) {
|
||||
return await createActionHelper({
|
||||
name,
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
});
|
||||
|
@ -1,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: string;
|
||||
}) {
|
||||
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,3 +1,4 @@
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
EVENT_PUSH_SECRETS,
|
||||
EVENT_PULL_SECRETS
|
||||
@ -22,13 +23,16 @@ interface PushSecret {
|
||||
* @returns
|
||||
*/
|
||||
const eventPushSecrets = ({
|
||||
workspaceId
|
||||
workspaceId,
|
||||
environment
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
}) => {
|
||||
return ({
|
||||
name: EVENT_PUSH_SECRETS,
|
||||
workspaceId,
|
||||
environment,
|
||||
payload: {
|
||||
|
||||
}
|
||||
|
@ -1,15 +1,19 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcrypt';
|
||||
import {
|
||||
IUser,
|
||||
User,
|
||||
ServiceTokenData,
|
||||
APIKeyData
|
||||
ServiceAccount,
|
||||
APIKeyData,
|
||||
TokenVersion,
|
||||
ITokenVersion
|
||||
} from '../models';
|
||||
import {
|
||||
AccountNotFoundError,
|
||||
ServiceTokenDataNotFoundError,
|
||||
ServiceAccountNotFoundError,
|
||||
APIKeyDataNotFoundError,
|
||||
UnauthorizedRequestError,
|
||||
BadRequestError
|
||||
@ -17,27 +21,33 @@ import {
|
||||
import {
|
||||
getJwtAuthLifetime,
|
||||
getJwtAuthSecret,
|
||||
getJwtProviderAuthSecret,
|
||||
getJwtRefreshLifetime,
|
||||
getJwtRefreshSecret
|
||||
} from '../config';
|
||||
import {
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../variables';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.headers - HTTP request headers object
|
||||
*/
|
||||
const validateAuthMode = ({
|
||||
export const validateAuthMode = ({
|
||||
headers,
|
||||
acceptedAuthModes
|
||||
}: {
|
||||
headers: { [key: string]: string | string[] | undefined },
|
||||
acceptedAuthModes: string[]
|
||||
}) => {
|
||||
// TODO: refactor middleware
|
||||
const apiKey = headers['x-api-key'];
|
||||
const authHeader = headers['authorization'];
|
||||
|
||||
let authTokenType, authTokenValue;
|
||||
let authMode, authTokenValue;
|
||||
if (apiKey === undefined && authHeader === undefined) {
|
||||
// case: no auth or X-API-KEY header present
|
||||
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
|
||||
@ -45,7 +55,7 @@ const validateAuthMode = ({
|
||||
|
||||
if (typeof apiKey === 'string') {
|
||||
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
|
||||
authTokenType = 'apiKey';
|
||||
authMode = AUTH_MODE_API_KEY;
|
||||
authTokenValue = apiKey;
|
||||
}
|
||||
|
||||
@ -61,20 +71,24 @@ const validateAuthMode = ({
|
||||
|
||||
switch (tokenValue.split('.', 1)[0]) {
|
||||
case 'st':
|
||||
authTokenType = 'serviceToken';
|
||||
authMode = AUTH_MODE_SERVICE_TOKEN;
|
||||
break;
|
||||
case 'sa':
|
||||
authMode = AUTH_MODE_SERVICE_ACCOUNT;
|
||||
break;
|
||||
default:
|
||||
authTokenType = 'jwt';
|
||||
authMode = AUTH_MODE_JWT;
|
||||
}
|
||||
|
||||
authTokenValue = tokenValue;
|
||||
}
|
||||
|
||||
if (!authTokenType || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
|
||||
if (!authMode || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
|
||||
|
||||
if (!acceptedAuthModes.includes(authTokenType)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
|
||||
if (!acceptedAuthModes.includes(authMode)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
|
||||
|
||||
return ({
|
||||
authTokenType,
|
||||
authMode,
|
||||
authTokenValue
|
||||
});
|
||||
}
|
||||
@ -85,32 +99,42 @@ const validateAuthMode = ({
|
||||
* @param {String} obj.authTokenValue - JWT token value
|
||||
* @returns {User} user - user corresponding to JWT token
|
||||
*/
|
||||
const getAuthUserPayload = async ({
|
||||
export const getAuthUserPayload = async ({
|
||||
authTokenValue
|
||||
}: {
|
||||
authTokenValue: string;
|
||||
}) => {
|
||||
let user;
|
||||
try {
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(authTokenValue, getJwtAuthSecret())
|
||||
);
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(authTokenValue, await getJwtAuthSecret())
|
||||
);
|
||||
|
||||
user = await User.findOne({
|
||||
_id: decodedToken.userId
|
||||
}).select('+publicKey');
|
||||
const user = await User.findOne({
|
||||
_id: new Types.ObjectId(decodedToken.userId)
|
||||
}).select('+publicKey +accessVersion');
|
||||
|
||||
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
|
||||
if (!user) throw AccountNotFoundError({ message: 'Failed to find user' });
|
||||
|
||||
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });
|
||||
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate user with partially set up account' });
|
||||
|
||||
} catch (err) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate JWT token'
|
||||
});
|
||||
}
|
||||
const tokenVersion = await TokenVersion.findOneAndUpdate({
|
||||
_id: new Types.ObjectId(decodedToken.tokenVersionId),
|
||||
user: user._id
|
||||
}, {
|
||||
lastUsed: new Date()
|
||||
});
|
||||
|
||||
if (!tokenVersion) throw UnauthorizedRequestError({
|
||||
message: 'Failed to validate access token'
|
||||
});
|
||||
|
||||
if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError({
|
||||
message: 'Failed to validate access token'
|
||||
});
|
||||
|
||||
return user;
|
||||
return ({
|
||||
user,
|
||||
tokenVersionId: tokenVersion._id
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -119,88 +143,124 @@ const getAuthUserPayload = async ({
|
||||
* @param {String} obj.authTokenValue - service token value
|
||||
* @returns {ServiceTokenData} serviceTokenData - service token data
|
||||
*/
|
||||
const getAuthSTDPayload = async ({
|
||||
export const getAuthSTDPayload = async ({
|
||||
authTokenValue
|
||||
}: {
|
||||
authTokenValue: string;
|
||||
}) => {
|
||||
let serviceTokenData;
|
||||
try {
|
||||
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||
|
||||
// TODO: optimize double query
|
||||
serviceTokenData = await ServiceTokenData
|
||||
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
|
||||
let serviceTokenData = await ServiceTokenData
|
||||
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
|
||||
|
||||
if (!serviceTokenData) {
|
||||
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
||||
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
|
||||
// case: service token expired
|
||||
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate expired service token'
|
||||
});
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
|
||||
if (!isMatch) throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate service token'
|
||||
});
|
||||
|
||||
serviceTokenData = await ServiceTokenData
|
||||
.findById(TOKEN_IDENTIFIER)
|
||||
.select('+encryptedKey +iv +tag')
|
||||
.populate<{user: IUser}>('user');
|
||||
|
||||
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
||||
|
||||
} catch (err) {
|
||||
if (!serviceTokenData) {
|
||||
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
||||
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
|
||||
// case: service token expired
|
||||
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate service token'
|
||||
message: 'Failed to authenticate expired service token'
|
||||
});
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
|
||||
if (!isMatch) throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate service token'
|
||||
});
|
||||
|
||||
serviceTokenData = await ServiceTokenData
|
||||
.findOneAndUpdate({
|
||||
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
|
||||
}, {
|
||||
lastUsed: new Date()
|
||||
}, {
|
||||
new: true
|
||||
})
|
||||
.select('+encryptedKey +iv +tag');
|
||||
|
||||
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
||||
|
||||
return serviceTokenData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return service account access key payload
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.authTokenValue - service account access token value
|
||||
* @returns {ServiceAccount} serviceAccount
|
||||
*/
|
||||
export const getAuthSAAKPayload = async ({
|
||||
authTokenValue
|
||||
}: {
|
||||
authTokenValue: string;
|
||||
}) => {
|
||||
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||
|
||||
const serviceAccount = await ServiceAccount.findById(
|
||||
Buffer.from(TOKEN_IDENTIFIER, 'base64').toString('hex')
|
||||
).select('+secretHash');
|
||||
|
||||
if (!serviceAccount) {
|
||||
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
|
||||
}
|
||||
|
||||
const result = await bcrypt.compare(TOKEN_SECRET, serviceAccount.secretHash);
|
||||
if (!result) throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate service account access key'
|
||||
});
|
||||
|
||||
return serviceAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return API key data payload corresponding to API key [authTokenValue]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.authTokenValue - API key value
|
||||
* @returns {APIKeyData} apiKeyData - API key data
|
||||
*/
|
||||
const getAuthAPIKeyPayload = async ({
|
||||
export const getAuthAPIKeyPayload = async ({
|
||||
authTokenValue
|
||||
}: {
|
||||
authTokenValue: string;
|
||||
}) => {
|
||||
let user;
|
||||
try {
|
||||
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||
|
||||
const apiKeyData = await APIKeyData
|
||||
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
|
||||
.populate('user', '+publicKey');
|
||||
let apiKeyData = await APIKeyData
|
||||
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
|
||||
.populate<{ user: IUser }>('user', '+publicKey');
|
||||
|
||||
if (!apiKeyData) {
|
||||
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
|
||||
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
|
||||
// case: API key expired
|
||||
await APIKeyData.findByIdAndDelete(apiKeyData._id);
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate expired API key'
|
||||
});
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
|
||||
if (!isMatch) throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate API key'
|
||||
});
|
||||
|
||||
user = apiKeyData.user;
|
||||
} catch (err) {
|
||||
if (!apiKeyData) {
|
||||
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
|
||||
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
|
||||
// case: API key expired
|
||||
await APIKeyData.findByIdAndDelete(apiKeyData._id);
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate API key'
|
||||
message: 'Failed to authenticate expired API key'
|
||||
});
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
|
||||
if (!isMatch) throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate API key'
|
||||
});
|
||||
|
||||
apiKeyData = await APIKeyData.findOneAndUpdate({
|
||||
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
|
||||
}, {
|
||||
lastUsed: new Date()
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
|
||||
if (!apiKeyData) {
|
||||
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
|
||||
}
|
||||
|
||||
const user = await User.findById(apiKeyData.user).select('+publicKey');
|
||||
|
||||
if (!user) {
|
||||
throw AccountNotFoundError({
|
||||
message: 'Failed to find user'
|
||||
});
|
||||
}
|
||||
|
||||
@ -215,32 +275,58 @@ const getAuthAPIKeyPayload = async ({
|
||||
* @return {String} obj.token - issued JWT token
|
||||
* @return {String} obj.refreshToken - issued refresh token
|
||||
*/
|
||||
const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
||||
let token: string;
|
||||
let refreshToken: string;
|
||||
try {
|
||||
// issue tokens
|
||||
token = createToken({
|
||||
payload: {
|
||||
userId
|
||||
},
|
||||
expiresIn: getJwtAuthLifetime(),
|
||||
secret: getJwtAuthSecret()
|
||||
});
|
||||
export const issueAuthTokens = async ({
|
||||
userId,
|
||||
ip,
|
||||
userAgent
|
||||
}: {
|
||||
userId: Types.ObjectId;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
}) => {
|
||||
let tokenVersion: ITokenVersion | null;
|
||||
|
||||
refreshToken = createToken({
|
||||
payload: {
|
||||
userId
|
||||
},
|
||||
expiresIn: getJwtRefreshLifetime(),
|
||||
secret: getJwtRefreshSecret()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to issue tokens');
|
||||
// continue with (session) token version matching existing ip and user agent
|
||||
tokenVersion = await TokenVersion.findOne({
|
||||
user: userId,
|
||||
ip,
|
||||
userAgent
|
||||
});
|
||||
|
||||
if (!tokenVersion) {
|
||||
// case: no existing ip and user agent exists
|
||||
// -> create new (session) token version for ip and user agent
|
||||
tokenVersion = await new TokenVersion({
|
||||
user: userId,
|
||||
refreshVersion: 0,
|
||||
accessVersion: 0,
|
||||
ip,
|
||||
userAgent,
|
||||
lastUsed: new Date()
|
||||
}).save();
|
||||
}
|
||||
|
||||
// issue tokens
|
||||
const token = createToken({
|
||||
payload: {
|
||||
userId,
|
||||
tokenVersionId: tokenVersion._id.toString(),
|
||||
accessVersion: tokenVersion.accessVersion
|
||||
},
|
||||
expiresIn: await getJwtAuthLifetime(),
|
||||
secret: await getJwtAuthSecret()
|
||||
});
|
||||
|
||||
const refreshToken = createToken({
|
||||
payload: {
|
||||
userId,
|
||||
tokenVersionId: tokenVersion._id.toString(),
|
||||
refreshVersion: tokenVersion.refreshVersion
|
||||
},
|
||||
expiresIn: await getJwtRefreshLifetime(),
|
||||
secret: await getJwtRefreshSecret()
|
||||
});
|
||||
|
||||
return {
|
||||
token,
|
||||
refreshToken
|
||||
@ -252,20 +338,17 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.userId - id of user whose tokens are cleared.
|
||||
*/
|
||||
const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
|
||||
try {
|
||||
// increment refreshVersion on user by 1
|
||||
User.findOneAndUpdate({
|
||||
_id: userId
|
||||
}, {
|
||||
$inc: {
|
||||
refreshVersion: 1
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
export const clearTokens = async (tokenVersionId: Types.ObjectId): Promise<void> => {
|
||||
// increment refreshVersion on user by 1
|
||||
|
||||
await TokenVersion.findOneAndUpdate({
|
||||
_id: tokenVersionId
|
||||
}, {
|
||||
$inc: {
|
||||
refreshVersion: 1,
|
||||
accessVersion: 1
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -276,7 +359,7 @@ const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
|
||||
* @param {String} obj.secret - (JWT) secret such as [JWT_AUTH_SECRET]
|
||||
* @param {String} obj.expiresIn - string describing time span such as '10h' or '7d'
|
||||
*/
|
||||
const createToken = ({
|
||||
export const createToken = ({
|
||||
payload,
|
||||
expiresIn,
|
||||
secret
|
||||
@ -285,23 +368,32 @@ const createToken = ({
|
||||
expiresIn: string | number;
|
||||
secret: string;
|
||||
}) => {
|
||||
try {
|
||||
return jwt.sign(payload, secret, {
|
||||
expiresIn
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create a token');
|
||||
}
|
||||
return jwt.sign(payload, secret, {
|
||||
expiresIn
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
validateAuthMode,
|
||||
getAuthUserPayload,
|
||||
getAuthSTDPayload,
|
||||
getAuthAPIKeyPayload,
|
||||
createToken,
|
||||
issueAuthTokens,
|
||||
clearTokens
|
||||
};
|
||||
export 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.')
|
||||
}
|
||||
}
|
@ -1,57 +1,101 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
Bot,
|
||||
BotKey,
|
||||
Secret,
|
||||
ISecret,
|
||||
IUser
|
||||
} 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 } from '../variables';
|
||||
import { getEncryptionKey } from '../config';
|
||||
import {
|
||||
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,
|
||||
export const createBot = async ({
|
||||
name,
|
||||
workspaceId,
|
||||
}: {
|
||||
name: string;
|
||||
workspaceId: string;
|
||||
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 whether or not workspace with id [workspaceId] is end-to-end encrypted
|
||||
* @param {Types.ObjectId} workspaceId - id of workspace to check
|
||||
*/
|
||||
export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
|
||||
const botKey = await BotKey.exists({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
return botKey ? false : true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -61,126 +105,126 @@ const createBot = async ({
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.environment - environment
|
||||
*/
|
||||
const getSecretsHelper = async ({
|
||||
workspaceId,
|
||||
environment
|
||||
export const getSecretsBotHelper = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
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 });
|
||||
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: string }) => {
|
||||
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');
|
||||
}
|
||||
export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => {
|
||||
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
|
||||
export const encryptSymmetricHelper = async ({
|
||||
workspaceId,
|
||||
plaintext,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
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 });
|
||||
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext,
|
||||
key,
|
||||
});
|
||||
|
||||
return {
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Return symmetrically decrypted [ciphertext] using the
|
||||
* key for workspace with id [workspaceId]
|
||||
@ -190,40 +234,24 @@ const encryptSymmetricHelper = async ({
|
||||
* @param {String} obj.iv - iv
|
||||
* @param {String} obj.tag - tag
|
||||
*/
|
||||
const decryptSymmetricHelper = async ({
|
||||
workspaceId,
|
||||
export const decryptSymmetricHelper = async ({
|
||||
workspaceId,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}) => {
|
||||
const key = await getKey({ workspaceId: workspaceId });
|
||||
const plaintext = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
}: {
|
||||
workspaceId: string;
|
||||
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,
|
||||
});
|
||||
|
||||
export {
|
||||
createBot,
|
||||
getSecretsHelper,
|
||||
encryptSymmetricHelper,
|
||||
decryptSymmetricHelper
|
||||
}
|
||||
return plaintext;
|
||||
};
|
@ -1,5 +1,4 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { EESecretService } from '../ee/services';
|
||||
import { getLogger } from '../utils/logger';
|
||||
|
||||
/**
|
||||
@ -8,7 +7,7 @@ import { getLogger } from '../utils/logger';
|
||||
* @param {String} obj.mongoURL - mongo connection string
|
||||
* @returns
|
||||
*/
|
||||
const initDatabaseHelper = async ({
|
||||
export const initDatabaseHelper = async ({
|
||||
mongoURL
|
||||
}: {
|
||||
mongoURL: string;
|
||||
@ -19,11 +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 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;
|
||||
@ -32,7 +30,7 @@ const initDatabaseHelper = async ({
|
||||
/**
|
||||
* Close database conection
|
||||
*/
|
||||
const closeDatabaseHelper = async () => {
|
||||
export const closeDatabaseHelper = async () => {
|
||||
return Promise.all([
|
||||
new Promise((resolve) => {
|
||||
if (mongoose.connection && mongoose.connection.readyState == 1) {
|
||||
@ -43,9 +41,4 @@ const closeDatabaseHelper = async () => {
|
||||
}
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
export {
|
||||
initDatabaseHelper,
|
||||
closeDatabaseHelper
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
import { Bot, IBot } from '../models';
|
||||
import * as Sentry from '@sentry/node';
|
||||
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: string;
|
||||
payload: any;
|
||||
name: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -17,35 +18,23 @@ 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 } = 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
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
}
|
||||
export 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;
|
||||
}
|
||||
};
|
17
backend/src/helpers/index.ts
Normal file
17
backend/src/helpers/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export * from './auth';
|
||||
export * from './bot';
|
||||
export * from './database';
|
||||
export * from './event';
|
||||
export * from './integration';
|
||||
export * from './key';
|
||||
export * from './membership';
|
||||
export * from './membershipOrg';
|
||||
export * from './nodemailer';
|
||||
export * from './organization';
|
||||
export * from './rateLimiter';
|
||||
export * from './secret';
|
||||
export * from './secrets';
|
||||
export * from './signup';
|
||||
export * from './token';
|
||||
export * from './user';
|
||||
export * from './workspace';
|
@ -1,17 +1,20 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Bot,
|
||||
Integration,
|
||||
IntegrationAuth,
|
||||
IntegrationAuth
|
||||
} from '../models';
|
||||
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
||||
import { BotService } from '../services';
|
||||
import {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
INTEGRATION_NETLIFY,
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_UTF8
|
||||
} from '../variables';
|
||||
import { UnauthorizedRequestError } from '../utils/errors';
|
||||
import RequestError from '../utils/requestError';
|
||||
import {
|
||||
UnauthorizedRequestError,
|
||||
} from '../utils/errors';
|
||||
|
||||
interface Update {
|
||||
workspace: string;
|
||||
@ -32,7 +35,7 @@ interface Update {
|
||||
* @param {String} obj.code - code
|
||||
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
|
||||
*/
|
||||
const handleOAuthExchangeHelper = async ({
|
||||
export const handleOAuthExchangeHelper = async ({
|
||||
workspaceId,
|
||||
integration,
|
||||
code,
|
||||
@ -43,66 +46,59 @@ const handleOAuthExchangeHelper = async ({
|
||||
code: string;
|
||||
environment: string;
|
||||
}) => {
|
||||
let integrationAuth;
|
||||
try {
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId,
|
||||
isActive: true
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
|
||||
|
||||
// exchange code for access and refresh tokens
|
||||
const res = await exchangeCode({
|
||||
integration,
|
||||
code
|
||||
});
|
||||
|
||||
const update: Update = {
|
||||
workspace: workspaceId,
|
||||
integration
|
||||
}
|
||||
|
||||
switch (integration) {
|
||||
case INTEGRATION_VERCEL:
|
||||
update.teamId = res.teamId;
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
update.accountId = res.accountId;
|
||||
break;
|
||||
}
|
||||
|
||||
const integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
workspace: workspaceId,
|
||||
integration
|
||||
}, update, {
|
||||
new: true,
|
||||
upsert: true
|
||||
});
|
||||
|
||||
if (res.refreshToken) {
|
||||
// case: refresh token returned from exchange
|
||||
// set integration auth refresh token
|
||||
await setIntegrationAuthRefreshHelper({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
refreshToken: res.refreshToken
|
||||
});
|
||||
|
||||
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
|
||||
|
||||
// exchange code for access and refresh tokens
|
||||
const res = await exchangeCode({
|
||||
integration,
|
||||
code
|
||||
}
|
||||
|
||||
if (res.accessToken) {
|
||||
// case: access token returned from exchange
|
||||
// set integration auth access token
|
||||
await setIntegrationAuthAccessHelper({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
accessId: null,
|
||||
accessToken: res.accessToken,
|
||||
accessExpiresAt: res.accessExpiresAt
|
||||
});
|
||||
|
||||
const update: Update = {
|
||||
workspace: workspaceId,
|
||||
integration
|
||||
}
|
||||
|
||||
switch (integration) {
|
||||
case INTEGRATION_VERCEL:
|
||||
update.teamId = res.teamId;
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
update.accountId = res.accountId;
|
||||
break;
|
||||
}
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
workspace: workspaceId,
|
||||
integration
|
||||
}, update, {
|
||||
new: true,
|
||||
upsert: true
|
||||
});
|
||||
|
||||
if (res.refreshToken) {
|
||||
// case: refresh token returned from exchange
|
||||
// set integration auth refresh token
|
||||
await setIntegrationAuthRefreshHelper({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
refreshToken: res.refreshToken
|
||||
});
|
||||
}
|
||||
|
||||
if (res.accessToken) {
|
||||
// case: access token returned from exchange
|
||||
// set integration auth access token
|
||||
await setIntegrationAuthAccessHelper({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
accessId: null,
|
||||
accessToken: res.accessToken,
|
||||
accessExpiresAt: res.accessExpiresAt
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to handle OAuth2 code-token exchange')
|
||||
}
|
||||
|
||||
return integrationAuth;
|
||||
@ -113,49 +109,47 @@ const handleOAuthExchangeHelper = async ({
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.workspaceId - id of workspace
|
||||
*/
|
||||
const syncIntegrationsHelper = async ({
|
||||
workspaceId
|
||||
export const syncIntegrationsHelper = async ({
|
||||
workspaceId,
|
||||
environment
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
}) => {
|
||||
let integrations;
|
||||
try {
|
||||
integrations = await Integration.find({
|
||||
workspace: workspaceId,
|
||||
isActive: true,
|
||||
app: { $ne: null }
|
||||
const integrations = await Integration.find({
|
||||
workspace: workspaceId,
|
||||
...(environment ? {
|
||||
environment
|
||||
} : {}),
|
||||
isActive: true,
|
||||
app: { $ne: null }
|
||||
});
|
||||
|
||||
// for each workspace integration, sync/push secrets
|
||||
// to that integration
|
||||
for await (const integration of integrations) {
|
||||
// get workspace, environment (shared) secrets
|
||||
const secrets = await BotService.getSecrets({ // issue here?
|
||||
workspaceId: integration.workspace,
|
||||
environment: integration.environment
|
||||
});
|
||||
|
||||
// for each workspace integration, sync/push secrets
|
||||
// to that integration
|
||||
for await (const integration of integrations) {
|
||||
// get workspace, environment (shared) secrets
|
||||
const secrets = await BotService.getSecrets({ // issue here?
|
||||
workspaceId: integration.workspace.toString(),
|
||||
environment: integration.environment
|
||||
});
|
||||
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
// get integration auth access token
|
||||
const access = await getIntegrationAuthAccessHelper({
|
||||
integrationAuthId: integration.integrationAuth
|
||||
});
|
||||
|
||||
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
// get integration auth access token
|
||||
const access = await getIntegrationAuthAccessHelper({
|
||||
integrationAuthId: integration.integrationAuth.toString()
|
||||
});
|
||||
|
||||
// sync secrets to integration
|
||||
await syncSecrets({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessId: access.accessId,
|
||||
accessToken: access.accessToken
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to integrations');
|
||||
// sync secrets to integration
|
||||
await syncSecrets({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessId: access.accessId === undefined ? null : access.accessId,
|
||||
accessToken: access.accessToken
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,31 +161,19 @@ const syncIntegrationsHelper = async ({
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} refreshToken - decrypted refresh token
|
||||
*/
|
||||
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
||||
let refreshToken;
|
||||
|
||||
try {
|
||||
const integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId)
|
||||
.select('+refreshCiphertext +refreshIV +refreshTag');
|
||||
export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
|
||||
const integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId)
|
||||
.select('+refreshCiphertext +refreshIV +refreshTag');
|
||||
|
||||
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
||||
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
||||
|
||||
refreshToken = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
ciphertext: integrationAuth.refreshCiphertext as string,
|
||||
iv: integrationAuth.refreshIV as string,
|
||||
tag: integrationAuth.refreshTag as string
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
if(err instanceof RequestError)
|
||||
throw err
|
||||
else
|
||||
throw new Error('Failed to get integration refresh token');
|
||||
}
|
||||
const refreshToken = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
ciphertext: integrationAuth.refreshCiphertext as string,
|
||||
iv: integrationAuth.refreshIV as string,
|
||||
tag: integrationAuth.refreshTag as string
|
||||
});
|
||||
|
||||
return refreshToken;
|
||||
}
|
||||
@ -204,53 +186,43 @@ const syncIntegrationsHelper = async ({
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @returns {String} accessToken - decrypted access token
|
||||
*/
|
||||
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
||||
export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
|
||||
let accessId;
|
||||
let accessToken;
|
||||
try {
|
||||
const integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId)
|
||||
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
|
||||
const integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId)
|
||||
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
|
||||
|
||||
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
||||
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
||||
|
||||
accessToken = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
ciphertext: integrationAuth.accessCiphertext as string,
|
||||
iv: integrationAuth.accessIV as string,
|
||||
tag: integrationAuth.accessTag as string
|
||||
});
|
||||
accessToken = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
ciphertext: integrationAuth.accessCiphertext as string,
|
||||
iv: integrationAuth.accessIV as string,
|
||||
tag: integrationAuth.accessTag as string
|
||||
});
|
||||
|
||||
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
||||
// there is a access token expiration date
|
||||
// and refresh token to exchange with the OAuth2 server
|
||||
|
||||
if (integrationAuth.accessExpiresAt < new Date()) {
|
||||
// access token is expired
|
||||
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
|
||||
accessToken = await exchangeRefresh({
|
||||
integrationAuth,
|
||||
refreshToken
|
||||
});
|
||||
}
|
||||
}
|
||||
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
||||
// there is a access token expiration date
|
||||
// and refresh token to exchange with the OAuth2 server
|
||||
|
||||
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
|
||||
accessId = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
ciphertext: integrationAuth.accessIdCiphertext as string,
|
||||
iv: integrationAuth.accessIdIV as string,
|
||||
tag: integrationAuth.accessIdTag as string
|
||||
if (integrationAuth.accessExpiresAt < new Date()) {
|
||||
// access token is expired
|
||||
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
|
||||
accessToken = await exchangeRefresh({
|
||||
integrationAuth,
|
||||
refreshToken
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
if(err instanceof RequestError)
|
||||
throw err
|
||||
else
|
||||
throw new Error('Failed to get integration access token');
|
||||
}
|
||||
|
||||
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
|
||||
accessId = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
ciphertext: integrationAuth.accessIdCiphertext as string,
|
||||
iv: integrationAuth.accessIdIV as string,
|
||||
tag: integrationAuth.accessIdTag as string
|
||||
});
|
||||
}
|
||||
|
||||
return ({
|
||||
@ -267,7 +239,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} obj.refreshToken - refresh token
|
||||
*/
|
||||
const setIntegrationAuthRefreshHelper = async ({
|
||||
export const setIntegrationAuthRefreshHelper = async ({
|
||||
integrationAuthId,
|
||||
refreshToken
|
||||
}: {
|
||||
@ -275,32 +247,27 @@ const setIntegrationAuthRefreshHelper = async ({
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
|
||||
let integrationAuth;
|
||||
try {
|
||||
integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
const obj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
plaintext: refreshToken
|
||||
});
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
_id: integrationAuthId
|
||||
}, {
|
||||
refreshCiphertext: obj.ciphertext,
|
||||
refreshIV: obj.iv,
|
||||
refreshTag: obj.tag
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to set integration auth refresh token');
|
||||
}
|
||||
let integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
const obj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
plaintext: refreshToken
|
||||
});
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
_id: integrationAuthId
|
||||
}, {
|
||||
refreshCiphertext: obj.ciphertext,
|
||||
refreshIV: obj.iv,
|
||||
refreshTag: obj.tag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
|
||||
return integrationAuth;
|
||||
}
|
||||
@ -314,7 +281,7 @@ const setIntegrationAuthRefreshHelper = async ({
|
||||
* @param {String} obj.accessToken - access token
|
||||
* @param {Date} obj.accessExpiresAt - expiration date of access token
|
||||
*/
|
||||
const setIntegrationAuthAccessHelper = async ({
|
||||
export const setIntegrationAuthAccessHelper = async ({
|
||||
integrationAuthId,
|
||||
accessId,
|
||||
accessToken,
|
||||
@ -325,52 +292,38 @@ const setIntegrationAuthAccessHelper = async ({
|
||||
accessToken: string;
|
||||
accessExpiresAt: Date | undefined;
|
||||
}) => {
|
||||
let integrationAuth;
|
||||
try {
|
||||
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
plaintext: accessToken
|
||||
});
|
||||
|
||||
let encryptedAccessIdObj;
|
||||
if (accessId) {
|
||||
encryptedAccessIdObj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
plaintext: accessId
|
||||
});
|
||||
}
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
_id: integrationAuthId
|
||||
}, {
|
||||
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
|
||||
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
|
||||
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
|
||||
accessCiphertext: encryptedAccessTokenObj.ciphertext,
|
||||
accessIV: encryptedAccessTokenObj.iv,
|
||||
accessTag: encryptedAccessTokenObj.tag,
|
||||
accessExpiresAt
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to save integration auth access token');
|
||||
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
plaintext: accessToken
|
||||
});
|
||||
|
||||
let encryptedAccessIdObj;
|
||||
if (accessId) {
|
||||
encryptedAccessIdObj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
plaintext: accessId
|
||||
});
|
||||
}
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
_id: integrationAuthId
|
||||
}, {
|
||||
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
|
||||
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
|
||||
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
|
||||
accessCiphertext: encryptedAccessTokenObj.ciphertext,
|
||||
accessIV: encryptedAccessTokenObj.iv,
|
||||
accessTag: encryptedAccessTokenObj.tag,
|
||||
accessExpiresAt,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
|
||||
return integrationAuth;
|
||||
}
|
||||
|
||||
export {
|
||||
handleOAuthExchangeHelper,
|
||||
syncIntegrationsHelper,
|
||||
getIntegrationAuthRefreshHelper,
|
||||
getIntegrationAuthAccessHelper,
|
||||
setIntegrationAuthRefreshHelper,
|
||||
setIntegrationAuthAccessHelper
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Key, IKey } from '../models';
|
||||
|
||||
interface Key {
|
||||
@ -18,7 +17,7 @@ interface Key {
|
||||
* @param {String} obj.keys.nonce - nonce for encryption
|
||||
* @param {String} obj.keys.userId - id of receiver user
|
||||
*/
|
||||
const pushKeys = async ({
|
||||
export const pushKeys = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
keys
|
||||
@ -27,36 +26,28 @@ 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');
|
||||
}
|
||||
};
|
||||
|
||||
export { pushKeys };
|
||||
// add new shared keys only
|
||||
await Key.insertMany(
|
||||
keys.map((k) => ({
|
||||
encryptedKey: k.encryptedKey,
|
||||
nonce: k.nonce,
|
||||
sender: userId,
|
||||
receiver: k.userId,
|
||||
workspace: workspaceId
|
||||
}))
|
||||
);
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Membership, Key } from '../models';
|
||||
import { Types } from "mongoose";
|
||||
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]
|
||||
@ -9,54 +10,45 @@ import { Membership, Key } from '../models';
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
|
||||
*/
|
||||
const validateMembership = async ({
|
||||
export const validateMembership = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
acceptedRoles,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
acceptedRoles: string[];
|
||||
userId: Types.ObjectId | string;
|
||||
workspaceId: Types.ObjectId | string;
|
||||
acceptedRoles?: Array<"admin" | "member">;
|
||||
}) => {
|
||||
|
||||
let membership;
|
||||
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
|
||||
try {
|
||||
membership = await Membership.findOne({
|
||||
user: userId,
|
||||
workspace: workspaceId
|
||||
}).populate("workspace");
|
||||
|
||||
if (!membership) throw new Error('Failed to find membership');
|
||||
|
||||
if (!acceptedRoles.includes(membership.role)) {
|
||||
throw new Error('Failed to validate membership role');
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to validate membership');
|
||||
}
|
||||
|
||||
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]
|
||||
* @param {Object} queryObj - query object
|
||||
* @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');
|
||||
}
|
||||
|
||||
return membership;
|
||||
export const findMembership = async (queryObj: any) => {
|
||||
const membership = await Membership.findOne(queryObj);
|
||||
return membership;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -67,40 +59,33 @@ const findMembership = async (queryObj: any) => {
|
||||
* @param {String} obj.workspaceId - id of workspace.
|
||||
* @param {String[]} obj.roles - roles of users.
|
||||
*/
|
||||
const addMemberships = async ({
|
||||
export const addMemberships = async ({
|
||||
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
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
await Membership.bulkWrite(operations as any);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to add users to workspace');
|
||||
}
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -108,33 +93,19 @@ const addMemberships = async ({
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.membershipId - id of membership to delete
|
||||
*/
|
||||
const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
||||
let deletedMembership;
|
||||
try {
|
||||
deletedMembership = await Membership.findOneAndDelete({
|
||||
_id: membershipId
|
||||
});
|
||||
export const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
||||
const 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,
|
||||
});
|
||||
}
|
||||
|
||||
return deletedMembership;
|
||||
};
|
||||
|
||||
export {
|
||||
validateMembership,
|
||||
addMemberships,
|
||||
findMembership,
|
||||
deleteMembership
|
||||
};
|
||||
|
@ -1,40 +1,56 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { MembershipOrg, Workspace, Membership, Key } from '../models';
|
||||
import {
|
||||
MembershipOrg,
|
||||
Workspace,
|
||||
Membership,
|
||||
Key
|
||||
} from '../models';
|
||||
import {
|
||||
MembershipOrgNotFoundError,
|
||||
UnauthorizedRequestError
|
||||
} from '../utils/errors';
|
||||
|
||||
/**
|
||||
* Validate that user with id [userId] is a member of organization with id [organizationId]
|
||||
* and has at least one of the roles in [acceptedRoles]
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.userId
|
||||
* @param {Types.ObjectId} obj.organizationId
|
||||
* @param {String[]} obj.acceptedRoles
|
||||
*/
|
||||
const validateMembership = async ({
|
||||
export const validateMembershipOrg = async ({
|
||||
userId,
|
||||
organizationId,
|
||||
acceptedRoles
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
}: {
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
acceptedRoles: string[];
|
||||
userId: Types.ObjectId;
|
||||
organizationId: Types.ObjectId;
|
||||
acceptedRoles?: Array<'owner' | 'admin' | 'member'>;
|
||||
acceptedStatuses?: Array<'invited' | 'accepted'>;
|
||||
}) => {
|
||||
let membership;
|
||||
try {
|
||||
membership = await MembershipOrg.findOne({
|
||||
user: new Types.ObjectId(userId),
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
if (!membership) throw new Error('Failed to find organization membership');
|
||||
|
||||
if (!acceptedRoles.includes(membership.role)) {
|
||||
throw new Error('Failed to validate organization membership role');
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to validate organization membership');
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: userId,
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
if (!membershipOrg) {
|
||||
throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' });
|
||||
}
|
||||
|
||||
return membership;
|
||||
if (acceptedRoles) {
|
||||
if (!acceptedRoles.includes(membershipOrg.role)) {
|
||||
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership role' });
|
||||
}
|
||||
}
|
||||
|
||||
if (acceptedStatuses) {
|
||||
if (!acceptedStatuses.includes(membershipOrg.status)) {
|
||||
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership status' });
|
||||
}
|
||||
}
|
||||
|
||||
return membershipOrg;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,16 +59,8 @@ const validateMembership = async ({
|
||||
* @param {Object} queryObj - query object
|
||||
* @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');
|
||||
}
|
||||
|
||||
export const findMembershipOrg = (queryObj: any) => {
|
||||
const membershipOrg = MembershipOrg.findOne(queryObj);
|
||||
return membershipOrg;
|
||||
};
|
||||
|
||||
@ -64,7 +72,7 @@ const findMembershipOrg = (queryObj: any) => {
|
||||
* @param {String} obj.organizationId - id of organization.
|
||||
* @param {String[]} obj.roles - roles of users.
|
||||
*/
|
||||
const addMembershipsOrg = async ({
|
||||
export const addMembershipsOrg = async ({
|
||||
userIds,
|
||||
organizationId,
|
||||
roles,
|
||||
@ -75,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);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -109,55 +111,41 @@ const addMembershipsOrg = async ({
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.membershipOrgId - id of organization membership to delete
|
||||
*/
|
||||
const deleteMembershipOrg = async ({
|
||||
export const deleteMembershipOrg = async ({
|
||||
membershipOrgId
|
||||
}: {
|
||||
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 {
|
||||
validateMembership,
|
||||
findMembershipOrg,
|
||||
addMembershipsOrg,
|
||||
deleteMembershipOrg
|
||||
};
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user