mirror of
https://github.com/Infisical/infisical.git
synced 2025-04-06 22:14:48 +00:00
Compare commits
3377 Commits
v0.2.2
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
1a375ec45b | |||
81f3a6a7a8 | |||
dc8b64708a | |||
dd3790c995 | |||
a91b6ebc03 | |||
e68d1d06a8 | |||
8f754d659a | |||
bef28fea2d | |||
28f15de8df | |||
66d258f02b | |||
d13eafcef7 | |||
6db47499de | |||
1cefb036e0 | |||
af77ad8b2f | |||
dd0f9f5216 | |||
29df6f067e | |||
4dc9a3692e | |||
49bb2121b9 | |||
07679d9318 | |||
fb271726fe | |||
f9e7d4ddd2 | |||
442c454932 | |||
8b22ee1fac | |||
8a10dc8983 | |||
d57920706a | |||
425611b409 | |||
b20489982d | |||
4b569ee4b4 | |||
af9661b221 | |||
53f16f01b8 | |||
f519d1cffc | |||
b8b28d2710 | |||
deab700716 | |||
4d184003a8 | |||
9849312317 | |||
79454a6aea | |||
8e0fb9fe9b | |||
a729114376 | |||
0c20cb0f91 | |||
d1597ed8da | |||
6cee8dc7e5 | |||
13040439c3 | |||
9a4f294749 | |||
2436a572f0 | |||
7cd21e3a93 | |||
e28416b50b | |||
9677836b76 | |||
ca858f8e13 | |||
c2beff493f | |||
34fafd815c | |||
c05ebbb864 | |||
372f2be2f3 | |||
23e621f557 | |||
464b80140f | |||
01cd496afe | |||
6094940a8b | |||
30b959babb | |||
cec14efe86 | |||
b3f090d87e | |||
1deb6827cf | |||
24dbf11962 | |||
20fb3906aa | |||
e7e2ca0f45 | |||
caabf2c952 | |||
35eade0206 | |||
6d1b79430d | |||
7864524944 | |||
adc90e91fe | |||
db7db0cc04 | |||
aa4d9ad267 | |||
27fd857120 | |||
205e46571a | |||
70a6a7cfa1 | |||
558315c24b | |||
a2bd808196 | |||
cfb0d4ee96 | |||
15fc4fd609 | |||
83bff9ae14 | |||
86ba6355cf | |||
6b427adfe0 | |||
01f711ad19 | |||
fa572f7ee0 | |||
249edf98e9 | |||
753a4daf69 | |||
b9320ed9bd | |||
8eace5528f | |||
9407c16e83 | |||
dcdcc40a4a | |||
edd78eaeba | |||
c21ea6fb75 | |||
a60dbe523b | |||
bb9a6b4272 | |||
eaca1b694a | |||
0afb44af29 | |||
3b39e38c89 | |||
4189d1028d | |||
f227824fb8 | |||
49d6918297 | |||
00212f1c72 | |||
987f0c9081 | |||
e4f00f74e9 | |||
ce580f417e | |||
c1662d6db5 | |||
2dae59c6be | |||
9bd764e535 | |||
e4f32f092a | |||
e02c082c7a | |||
80b6c4ad94 | |||
42eb01e1e2 | |||
b8157122e6 | |||
4f66749430 | |||
bfe5ee672b | |||
32a1a0a9e5 | |||
f7e1da65d5 | |||
6bf9bc1d2c | |||
59c747cf72 | |||
b04030a060 | |||
e8fd693da3 | |||
6c2803da93 | |||
4031f4a559 | |||
8ab89bc420 | |||
e46e87d758 | |||
9c2ef15314 | |||
3213dafba9 | |||
33c3c5ecc5 | |||
627c8711dc | |||
8d3d4f222f | |||
65d3038414 | |||
6b7b888fb4 | |||
bf059e0fe5 | |||
28fdf4ed4b | |||
2606e42079 | |||
b1285b401b | |||
3afafbb885 | |||
257547ff4d | |||
da4ae3c503 | |||
fb79e9e6fb | |||
5373cb6afb | |||
31c0bf6831 | |||
67618046c0 | |||
df642179ba | |||
c7d0f2325c | |||
bb6d482ff9 | |||
4f619d7e48 | |||
295c1e5d4a | |||
0be3ad9517 | |||
606ed25104 | |||
c880a48749 | |||
92f7b45e01 | |||
5fe8bdc00b | |||
9f813d72f2 | |||
d90fdac5ce | |||
87709dc86f | |||
deb8e74749 | |||
3519412639 | |||
0506389ada | |||
dd7c449483 | |||
c7572a3374 | |||
fe416556f2 | |||
20fb99f042 | |||
daa94db874 | |||
850e7bff98 | |||
bfaf87c4c2 | |||
55f1392faf | |||
0bf658e501 | |||
aed94ff5bb | |||
0d3f09d668 | |||
16f0ac6d43 | |||
6e6a1c87f2 | |||
b5aa6c0000 | |||
784cdb4201 | |||
17e61bfc68 | |||
a6a60b7bbb | |||
d154f68a59 | |||
f5159583ae | |||
771bec6d6d | |||
010963a80c | |||
0e1191f2ea | |||
8a6ab7f2f6 | |||
4f3582a98b | |||
498a90c484 | |||
76e5d61da5 | |||
53bb3bc610 | |||
1df7b88abf | |||
3670b16657 | |||
9a4b2f7d81 | |||
fadb36edb8 | |||
fbe5a1adb0 | |||
d0695a8998 | |||
a19e8ad016 | |||
15b57de0ed | |||
aaba4a0895 | |||
f3b37de3f3 | |||
fcfd6b3fb2 | |||
05205d1eff | |||
2243bcb3a4 | |||
356e981401 | |||
5b41fb0ff5 | |||
8893aec213 | |||
c4cb8f8008 | |||
046557c97f | |||
a15ba28c18 | |||
8386f4dcbd | |||
ada0fd9c5b | |||
6376c29e49 | |||
402692614e | |||
34de6d4e29 | |||
829e906650 | |||
b7cbb0f1a8 | |||
a50ffbb59d | |||
48eda0c684 | |||
ed89413689 | |||
0c94f77a6d | |||
e6068826f8 | |||
cfa0a2044e | |||
134b503c28 | |||
efcbf1aa88 | |||
284c18db07 | |||
1410a44610 | |||
746ffb3840 | |||
f9f12eafdf | |||
11470a5a0e | |||
9fe2190115 | |||
9e2bd31833 | |||
e88b0ad3c4 | |||
74644fd8bb | |||
2069ac1554 | |||
5a2516e0a7 | |||
b52bc3bed7 | |||
4a153e5658 | |||
7324822be5 | |||
766f301aea | |||
8fbc930012 | |||
0e5190a920 | |||
b815e3eb56 | |||
31231cfcca | |||
ee772e4a77 | |||
7bc29c5981 | |||
e9a89930da | |||
b85499859c | |||
7f17194c0f | |||
1e1ad450d2 | |||
5287b322d8 | |||
45d96be1ff | |||
12840bfdbd | |||
fef5369738 | |||
c94b7d63f6 | |||
485ddc5c50 | |||
edd9c66e49 | |||
0a3b85534b | |||
ec2cc5162e | |||
7ce472957c | |||
8529e0da3d | |||
e5a5433f10 | |||
ee6e518ff8 | |||
15a7222505 | |||
25d482cc62 | |||
785a2bec6a | |||
449466f326 | |||
4131e9c3f1 | |||
310595256f | |||
1737880e58 | |||
b72483f5f2 | |||
ee14bda706 | |||
e56463d52b | |||
ebd3d7c7c4 | |||
9ecbfe201b | |||
ba2a03897f | |||
304f14c0ed | |||
51e5c25e16 | |||
0f6490b1e7 | |||
f894e48fcb | |||
37cfa22619 | |||
94557344b7 | |||
d5063018eb | |||
51d68505d3 | |||
ade27ad072 | |||
683c512bce | |||
43ff28b5fb | |||
ce41855e84 | |||
d24461b17c | |||
1797e56f9f | |||
74f3ca5356 | |||
db27beaf0b | |||
d6e55f51f2 | |||
e9b5996567 | |||
094fe73917 | |||
dc3f85e92e | |||
c463256058 | |||
8df22302fd | |||
f37fa2bbf5 | |||
597c9d6f2a | |||
24d2eea930 | |||
382cb910af | |||
6725475575 | |||
026864951b | |||
287ed05ab7 | |||
37b036e614 | |||
024914c168 | |||
19e8b6d37b | |||
b6d648f1f3 | |||
a514a62a29 | |||
2f24956651 | |||
13d058025c | |||
8ccaa7f29b | |||
b83964051c | |||
0a2b078bdc | |||
40d16fa996 | |||
a3739cfe50 | |||
a73623258e | |||
6da39f41a6 | |||
69bbbfcfd8 | |||
c9d58ec77d | |||
cb364186d8 | |||
918afe05b6 | |||
e822820151 | |||
b5ac49eefe | |||
b21d1a0ed2 | |||
70f1122362 | |||
ea03db8a2c | |||
38d9abca17 | |||
5bed2580c3 | |||
d0b899897b | |||
1861dc85de | |||
bc6bf33674 | |||
44fd35baf5 | |||
8ddfee4c36 | |||
4d0bff4377 | |||
c7b2489d0b | |||
68eb0f8dd9 | |||
5941e8e836 | |||
80e50d13ec | |||
99c8dda4e1 | |||
14c8e3fa3b | |||
7aa3cb53a2 | |||
567309e848 | |||
f264340903 | |||
51b788cc5b | |||
8e0f424249 | |||
f3767d3963 | |||
51cbfdbc46 | |||
f5a580eb72 | |||
460ebf3296 | |||
7f7f11c970 | |||
f799e224a0 | |||
8a87277fe6 | |||
32805c726a | |||
6c4a6d31e4 | |||
e7b89b645f | |||
b60cf2eb07 | |||
cf5a79995f | |||
c51f09fd3a | |||
f9444c5205 | |||
7dd0943b2d | |||
31a9f032b3 | |||
9c55d1906d | |||
ff54a20ace | |||
8bf7eba07b | |||
bb75ea550a | |||
344f7276d2 | |||
c375662411 | |||
cc4ad1df4b | |||
c92c0f7288 | |||
fbe0cf006f | |||
d2f959558e | |||
e50c89e326 | |||
6cda14328b | |||
b551ee50e7 | |||
93aeacc6b6 | |||
f940f8b79d | |||
72ac2c04b8 | |||
bb3d591f21 | |||
763ce1b206 | |||
1f97ac5192 | |||
5f29562fad | |||
f3e8ef1537 | |||
544d37bbc4 | |||
4f6adb50d1 | |||
444ce9508d | |||
aabd896c37 | |||
50ef23e8a0 | |||
b87f51a044 | |||
1233d9c1a0 | |||
ff0b4d7f2b | |||
ef61bc6a40 | |||
13ee8c4e13 | |||
6ea9fc7134 | |||
00e1742a30 | |||
5055b5402c | |||
ff9418c0c7 | |||
d03921eef3 | |||
602afdefc3 | |||
5eb505326b | |||
fcf4153d87 | |||
097282c5e1 | |||
0eeef9a66c | |||
df0bec8a68 | |||
13014b5345 | |||
66d0cae066 | |||
8e82222fc5 | |||
f822bcd10f | |||
89d0c0e3c3 | |||
a4f6b828ad | |||
0fb2056b8b | |||
c51f8c5838 | |||
ec5cf97f18 | |||
69b57817d6 | |||
aafbe40c02 | |||
9d9b83f909 | |||
ea1f144b54 | |||
591f33ffbe | |||
855158d0bb | |||
87e997e7a0 | |||
3c449214d1 | |||
d813f0716f | |||
6787c0eaaa | |||
377a79f17d | |||
c91f6521c1 | |||
0ebd1d3d81 | |||
d257a449bb | |||
6a744c96e5 | |||
2a768a7bc4 | |||
28b617fd89 | |||
8b1eaad7b5 | |||
c917cf8a18 | |||
282830e7a2 | |||
3d6f04b94e | |||
60a5092947 | |||
69dae1f0b2 | |||
4b41664fa4 | |||
735cf093f0 | |||
98906f190c | |||
5f80e2f432 | |||
afd6a7736a | |||
057fcb164d | |||
b575c0e207 | |||
372afa2111 | |||
6557d7668e | |||
33017b50f0 | |||
f5de501348 | |||
758c3a2423 | |||
d01c6e4df9 | |||
ed94d218fd | |||
adc7be2e84 | |||
697485f8ed | |||
19e2523d0e | |||
8851987ac4 | |||
64d862ebe9 | |||
70d1cc0e06 | |||
aee91a9558 | |||
d6218eaa82 | |||
8886e57d4f | |||
6efb58da1a | |||
f187cc2c26 | |||
d5c5495475 | |||
8e33692cda | |||
6c31e70f4f | |||
d7026cbbfa | |||
df0e0bf988 | |||
640366a0ec | |||
ab5514fcf7 | |||
c8951f347b | |||
d76f556464 | |||
6ccaa24e59 | |||
4ec0c9cdbf | |||
75cd3bfa35 | |||
1db7d50a09 | |||
b4980b4a53 | |||
1351ba936f | |||
57bccaefba | |||
86af452888 | |||
6cef7532da | |||
74e33144a7 | |||
a9776eaeb5 | |||
3c2a66f722 | |||
d92b1d3cd8 | |||
f560242f1d | |||
33fc968055 | |||
f289b99cf1 | |||
019b477d2d | |||
9ad2d9d218 | |||
a575530ddf | |||
33ea019d70 | |||
5ae3b66e2e | |||
20210d7471 | |||
77e3d10a64 | |||
814b71052d | |||
6579b3c93f | |||
99c41bb63b | |||
63df0dba64 | |||
4e050cfe7a | |||
32f5c96dd2 | |||
d7262d4291 | |||
7a9221769d | |||
5e5761424a | |||
5b923c25b5 | |||
46c76e3984 | |||
b212681d09 | |||
be67b9b341 | |||
29016fbb23 | |||
0c0139ac8f | |||
180274be34 | |||
595a26a366 | |||
41c41a647f | |||
c3d2b7d3fc | |||
84e32faac9 | |||
87984a704a | |||
33e4104e98 | |||
b5d5eb87a7 | |||
bcfb14ca86 | |||
87e2844499 | |||
ae4b8ca9b2 | |||
0cff39f918 | |||
53ac05694c | |||
097a8cae89 | |||
597e1e1ca8 | |||
4757ceb938 | |||
e3d536ef58 | |||
89ae3070ce | |||
f3895b70ee | |||
3478d71e99 | |||
1f5e458b64 | |||
6f8373c977 | |||
c55a36b291 | |||
e4a04bdf0a | |||
4db9a5279f | |||
c37ff79927 | |||
98e299c2ac | |||
9d9e830d73 | |||
d909ff6a97 | |||
5301bcc91f | |||
77438f9282 | |||
122a1e32e1 | |||
9d2a08dbec | |||
87c2e417d2 | |||
341a745843 | |||
085ddb2c48 | |||
6734ce50a5 | |||
94f893017b | |||
a0dfa5eedf | |||
1fea2f1121 | |||
7fe8999432 | |||
fca5ae9172 | |||
4aacbed28b | |||
9fbf01c19e | |||
954bc0c5f1 | |||
4ac3669756 | |||
6b334b3103 | |||
3aae1b8432 | |||
e462722ec3 | |||
f58c560fc0 | |||
d035fe1008 | |||
8be0071413 | |||
c3ca992777 | |||
829f65cdb7 | |||
7785fbafbd | |||
35cff782e1 | |||
a35643bf6e | |||
85de985321 | |||
40f5bbbc07 | |||
85254ba984 | |||
4cb586996c | |||
29fa85e499 | |||
df7d8e7be9 | |||
1de5fd28a9 | |||
b3cdc4fdd2 | |||
5ee79be873 | |||
cae7e1808d | |||
131d5d7207 | |||
393cfe8953 | |||
5098c0731b | |||
c9ed5f793a | |||
50ce977c55 | |||
c29a11866e | |||
b3a468408e | |||
d1a26766ca | |||
73c8e8dc0f | |||
32882848ba | |||
5fb406884d | |||
176d92546c | |||
1063c12d25 | |||
3402acb05c | |||
db7a064961 | |||
b521d9fa3a | |||
73c7b917ab | |||
a8470d2133 | |||
ca8fff320d | |||
f9c28ab045 | |||
d4a5eb12e8 | |||
86d82737f4 | |||
abbeb67b95 | |||
c0c96d6407 | |||
58ff6a43bc | |||
079a09a3d1 | |||
a07bd5ad40 | |||
9cc99e41b8 | |||
f256493cb3 | |||
7bf2e96ad3 | |||
40238788e5 | |||
75eeda4278 | |||
c1ea441e3a | |||
8b522a3fb5 | |||
c36352f05f | |||
2de898fdbd | |||
bc68a00265 | |||
1382688e58 | |||
9248f36edb | |||
c9c40521b2 | |||
97e4338335 | |||
82e924baff | |||
2350219cc9 | |||
28d7c72390 | |||
e7321e8060 | |||
28a2aebe67 | |||
20d4f16d33 | |||
7d802b41a8 | |||
75992e5566 | |||
911aa3fd8a | |||
7622a3f518 | |||
3b0bd362c9 | |||
ad4513f926 | |||
a2c1a17222 | |||
279958d54c | |||
1be46b5e57 | |||
98d9dd256b | |||
e5eee14409 | |||
f3c76c79ee | |||
5bdb6ad6a1 | |||
c6846f8bf1 | |||
46f03f33b0 | |||
6280d7eb34 | |||
29286d2125 | |||
c9f01ce086 | |||
bc43e109eb | |||
238c43a360 | |||
040a50d599 | |||
8a1a3e9ab9 | |||
2585d50b29 | |||
4792e752c2 | |||
1d161f6c97 | |||
0d94b6deed | |||
75428bb750 | |||
d90680cc91 | |||
031c05b82d | |||
d414353258 | |||
ffc6dcdeb4 | |||
dfc74262ee | |||
59e46ef1d0 | |||
36e4cd71d3 | |||
d60b3d1598 | |||
e555a8d313 | |||
44ec88acd6 | |||
15504346cd | |||
508ed7f7d6 | |||
52bcee2785 | |||
c097e43a4e | |||
65afaa8177 | |||
01cbd4236d | |||
40a9a15709 | |||
abfc69fc75 | |||
3ea20328dc | |||
2aa46f5a65 | |||
f0ca059b17 | |||
b1369d66c2 | |||
151ba2ffc9 | |||
152feba1fa | |||
16125157f3 | |||
c0592ad904 | |||
32970e4990 | |||
7487b373fe | |||
6df678067c | |||
b97dc7f599 | |||
96db649cbc | |||
80ce695355 | |||
93a1725da6 | |||
619bbf2027 | |||
1476d06b7e | |||
fb59b02ab4 | |||
fc3db93f8b | |||
120f1cb5dd | |||
bb9b060fc0 | |||
26605638fa | |||
76758732af | |||
827d5b25c2 | |||
b32b19bcc1 | |||
69b9881cbc | |||
1084323d6d | |||
c98c45157a | |||
9a500504a4 | |||
6009dda2d2 | |||
d4e8162c41 | |||
f6ad641858 | |||
32acc370a4 | |||
ba9b1b45ae | |||
e05b26c727 | |||
e22557b4bb | |||
cbbb12c74e | |||
60beda604f | |||
ae50987f91 | |||
32977e06f8 | |||
4d78f4a824 | |||
47bf483c2e | |||
40e5ecfd7d | |||
0fb0744f09 | |||
1510c39631 | |||
d0579b383f | |||
b4f4c1064d | |||
d72d3940e6 | |||
7217bcb3d8 | |||
2faa9222d8 | |||
058712e8ec | |||
589f0bc134 | |||
bd6dc3d4c0 | |||
9338babda6 | |||
6f9e8644d7 | |||
2fdb10277e | |||
15d2c536ed | |||
e13b3f72b1 | |||
a6e02238ad | |||
ebe4f70b51 | |||
c3c7316ec0 | |||
2cd791a433 | |||
a304228961 | |||
c865b78b41 | |||
be80ac999e | |||
076fe58325 | |||
66bfab1994 | |||
b92c50addd | |||
8fbca05052 | |||
d99830067e | |||
cdc8ef95ab | |||
072e97b956 | |||
4f26a7cad3 | |||
7bb6ff3d0c | |||
ecccec8e35 | |||
7fd15a06e5 | |||
5d4a37004e | |||
aa61fd091d | |||
04ac54bcfa | |||
38dbf1e738 | |||
ddf9d7848c | |||
0b40de49ec | |||
b1d16cab39 | |||
fb7c7045e9 | |||
d570828e47 | |||
2a92b6c787 | |||
ee54fdabe1 | |||
912818eec8 | |||
136308f299 | |||
ba41244877 | |||
c4dcf334f0 | |||
66bac3ef48 | |||
e5347719c3 | |||
275416a08f | |||
abe1f54aab | |||
13c1e2b349 | |||
f5a9afec61 | |||
d0a85c98b2 | |||
e0669cae7c | |||
e0dfb2548f | |||
01997a5187 | |||
2c011b7d53 | |||
1b24a9b6e9 | |||
00c173aead | |||
2e15ad0625 | |||
3f0b6dc6c1 | |||
f766a1eb29 | |||
543c55b5a6 | |||
cdb1d38f99 | |||
0a53b72cce | |||
840eef7bce | |||
70b9d435d1 | |||
b921c376b2 | |||
b1ec59eb67 | |||
4e6e12932a | |||
792c382743 | |||
f5c8e537c9 | |||
4bf09a8efc | |||
001265cf2a | |||
a56a135396 | |||
9838c29867 | |||
4f5946b252 | |||
dc23517133 | |||
5e4d4f56e3 | |||
a855a2cee6 | |||
e86258949c | |||
f119c921d0 | |||
b6ef55783e | |||
feade5d029 | |||
8f74d20e74 | |||
0eb7896b59 | |||
9fcecc9c92 | |||
ee6afa8983 | |||
6f4ac02558 | |||
5971480ca9 | |||
d222b09ba5 | |||
a9fd0374bd | |||
ca008c809a | |||
6df7590051 | |||
60bd5e57fc | |||
703a7a316a | |||
f4de7a2c56 | |||
383825672b | |||
c6124d7444 | |||
ee80f4a89b | |||
0b3b014bf5 | |||
7f463cabce | |||
b1962129a3 | |||
28ad403665 | |||
cb893f71ee | |||
80a3ea42ac | |||
aafd7f0884 | |||
faacb75034 | |||
7caac2e64c | |||
df636c91b4 | |||
9dc97f7208 | |||
4fd227c85f | |||
04c7d49477 | |||
63588b4e44 | |||
fc43511f5d | |||
a995627815 | |||
c2f7923c1d | |||
abf6034aca | |||
5fce85ca41 | |||
702d28faca | |||
dbeafe1f5d | |||
46f700023b | |||
25b988ca9a | |||
41af5cea93 | |||
e21daf6771 | |||
c0f81ec84e | |||
c85e2c71ca | |||
9ae6e5ea1c | |||
d3026a98d8 | |||
14b8c2c12a | |||
d9a69441c4 | |||
f46a23dabf | |||
9e2d6daeba | |||
a2bebb5afa | |||
9fc5303a97 | |||
97a5b509b7 | |||
7660119584 | |||
a51d202d51 | |||
34273b30f2 | |||
726f38c15f | |||
390c2cc4d9 | |||
49098b7693 | |||
501d940558 | |||
7234c014c8 | |||
f3908e6b2a | |||
cf4eb629f2 | |||
95af82963f | |||
bd8c17d720 | |||
d3bc95560c | |||
4a838b788f | |||
01c655699c | |||
3456dfbd86 | |||
560bde297c | |||
c2ca4d77fc | |||
3b3f78ee3c | |||
d6a5c50fd9 | |||
839fcc2775 | |||
eb2f433f43 | |||
04c74293ed | |||
cdf4440848 | |||
20b584b7b8 | |||
3779209ed5 | |||
9546916aad | |||
59c861c695 | |||
105c2e51ee | |||
66aa218ad9 | |||
fb3a386aa3 | |||
d723d26d2e | |||
8a576196a3 | |||
2cf5fd80ca | |||
74534cfbaa | |||
66787b1f93 | |||
890082acbc | |||
a364b174e0 | |||
2bb2ccc19e | |||
3bbf770027 | |||
2610356d45 | |||
67e164e2bb | |||
84fcb82116 | |||
4502d12e46 | |||
ef6ee6b2e6 | |||
e902a54af0 | |||
50efb8b8bd | |||
5450c1126a | |||
4929022523 | |||
85378e25aa | |||
b54c29fc48 | |||
fcf3f2837e | |||
0ada343b6f | |||
d0b8aba990 | |||
4365be9b75 | |||
b0c398688b | |||
1141408d5b | |||
b24bff5af6 | |||
a1dc405516 | |||
896a34eb65 | |||
c67432a56f | |||
edeb6bbc66 | |||
f72e240ce5 | |||
77ec17ccd4 | |||
6e992858aa | |||
9cda85f03e | |||
ddae305fdb | |||
8265d18934 | |||
c65a53f1f7 | |||
aa1e0b0f28 | |||
4683dc7869 | |||
4c1324baa9 | |||
bc489e65ca | |||
5128466233 | |||
e11abb619a | |||
f51e9ba8ff | |||
45b85ab962 | |||
698a268b5f | |||
0d74752169 | |||
255705501f | |||
a255af6ad8 | |||
30da2e50b1 | |||
7f9bd93382 | |||
e81ea314e1 | |||
f19aca2904 | |||
763bdabd60 | |||
7ec708b71d | |||
2eff06cf06 | |||
a024eecf2c | |||
a2ad9e10b4 | |||
3c6c1891a8 | |||
01d3d84b40 | |||
32bec03adf | |||
f59b3b3305 | |||
5b6c2e05f2 | |||
c623f572b7 | |||
53856ff868 | |||
84d094b4d8 | |||
48f7bd146f | |||
da6fa6d8ce | |||
cf8e597c7d | |||
43c31332e4 | |||
88fbf6f88e | |||
119730ac1a | |||
1d66dbbce3 | |||
b0991c33b0 | |||
7fa4e09874 | |||
efb14ca267 | |||
d863dece79 | |||
96fbc6c5a0 | |||
1896442168 | |||
a93631d41c | |||
2c7aac37a2 | |||
6b8d4c2fea | |||
f84235eea3 | |||
63e8ecce5b | |||
ef7bf09398 | |||
3be3867579 | |||
7f753b23f8 | |||
81827e2deb | |||
f02ea8d9b8 | |||
1609bd4652 | |||
a620f1c924 | |||
0a3e7731d9 | |||
0ca8425965 | |||
14a260b785 | |||
b6219e14f0 | |||
663c4869b9 | |||
3103075c3f | |||
e3ef826f52 | |||
20c4e956aa | |||
4a227d05ce | |||
6f57ef03d1 | |||
257b4b0490 | |||
215ef0bb29 | |||
9cc220e51f | |||
8fa90d94ac | |||
609204f7f6 | |||
d501130e64 | |||
45734d78c0 | |||
dd9a2dd345 | |||
80bec24219 | |||
1cdd840485 | |||
4765dd0696 | |||
0d4cacdc3e | |||
0b59a92dfb | |||
64d5a82e1b | |||
a89ed40dcd | |||
1634f9ec49 | |||
dd1bb84361 | |||
0f003e8ab6 | |||
14d253d01a | |||
750c1b46da | |||
2e07512bae | |||
20a6497218 | |||
72839719fd | |||
d8d480f2bc | |||
58c3a4ebc1 | |||
0d83954c39 | |||
80cee40b39 | |||
6059070d29 | |||
f16944024b | |||
29da8843a3 | |||
8cd6a1f564 | |||
e8fd3c8045 | |||
59cd8580d5 | |||
859cec49d1 | |||
fccbf9810f | |||
5494bc6c3c | |||
95385b1f45 | |||
b88a319582 | |||
db5883ae56 | |||
26229b07bc | |||
3ab5db9b2a | |||
717b831e94 | |||
336b5897f0 | |||
0ce5aaf61c | |||
adfa90340d | |||
444aca0070 | |||
029766c534 | |||
bde788c4f6 | |||
9b14b64ec2 | |||
0a72dccdcf | |||
7fe94d66cd | |||
f503f8c76d | |||
7982b1d668 | |||
7a78209613 | |||
019024e4ae | |||
4d6895a793 | |||
36b5ba2855 | |||
44d2a6c553 | |||
a073a746f2 | |||
edb3e66267 | |||
75be302166 | |||
b459d2d5f5 | |||
942e1a82c2 | |||
9d2bc25cb4 | |||
13083d7676 | |||
18d843f3e6 | |||
ee96325034 | |||
954f15e4df | |||
88842951cb | |||
8e88a3a25f | |||
f1e1ca07df | |||
5bf2c2f52b | |||
3d2a2651b8 | |||
0f02ef701e | |||
1c5e80e68a | |||
c30381edbc | |||
2554ad2b3c | |||
c3696bdbbc | |||
1be924d210 | |||
2333675262 | |||
c3c16f4e42 | |||
99a2203b38 | |||
92b64d3553 | |||
533e628183 | |||
3b7096710c | |||
97b7a5ebdf | |||
4f69257595 | |||
8f93141d54 | |||
2bbba8a43a | |||
4da251bdfc | |||
8520ae8d43 | |||
e38abb128a | |||
d46a6f7270 | |||
33adbc0f24 | |||
fc3b0e1de9 | |||
ba225dd504 | |||
d221bf8ae9 | |||
d7354e1aca | |||
86b2b95d11 | |||
e7c5e6a789 | |||
1ca106279e | |||
49d6f85f42 | |||
c894952e84 | |||
4ce0eccfa1 | |||
ad710f4860 | |||
45117ba1f4 | |||
1b2b1ca30b | |||
b89a90066a | |||
83d2a39fb1 | |||
412b1123af | |||
dc91615b43 | |||
110153385b | |||
e37810f302 | |||
f02e39e7e3 | |||
305ddd3813 | |||
b023bb1df2 | |||
63ff669612 | |||
8003273b2c | |||
2f9b35b2f9 | |||
91c1aca588 | |||
e8f7b0c181 | |||
43735b8183 | |||
988bb4ffb6 | |||
0f46f53a7d | |||
9c9d46824c | |||
43b97b411b | |||
38c044f9a7 | |||
c91a93ef2a | |||
a4ef829046 | |||
2ed079830a | |||
98893a40f1 | |||
252042fb20 | |||
4ca95f4d79 | |||
26028e7312 | |||
6bbdc4a405 | |||
cb9ee00ed3 | |||
f1a291a52a | |||
bcfe1bda84 | |||
82d4c8f000 | |||
7c698e755a | |||
4b0bc238fc | |||
ea9e638d03 | |||
6671699867 | |||
549121f44e | |||
520a553ea1 | |||
aac3168c80 | |||
34fb7be1c4 | |||
e342e88499 | |||
96437fd1b7 | |||
c5f76b1e6f | |||
1167b1bc60 | |||
1bf9041ac9 | |||
6bca7dcc58 | |||
a9f062b469 | |||
43caccad9f | |||
9cef1d3b10 | |||
6c126606da | |||
ae37d80891 | |||
8e6b0ca0a9 | |||
a288a0bf2a | |||
9dd77ae36e | |||
1e20715511 | |||
d07b2dafc3 | |||
04548313ab | |||
86bf2ddd89 | |||
2ad663c021 | |||
56317a3f53 | |||
ad0bc4efdc | |||
bf74b75c4a | |||
7f543b635c | |||
353dfeb2a9 | |||
16196e6343 | |||
3f2b74a28a | |||
4a603da425 | |||
0d9ce70000 | |||
9fa1e415c8 | |||
09b22f36c0 | |||
43c8c42249 | |||
d4a7ad713c | |||
9afad7df32 | |||
3f61a24ef1 | |||
f342c345b7 | |||
6dd46885f8 | |||
5704dfb35f | |||
41774fa97c | |||
32b26c331c | |||
011507b8e0 | |||
4adb2a623e | |||
1d410c8420 | |||
35f3d6c776 | |||
5e24b517cc | |||
bf95415a0d | |||
4025063732 | |||
e3ecfbaaa5 | |||
a7b3d12844 | |||
f5145c6c39 | |||
d9abe671af | |||
37ae05fa2a | |||
086ce0d2a6 | |||
06dec29773 | |||
ed8e942a5d | |||
e770bdde24 | |||
a84dab1219 | |||
02d9d7b6a4 | |||
f21eb3b7c8 | |||
219e3884e7 | |||
41cd8b7408 | |||
f6be86a26b | |||
85e5822ece | |||
5c9e89a8e2 | |||
46a77d5e58 | |||
a6e9643464 | |||
affa2ee695 | |||
dc0d577cbb | |||
9e8ddd2956 | |||
b40b876fb2 | |||
2ba6a65da4 | |||
76cf79d201 | |||
a79c6227b1 | |||
f1f64e6ff5 | |||
d72ddfe315 | |||
f924d0c02c | |||
ef1b75d890 | |||
d8094b2ab1 | |||
ad61fa845c | |||
6bb5e7078f | |||
a07ddb806d | |||
6e7d3d6912 | |||
84a866eb88 | |||
9416fca832 | |||
2ea518b107 | |||
62399dd293 | |||
16f1360550 | |||
a99751eb72 | |||
9ea414fb25 | |||
a9fa3ebab2 | |||
293a62b632 | |||
a1f08b064e | |||
50977cf788 | |||
fccec083a9 | |||
63af7d4a15 | |||
ab3533ce1c | |||
4d6a8f0476 | |||
688cf91eb7 | |||
8ee6710e9b | |||
14fc78eaaf | |||
9fa28f5b5e | |||
368855a44e | |||
ae375916e8 | |||
21f1648998 | |||
88695a2f8c | |||
77114e02cf | |||
3ac1795a5b | |||
8d6f59b253 | |||
7fd77b14ff | |||
8d3d7d98e3 | |||
6cac879ed0 | |||
ac66834daa | |||
0616f24923 | |||
4e1abc6eba | |||
8f57377130 | |||
2d7c7f075e | |||
c342b22d49 | |||
b8120f7512 | |||
ca18883bd3 | |||
8b381b2b80 | |||
6bcf5cb54c | |||
51b425dceb | |||
7ec00475c6 | |||
84840bddb5 | |||
93640c9d69 | |||
ec856f0bcc | |||
3e46bec6f7 | |||
25fc508d5e | |||
ea262da505 | |||
954806d950 | |||
2960f86647 | |||
b2888272f2 | |||
d6d3302659 | |||
e5c87442e5 | |||
be08417c8b | |||
61e44e152c | |||
52c4f64655 | |||
81743d55ab | |||
3e36adcf5c | |||
1f60a3d73e | |||
00089a6bba | |||
026ea29847 | |||
1242d88acb | |||
f47a119474 | |||
0b359cd797 | |||
c5ae402787 | |||
e288402ec4 | |||
196beb8355 | |||
d6222d5cee | |||
e855d4a0ba | |||
20f34b4764 | |||
0eb21919fb | |||
fbeb210965 | |||
0d1aa713ea | |||
9a1b453c86 | |||
534d96ffb6 | |||
5b342409e3 | |||
a9f54009b8 | |||
82947e183c | |||
eb7ef2196a | |||
ad3801ce36 | |||
b7aac1a465 | |||
e28ced8eed | |||
4a95f936ea | |||
85a39c60bb | |||
66ea3ba172 | |||
01d91c0dc7 | |||
dedd27a781 | |||
57a6d1fff6 | |||
554f0c79a4 | |||
2af88d4c99 | |||
fc8b567352 | |||
ec234e198a | |||
6e1cc12e3a | |||
1b4b7a967b | |||
e47d6b7f2f | |||
45a13d06b5 | |||
4a48c088df | |||
2b65f65063 | |||
065e150847 | |||
ab72eb1178 | |||
816099a8b4 | |||
b5f672cc61 | |||
ddc7be18eb | |||
c0ce92cf3d | |||
0073fe459e | |||
a7f52a9298 | |||
29c0d8ab57 | |||
d7b26cbf04 | |||
767abe51ef | |||
5ac1816392 | |||
c5b1e7298e | |||
3436e6be0e | |||
b000a78f74 | |||
cb42db3de4 | |||
11bb0d648f | |||
90517258a2 | |||
d78b37c632 | |||
4a6fc9e84f | |||
8030104c02 | |||
3825269cbb | |||
baa907dbb6 | |||
83465dff2d | |||
67a8211cb0 | |||
bc108a82b6 | |||
05be5910d0 | |||
2341ec0e11 | |||
9652d534b6 | |||
dd8f55804c | |||
95d25b114e | |||
c0f3aecad3 | |||
f650cd3925 | |||
8a514e329f | |||
dbd55441f2 | |||
01e613301a | |||
de7bd27b4b | |||
a4cdd14014 | |||
632c78f401 | |||
051f4501e8 | |||
69605a1a54 | |||
e47912edd7 | |||
a4edf6bd0c | |||
b11cd29943 | |||
395b51c265 | |||
27f56be466 | |||
dfe95ac773 | |||
2dba7847b6 | |||
78802409bd | |||
9963724a6a | |||
b49ef9efc9 | |||
a31ffe9617 | |||
18beed7540 | |||
0a538ac1a7 | |||
a75ad5ef26 | |||
b47f61f1ad | |||
2a1665a2c3 | |||
e993bd048e | |||
11833ccf0f | |||
37d52432d0 | |||
04b7e04d98 | |||
57a3384f32 | |||
c813c91aec | |||
91947df5f6 | |||
8330890087 | |||
221e601173 | |||
28c24fc8c1 | |||
a9389643b8 | |||
58854e6b81 | |||
7ae859e9ae | |||
ff6e07bdcf | |||
fc9393b77f | |||
0cad823267 | |||
97a0728f02 | |||
6cb8cf53f8 | |||
1ac607b42e | |||
ec21e35f8c | |||
2591161272 | |||
be86e4176c | |||
2067c021ed | |||
648968c453 | |||
dc3f2c78c1 | |||
b4dbdbabac | |||
681255187f | |||
bde30049bc | |||
0a140f5333 | |||
3a9bf5409b | |||
04fdccc45d | |||
5604232aea | |||
373dfff8e0 | |||
b9ce448bed | |||
142fcf0a01 | |||
49bcd8839f | |||
d5f6e20c78 | |||
00030f2231 | |||
24d23e89d0 | |||
3fe592686a | |||
9cba0970be | |||
8b50150ec8 | |||
5af1eb508c | |||
9d57b1db87 | |||
9a5329300c | |||
b03c346985 | |||
84efc3de46 | |||
2ff3818ecb | |||
6fbcbc4807 | |||
9048988e2f | |||
98cfd72928 | |||
2293abfc80 | |||
817a783ec2 | |||
9006212ab5 | |||
1627674c2a | |||
bc65bf1238 | |||
3990b6dc49 | |||
a3b8de2e84 | |||
b5bffdbcac | |||
23e40e523a | |||
d1749deff0 | |||
960aceed29 | |||
bd8397bda7 | |||
9dac06744b | |||
bd80c2ccc3 | |||
466dadc611 | |||
cc5ca30057 | |||
62fa59619b | |||
7accaeffcf | |||
7f69a3b23f | |||
285a6d633a | |||
12b71bcf67 | |||
6c0be52ffa | |||
9df51424a2 | |||
bb466dbe1c | |||
531938a3f1 | |||
941a8699b5 | |||
6e42da9063 | |||
b1981df8f0 | |||
086652a89f | |||
6574b6489f | |||
69903c0d5c | |||
8ff33a4e63 | |||
1d71864092 | |||
4b1a27b301 | |||
b78150e78d | |||
a0f08c73af | |||
59ebe0c22e | |||
6729caeb75 | |||
3543a15c09 | |||
33e0f13eea | |||
e9cff4fe69 | |||
26867f7328 | |||
233459d063 | |||
ba6355e4d2 | |||
e961a30937 | |||
53ff420304 | |||
196a613f16 | |||
cc4b749ce8 | |||
8cc5f2ef43 | |||
06bc02c392 | |||
3682c4d044 | |||
52892c26e5 | |||
5ce67bf750 | |||
ed2cf68935 | |||
386bc09d49 | |||
353c6e9166 | |||
1f69467207 | |||
5ab218f1f8 | |||
e1b25aaa54 | |||
9193e7ef58 | |||
3f998296fe | |||
6f7601f2c4 | |||
b7c7544baf | |||
4b7ae2477a | |||
e548883bba | |||
a7ece1830e | |||
6502d232c9 | |||
f31e8ddfe9 | |||
7bbbdcc58b | |||
bca14dd5c4 | |||
b6b3c8a736 | |||
d458bd7948 | |||
239989ceab | |||
7ff13242c0 | |||
7db8555b65 | |||
980a578bd5 | |||
adb27bb729 | |||
d89d360880 | |||
8ed5dbb26a | |||
221a43e8a4 | |||
e8a2575f7e | |||
41c1828324 | |||
c2c8cf90b7 | |||
00b4d6bd45 | |||
f5a6270d2a | |||
bc9d6253be | |||
a5b37c80ad | |||
7b1a4fa8e4 | |||
7457f573e9 | |||
d67e96507a | |||
46545c1462 | |||
8331cd4de8 | |||
3447074eb5 | |||
5a708ee931 | |||
9913b2fb6c | |||
2c021f852f | |||
8dbc894ce9 | |||
511904605f | |||
7ae6d1610f | |||
7da6d72f13 | |||
ad33356994 | |||
cfa2461479 | |||
bf08bfacb5 | |||
cf77820059 | |||
1ca90f56b8 | |||
5899d7aee9 | |||
b565194c43 | |||
86e04577c9 | |||
f4b3cafc5b | |||
18aad7d520 | |||
54c79012db | |||
4b720bf940 | |||
993866bb8b | |||
8c39fa2438 | |||
7bccfaefac | |||
e2b666345b | |||
90910819a3 | |||
8b070484dd | |||
a764087c83 | |||
27d5fa5aa0 | |||
2e7705999c | |||
428bf8e252 | |||
264740d84d | |||
723bcd4d83 | |||
9ed516ccb6 | |||
067ade94c8 | |||
446edb6ed9 | |||
896529b7c6 | |||
5c836d1c10 | |||
409d46aa10 | |||
682c63bc2a | |||
1419371588 | |||
77fdb6307c | |||
c61bba2b6b | |||
2dc0563042 | |||
b5fb2ef354 | |||
dc01758946 | |||
1f8683f59e | |||
a5273cb86f | |||
d48b5157d4 | |||
94a23bfa23 | |||
fcdfa424bc | |||
3fba1b3ff7 | |||
953eed70b2 | |||
39ba795604 | |||
5b36227321 | |||
70d04be978 | |||
c2be6674b1 | |||
565f234921 | |||
ab43e32982 | |||
be677fd6c2 | |||
3d93c6a995 | |||
edb201e11f | |||
1807b3e029 | |||
c02c8e67d3 | |||
d4c5be5f48 | |||
5f33c9a389 | |||
c9acb22261 | |||
33f0510995 | |||
25b239a18b | |||
504e0f6dc3 | |||
f450be3a00 | |||
d9f6c27e4d | |||
9cef35e9e6 | |||
2621ccdcf1 | |||
75e90201c0 | |||
fd3cf70e13 | |||
44108621b4 | |||
5ee65359bf | |||
241dceb845 | |||
af650ef4c7 | |||
817ddd228c | |||
15d81233b4 | |||
705b1833d0 | |||
beb8d2634a | |||
fb3ceb4581 | |||
2df33dc84f | |||
c62504d658 | |||
ce08512ab5 | |||
8abe7c7f99 | |||
043133444d | |||
b3baaac5c8 | |||
aa019e1501 | |||
0f8b505c78 | |||
5b7e23cdc5 | |||
df25657715 | |||
79c2baba1a | |||
52a2a782f1 | |||
eda095b55f | |||
93761f6487 | |||
c5438fbe6d | |||
e8fdaf571c | |||
846e2e037f | |||
a0a7ff8715 | |||
ec1e842202 | |||
83d5291998 | |||
638e011cc0 | |||
d2d23a7aba | |||
a52c2f03bf | |||
51c12e0202 | |||
4db7b0c05e | |||
284608762b | |||
8960773150 | |||
4684c9f8b1 | |||
abbf3e80f9 | |||
d272f580cf | |||
da9cb70184 | |||
1f3f0375b9 | |||
8ad851d4b0 | |||
edef22d28e | |||
3b5bc151ba | |||
678cdd3308 | |||
76f43ab6b4 | |||
33554f4057 | |||
c539d4d243 | |||
124e6dd998 | |||
cef29f5dd7 | |||
95c914631a | |||
49ae61da08 | |||
993abd0921 | |||
f37b497e48 | |||
0d2e55a06f | |||
040243d4f7 | |||
c450b01763 | |||
4cd203c194 | |||
178d444deb | |||
139ca9022e | |||
34d3e80d17 | |||
deac5fe101 | |||
216f3a0d1b | |||
6ee7081640 | |||
43f4110c94 | |||
56d430afd6 | |||
f681f0a98d | |||
23cd6fd861 | |||
cf45c3dc8b | |||
45584e0c1a | |||
202900a7a3 | |||
38b6a48bee | |||
04611d980b | |||
6125246794 | |||
52e26fc6fa | |||
06bd98bf56 | |||
7c24e0181a | |||
53abce5780 | |||
8c844fb188 | |||
ceeebc24fa | |||
df7ad9e645 | |||
a9135cdbcd | |||
9b96daa185 | |||
9919d3ee6a | |||
dfcd6b1efd | |||
07bc4c4a3a | |||
d69465517f | |||
6d807c0c74 | |||
868cc80210 | |||
3d4a616147 | |||
bd3f9130e4 | |||
f607841acf | |||
55d813043d | |||
b2a3a3a0e6 | |||
67d5f52aca | |||
a34047521c | |||
7ff806e8a6 | |||
9763353d59 | |||
4382935cb5 | |||
7e3646ddcd | |||
f7766fc182 | |||
3176370ef6 | |||
9bed1682fc | |||
daf2e2036e | |||
0f81c78639 | |||
8a19cfe0c6 | |||
a00fec9bca | |||
209f224517 | |||
0b7f2b7d4b | |||
eff15fc3d0 | |||
2614459772 | |||
4e926746cf | |||
f022f6d3ee | |||
1133ae4ae9 | |||
edd5afa13b | |||
442f572acc | |||
be58f3c429 | |||
3eea5d9322 | |||
e4e87163e8 | |||
d3aeb729e0 | |||
112d4ec9c0 | |||
2e7c7cf1da | |||
5d39416532 | |||
af95adb589 | |||
0fc4f96773 | |||
0a9adf33c8 | |||
f9110cedfa | |||
88ec55fc49 | |||
98b2a2a5c1 | |||
27eeafbf36 | |||
0cf63028df | |||
0b52b3cf58 | |||
e1764880a2 | |||
d3a47ffcdd | |||
9c1f88bb9c | |||
ae2f3184e2 | |||
3f1db47c30 | |||
3e3bbe298d | |||
46dc357651 | |||
07d25cb673 | |||
264f75ce8e | |||
9713a19405 | |||
a3836b970a | |||
ccfb8771f1 | |||
5e2b31cb6c | |||
b36801652f | |||
9e5b9cbdb5 | |||
bdf4ebd1bc | |||
e91e7f96c2 | |||
34fef4aaad | |||
09330458e5 | |||
ed95b99ed1 | |||
dc1e1e8dcb | |||
13a81c9222 | |||
6354464859 | |||
ec26404b94 | |||
3c45941474 | |||
91e172fd79 | |||
5ef2508736 | |||
93264fd2d0 | |||
7020c7aeab | |||
25b1673321 | |||
628bc711c2 | |||
a3b4228685 | |||
374c8e4a1a | |||
5afcf2798f | |||
1657cf0a7e | |||
c9820d0071 | |||
3e975dc4f0 | |||
b53c046eef | |||
fd10d7ed34 | |||
c5aae44249 | |||
83aa6127ec | |||
5a2299f758 | |||
57cdab0727 | |||
f82fa1b3b3 | |||
e95eef2071 | |||
53efdac0f0 | |||
f5eafc39c5 | |||
0f72ccf82e | |||
c191eb74fd | |||
f9fca42c5b | |||
11a19eef07 | |||
8a237af4ac | |||
24413e1edd | |||
5aba0c60b8 | |||
5599132efe | |||
7f9e27e3d3 | |||
7d36360111 | |||
d350297ce1 | |||
18d4e42d1f | |||
9faf5a3d5c | |||
da113612eb | |||
e9e2eade89 | |||
3cbc9c1b5c | |||
0772510e47 | |||
f389aa07eb | |||
27a110a93a | |||
13eaa4e9a1 | |||
7ec7d05fb0 | |||
7fe4089bb0 | |||
0cee453202 | |||
088d8097a9 | |||
4e6fae03ff | |||
732d0dfdca | |||
93e0232c21 | |||
37707c422a | |||
2f1bd9ca61 | |||
3d9ddbf9bc | |||
7c9140dcec | |||
a63d179a0d | |||
95dd8718bd | |||
ff2c9e98c0 | |||
d9ab38c590 | |||
23f4a350e7 | |||
696225d8d2 | |||
6c1ccc17b3 | |||
aa60f3a664 | |||
f01fb2830a | |||
9f6aa6b13e | |||
b2ee15a4ff | |||
42de0fbe73 | |||
553c986aa8 | |||
9a1e2260a0 | |||
98f7ce2585 | |||
c30ec8cb5f | |||
104c752f9a | |||
b66bea5671 | |||
f9313204a7 | |||
cb5c371a4f | |||
a32df58f46 | |||
e2658cc8dd | |||
1fbec20c6f | |||
ddff8be53c | |||
114d488345 | |||
c4da5a6ead | |||
056f5a4555 | |||
dfc88d99f6 | |||
033f41a7d5 | |||
5612a01039 | |||
f1d609cf40 | |||
0e9c71ae9f | |||
d1af399489 | |||
f445bac42f | |||
798f091ff2 | |||
8381944bb2 | |||
f9d0e0d971 | |||
29d50f850b | |||
81c69d92b3 | |||
5cd9f37fdf | |||
1cf65aca1b | |||
470c429bd9 | |||
c8d081e818 | |||
492c6a6f97 | |||
1dfd18e779 | |||
caed17152d | |||
825143f17c | |||
da144b4d02 | |||
f4c4545099 | |||
924a969307 | |||
072f6c737c | |||
5f683dd389 | |||
2526cbe6ca | |||
6959fc52ac | |||
81bd684305 | |||
68c8dad829 | |||
ca3f7bac6c | |||
a127d452bd | |||
7c77cc4ea4 | |||
9c0e32a790 | |||
611fae785a | |||
0ef4ac1cdc | |||
c04ea7e731 | |||
9bdecaf02f | |||
6b222bad01 | |||
079d68c042 | |||
4b800202fb | |||
12d0916625 | |||
e0976d6bd6 | |||
a31f364361 | |||
8efa17928c | |||
48bfdd500d | |||
4621122cfb | |||
62fb048cce | |||
d4d0fe60b3 | |||
0a6e8e009b | |||
9f319d7ce3 | |||
7b3bd54386 | |||
8d82e2d0fc | |||
ffd4655e2f | |||
8f119fbdd3 | |||
b22a179a17 | |||
1cbab58d29 | |||
28943f3b6f | |||
b1f4e17aaf | |||
afd0c6de08 | |||
cf114b0d3c | |||
f785d62315 | |||
7aeda9e245 | |||
8a5e655122 | |||
9b447a4ab0 | |||
f3e84dc6eb | |||
a18a86770e | |||
6300f86cc4 | |||
df662b1058 | |||
db019178b7 | |||
dcec2dfcb0 | |||
e6ad153e83 | |||
9d33e4756b | |||
c267aee20f | |||
381e40f9a3 | |||
1760b319d3 | |||
59737f89c1 | |||
17097965d9 | |||
1a54bf34ef | |||
7e8ba077ae | |||
6ca010e2ba | |||
e9eacc445d | |||
db12dafad2 | |||
75acda0d7d | |||
b98e276767 | |||
149c58fa3e | |||
62d79b82f8 | |||
7f7e63236b | |||
965a5cc113 | |||
5a4a36a06a | |||
dd0fdea19f | |||
af31549309 | |||
072e5013fc | |||
43f2cf8dc3 | |||
0aca308bbd | |||
ff567892f9 | |||
15fc12627a | |||
a743c12c1b | |||
2471418591 | |||
c77ebd4d0e | |||
ccaf9a9ffc | |||
381806d84b | |||
391e37d49e | |||
7088b3c9d8 | |||
ccf0877b81 | |||
9e9129dd02 | |||
0aa9390ece | |||
e47934a08a | |||
04b7383bbe | |||
930b1e8d0c | |||
82a026a426 | |||
92647341a9 | |||
776cecc3ef | |||
a4fb2378bb | |||
9742fdc770 | |||
786778fef6 | |||
3f946180dd | |||
b1b32a34c9 | |||
3d70333f9c | |||
a6cf7107b9 | |||
d590dd5db8 | |||
c64cf39b69 | |||
f4404f66b8 | |||
9a62496d5c | |||
e24c1f38e0 | |||
dffcee52d7 | |||
db28536ea8 | |||
3ca9b7d6bf | |||
37d2d580f4 | |||
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 | |||
53a7e0dac3 | |||
d36d7bfce6 | |||
1c4649cc9e | |||
77e537c35d | |||
935db128d3 | |||
49f26d591b | |||
3b37a03249 | |||
e7e2a869d2 | |||
94b18e6fc4 | |||
34edab25c8 | |||
3a858e7cd6 | |||
0008ff9a98 | |||
5cb6c663bb | |||
a90375ea3d | |||
9cf921bb1c | |||
5ec1a1eedf | |||
88a208f47e | |||
6cb036bb38 | |||
77bf857e91 | |||
b36559558d | |||
e88ed97528 | |||
ed73ded05f | |||
66fae1fa0a | |||
6e3ee3f4a6 | |||
2916c05101 | |||
1cc882462e | |||
30472505ce | |||
119600b64f | |||
18bbe09af4 | |||
fef1adb34f | |||
93d07c34ab | |||
3a0ce7c084 | |||
b944e8bb84 | |||
0b2e6a0d77 | |||
4cce75d128 | |||
b42c33107e | |||
686d3c409d | |||
51f4ab473b | |||
8fb473c57c | |||
9e8e538647 | |||
ebdcccb6ca | |||
809a551073 | |||
83e1900d89 | |||
9b2a31761a | |||
87c99df13d | |||
8adc53a8bc | |||
ba240f9e29 | |||
95bb9e2586 | |||
1487afb36b | |||
2bbcd3d9e6 | |||
fb1f93a3c0 | |||
1489604f82 | |||
273f4228d7 | |||
fdae5105f8 | |||
8e55d17a55 | |||
ded5c50157 | |||
46f2b7a3f8 | |||
3d818f953d | |||
3ac98ba326 | |||
c8b6eb0d6c | |||
fc41be9db8 | |||
164da9d8e0 | |||
767943368e | |||
e37f584d75 | |||
b2663fb3e0 | |||
bb2bcb8bd1 | |||
d103c81f67 | |||
b591d638d0 | |||
778631f396 | |||
2ec3143d27 | |||
d705440400 | |||
e6e3d82fa6 | |||
db48ab8f6c | |||
b868b6a5f3 | |||
dabc7e3eb1 | |||
38efb6a1e2 | |||
a6c8638345 | |||
fc8023b941 | |||
31111fc63b | |||
7fd06e36bc | |||
4899c4de5b | |||
744caf8c79 | |||
da888e27ad | |||
28369411f7 | |||
b7a1689aeb | |||
c034b62b71 | |||
a6b9400a4a | |||
aa5d761081 | |||
2cc8e59ca8 | |||
a031e84ab8 | |||
e2df6e94a5 | |||
9db69430b5 | |||
00feee6903 | |||
10dd747899 | |||
5c55e6e508 | |||
71fe15d56e | |||
0a71c993ed | |||
63adc181c8 | |||
76fc82811a | |||
1e859c19f4 | |||
175b4a3fb6 | |||
d89976802d | |||
dce5c8f621 | |||
d8ff36f59f | |||
c900022697 | |||
1090a61162 | |||
0e11ff198c | |||
cdbc6f5619 | |||
78cb18ad0e | |||
42374a775d | |||
0269b58a3c | |||
ef4a316558 | |||
a676ce7c21 | |||
f475daf7a6 | |||
c8110c31ef | |||
a5c8c9c279 | |||
5860136494 | |||
3f3516b7ba | |||
06e26da684 | |||
bb70ff96d2 | |||
c019d57fb6 | |||
7854a5eea2 | |||
29636173ef | |||
4edfc1e0be | |||
61d4da49aa | |||
56187ec43e | |||
971ac26033 | |||
1f316a0b65 | |||
23d09c37b5 | |||
fc7c3022be | |||
5b65adedbb | |||
6faf9bf4bf | |||
b5998d7f22 | |||
6abbc1c54d | |||
85e5319981 | |||
50da0a753a | |||
6a5f2d0566 | |||
d93277155f | |||
cb905e5ee6 | |||
71261e7594 | |||
27e4f490d3 | |||
298c8705d7 | |||
edc4382a48 | |||
5baab76f2e | |||
f9f30efe03 | |||
12701bdf98 | |||
70967ac7b0 | |||
98b443da82 | |||
10f75c8e55 | |||
b226642853 | |||
933f837f64 | |||
7327698305 | |||
1dc59d0d41 | |||
13e067dc4f | |||
885d348f96 | |||
c71ee77503 | |||
14206de926 | |||
c73d64d784 | |||
913067f014 | |||
ff2ee989d6 | |||
200cefc1b2 | |||
721af0f26d | |||
aca6269920 | |||
a4074c9687 | |||
205ec61549 | |||
0d16f707c2 | |||
d3d5ead6ed | |||
1f05d6ea4d | |||
ff82af8358 | |||
a7da858694 | |||
b5c2f6e551 | |||
77226e0924 | |||
0cc4286f5f | |||
99144143ff | |||
efff841121 | |||
2f8d914ecb | |||
7dd28a5941 | |||
a89fccdc1f | |||
40ddd3b2a5 | |||
74d17a20a4 | |||
d537bd2f58 | |||
2f045be8a4 | |||
c5ee4810ad | |||
1dbda5876f | |||
d948923d95 | |||
fb1085744a | |||
ec22291aca | |||
00a07fd27c | |||
ec0e77cc5a | |||
16c49a9626 | |||
06ea809d60 | |||
12364005c1 | |||
98573e9e05 | |||
c1a4ca6203 | |||
21c2fd8542 | |||
b27bc8fc1b | |||
091115e6ba | |||
d9c055872d | |||
f73d18ddc7 | |||
eb47126f68 | |||
4750767268 | |||
b0ed772885 | |||
7fdab81b5f | |||
c17bf13f8c | |||
0e17c9a6db | |||
1c4dd78dea | |||
23418b3a09 | |||
0f143adbde | |||
1f3f4b7900 | |||
2c5f26380e | |||
8f974fb087 | |||
a0722b4ca5 | |||
41e039578a | |||
515e010065 | |||
c89e8e8a96 | |||
cac83ab927 | |||
2c46e8a2dc | |||
0f0b894363 | |||
43f9af1bc6 | |||
f5ed14c84c | |||
2dd57d7c73 | |||
0b1891b64a | |||
5614b0f58a | |||
3bb178976d | |||
1777f98aef | |||
45e3706335 | |||
337ed1fc46 | |||
d1ea76e5a0 | |||
4a72d725b1 | |||
1693db3199 | |||
1ff42991b3 | |||
eebe3c164a | |||
978423ba5b | |||
4d0dc0d7b7 | |||
3817e666a9 | |||
b61350f6a4 | |||
0fb1a1dc6f | |||
9eefc87b7a | |||
53d35757ee | |||
e80e8e00b1 | |||
0b08e574c7 | |||
499323d0e3 | |||
89ad2f163a | |||
7f04617b7d | |||
44904628bc | |||
fafde7b1ad | |||
7e65314670 | |||
df52c56e83 | |||
4276fb54cc | |||
bb5a0db79c | |||
b906048ea1 | |||
7ce9c816c5 | |||
3fef6e4849 | |||
e7ce1e36e7 | |||
734c915206 | |||
783174adc6 | |||
d769db7668 | |||
00e532fce4 | |||
7cf8cba54b | |||
70b26811d9 | |||
e7aafecbc2 | |||
949fb052cd | |||
fcb1f5a51b | |||
e24f70b891 | |||
bd233ebe9b | |||
f92269f2ec | |||
2143db5eb5 | |||
0c72f50b5e | |||
3c4c616242 | |||
153baad49f | |||
75a2ab636c | |||
05a77e612c | |||
d02bc06dce | |||
e1f88f1a7b | |||
86a2647134 | |||
621b640af4 | |||
40c80f417c | |||
7bb2c1c278 | |||
a5278affe6 | |||
2f953192d6 | |||
af64582efd | |||
6ad70f24a2 | |||
8bf8968588 | |||
7e9ce0360a | |||
1d35c41dcb | |||
824315f773 | |||
8a74799d64 | |||
f0f6e8a988 | |||
89bc9a823c | |||
40250b7ecf | |||
2d6d32923d | |||
7cb6aee3f7 | |||
469d042f4b | |||
c38ccdb915 | |||
baaa92427f | |||
1ff2c61b3a | |||
0b356e0e83 | |||
eb55c053eb | |||
07b307e4b1 | |||
5bee6a5e24 | |||
bdc99e34cc | |||
cee10fb507 | |||
74e78bb967 | |||
ea5811c24c | |||
d31b7ae4af | |||
75eac1b972 | |||
c65ce14de3 | |||
f8c4ccd64c | |||
43ce222725 | |||
c7ebeecb6b | |||
243c6ca22e | |||
66f1c57a2a | |||
c0d1495761 | |||
e5f6ed3dc7 | |||
ab62d91b09 | |||
59beabb445 | |||
d5bc377e3d | |||
2bdb20f42f | |||
0062df58a2 | |||
b6bbfc08ad | |||
5baccc73c9 | |||
20e7eae4fe | |||
8432f71d58 | |||
604c22d64d | |||
c1deb08df8 | |||
66f201746f | |||
1c61ffbd36 | |||
e5ba8eb281 | |||
f542e07c33 | |||
1082d7f869 | |||
4a3adaa347 | |||
1659dab87d | |||
d88599714f | |||
71bf56a2b7 | |||
0fba78ad16 | |||
92560f5e1f | |||
0d484b93eb | |||
5f3b8c55b8 | |||
553416689c | |||
b0744fd21d | |||
be38844a5b | |||
54e2b661bc | |||
b81d8eba25 | |||
dbcd2b0988 | |||
1d11f11eaf | |||
f2d7401d1d | |||
91cb9750b4 | |||
3e0d4cb70a | |||
dab677b360 | |||
625c0785b5 | |||
540a8b4201 | |||
11f86da1f6 | |||
ab5ffa9ee6 | |||
65bec23292 | |||
635ae941d7 | |||
a9753fb784 | |||
b587d9b35a | |||
aa68bc05d9 | |||
66566a401f | |||
5aa75ecd3f | |||
0a77f9a0c8 | |||
b5d4cfed03 | |||
c57394bdab | |||
da857f321b | |||
754ea09400 | |||
f28a2ea151 | |||
c7dd028771 | |||
3c94bacda9 | |||
8e85847de3 | |||
0c10bbb569 | |||
b710944630 | |||
280f482fc8 | |||
e1ad8fbee8 | |||
56ca6039ba | |||
fba54ae0c6 | |||
e243c72ca6 | |||
23ea6fd4f9 | |||
3f9f2ef238 | |||
77cb20f5c7 | |||
ddf630c269 | |||
39adb9a0c2 | |||
97fde96b7b | |||
190391e493 | |||
d3fcb69c50 | |||
2db4a29ad7 | |||
4df82a6ff1 | |||
cdf73043e1 | |||
ca07d1c50e | |||
868011479b | |||
6f6df3e63a | |||
23c740d225 | |||
702d4de3b5 | |||
445fa35ab5 | |||
9868476965 | |||
bfa6b955ca | |||
13b1805d04 | |||
c233fd8ed1 | |||
90f5934440 | |||
30b2b85446 | |||
0adc3d2027 | |||
e53fd110f6 | |||
edf0294d51 | |||
8850b44115 | |||
17f9e53779 | |||
a61233d2ba | |||
2022988e77 | |||
409de81bd2 | |||
2b289ddf77 | |||
b066a55ead | |||
8dfc0138f5 | |||
517f508e44 | |||
2f1a671121 | |||
2fb4b261a8 | |||
9c3c745fdf | |||
6a75147719 | |||
295b363d8a | |||
d96b5943b9 | |||
17406e413d | |||
9b219f67b0 | |||
8fd2578a6d | |||
cc809a6bc0 | |||
66659c8fc8 | |||
31293bbe06 | |||
1c3488f8db | |||
20e536cec0 | |||
e8b498ca6d | |||
b82f8606a8 | |||
ab27fbccf7 | |||
d50de9366b | |||
4c56bca4e7 | |||
a60774a3f4 | |||
03426ee7f2 | |||
428022d1a2 | |||
b5bcd0a308 | |||
03c72ea00f | |||
a486390015 | |||
8dc47110a0 | |||
52a6fe64a7 | |||
081ef94399 | |||
eebde3ad12 | |||
669861d7a8 | |||
6ab6147ac8 | |||
dd7e8d254b | |||
2765f7e488 | |||
2d3a276dc2 | |||
55eddee6ce | |||
ab751d0db3 | |||
b2bd0ba340 | |||
224fa25fdf | |||
e6539a5566 | |||
6115a311ad | |||
a685ac3e73 | |||
9a22975732 | |||
cd0b2e3a26 | |||
80a3c196ae | |||
b0c541f8dc | |||
6188b04544 | |||
8ba4f964d4 | |||
0d2caddb12 | |||
4570c35658 | |||
72f7d81b80 | |||
231fa61805 | |||
9f74affd3a | |||
f58e1e1d6c | |||
074cf695b2 | |||
07c056523f | |||
65eb037020 | |||
c84add0a2a | |||
ace0e9c56f | |||
498705f330 | |||
7892624709 | |||
d8889beaf7 | |||
6e67304e92 | |||
8b23e89a64 | |||
7611b999fe | |||
aba8feb985 | |||
747cc1134c | |||
db05412865 | |||
679b1d9c23 | |||
a37cf91702 | |||
80d219c3e0 | |||
5ea5887146 | |||
13838861fb | |||
09c60322db | |||
68bf0b9efe | |||
3ec68daf2e | |||
9fafe02e16 | |||
56da34d343 | |||
086dd621b5 | |||
56a14925da | |||
c13cb23942 | |||
31df4a26fa | |||
9f9273bb02 | |||
86fd876850 | |||
b56d9287e4 | |||
a35e235744 | |||
77a44b4490 | |||
594f846943 | |||
8ae43cdcf6 | |||
1d72d310e5 | |||
b0ffac2f00 | |||
5ba851adff | |||
e72e6cf2b7 | |||
0ac40acc40 | |||
56710657bd | |||
92f4979715 | |||
1e9118df33 | |||
e16c0e53ff | |||
0d57a26925 | |||
1bd180596e | |||
fca003dfd7 | |||
f1ef23874c | |||
16883cf168 | |||
1781b71399 | |||
fb62fa4d32 | |||
ed148a542d | |||
a4f7843727 | |||
48cd84ce77 | |||
3859a7e09b | |||
76d0127029 | |||
a94cd8c85c | |||
ee555f3f15 | |||
bd230a8b7d | |||
a4926d8833 | |||
7560d2f673 | |||
d7acd7aef6 | |||
860b8efd7d | |||
6ca3fc5ad2 | |||
189af07ff5 | |||
caf7426f86 | |||
44b2bc1795 | |||
3ccc6e5d5c | |||
ccb579ecfd | |||
29f5e8aa78 | |||
d64357af61 | |||
37c91ae652 | |||
3a4cfa0834 | |||
cef45c2155 | |||
5143fc6eee | |||
186382619c | |||
91e70c5476 | |||
216ace9f61 | |||
6b99582a66 | |||
ea0fe1b92e | |||
72810acf2e | |||
a013768313 | |||
a660261678 | |||
7d181f334c | |||
46ab27af1a | |||
25bb966a32 | |||
c086579260 | |||
3d14bc9a00 | |||
75cd7a0f15 | |||
4722bb8fcd | |||
f2175b948c | |||
6f3d102ecb | |||
54fa39f347 | |||
52697dea97 | |||
c99b207e9e | |||
4886537a56 | |||
ca688764a3 | |||
71cf54c28b | |||
1878bed10a | |||
87fd5e33f1 | |||
ffda30bd65 | |||
716795532e | |||
f9ff99748b | |||
723fa153be | |||
1871d1a842 | |||
2c4e066f64 | |||
b371dad506 | |||
a6d4431940 | |||
871d80aad5 | |||
6711979445 | |||
cb080b356c | |||
9950c5e02d | |||
22a11be4e0 | |||
6e01c80282 | |||
4e14f84df9 | |||
55522404b4 | |||
4ef8c273f7 | |||
61c17ccc5e | |||
2832476c2b | |||
c0fc74b62a | |||
bb752863fa | |||
54caaffe3a | |||
cf5603c8e3 | |||
77b1011207 | |||
55f0a491cb | |||
a940fa210a | |||
5162ba9b91 | |||
3b6022de64 | |||
bf743f5f72 | |||
3e177539d5 | |||
5743dd3a8c | |||
9f8ad95a59 | |||
3c05a4cebd | |||
bc955a9afd | |||
ec8d86e662 | |||
bc70bedb78 | |||
7a4b77ce59 | |||
8600cee54c | |||
fe9573ea3c | |||
61db6c54c2 | |||
65093c73c5 | |||
9986521e41 | |||
655f015109 | |||
3cea59ce5d | |||
a184192452 | |||
2dbcab32d5 | |||
13aeeb4731 | |||
233a468127 | |||
8a9e05b08f | |||
3ef2ac8a77 | |||
fdac590a02 | |||
dd960aa5f0 | |||
0bd9a848c4 | |||
1b86c58f91 | |||
d5166d343d | |||
b315cf6022 | |||
37de32ec90 | |||
6eb81802c3 | |||
e6068a6f7f | |||
c059c088d1 | |||
b530847edc | |||
c87c2dadd7 | |||
7b1ff04436 | |||
83aa440b62 | |||
a555ef836b | |||
528601e442 | |||
13acb19e9f | |||
079063157f | |||
e38933c0b3 | |||
d09b406c4e | |||
a5eba8e722 | |||
7acb4cc22a | |||
b95ab6c6a1 | |||
038445e13e | |||
07e9dd5a39 | |||
6ec520d358 | |||
06bfd2429b | |||
099c4836e6 | |||
ddf8ceb45d | |||
8a49e0817a | |||
88908297f5 | |||
cf0e111c09 | |||
ae0ee727fa | |||
be2945c445 | |||
237a10da1e | |||
1baf14084d | |||
a6387e7552 | |||
a6f480d3f8 | |||
0413059fbe | |||
65f049f6ac | |||
62f886a3b3 | |||
271ca148e3 | |||
8aa294309f | |||
ca3233110b | |||
1e4f6a4b9d | |||
a73fc6de19 | |||
0bb750488b | |||
32f98f83c5 | |||
6943785ce5 | |||
86558a8221 | |||
f2c35a302d | |||
0794b6132a | |||
062c287e75 | |||
e67d68a7b9 | |||
054acc689a | |||
9b95d18b85 | |||
7f9bc77253 | |||
b92907aca6 | |||
c4ee03c73b | |||
89ba80740b | |||
606a5e5317 | |||
f859bf528e | |||
ad504fa84e | |||
e7ac74c5a0 | |||
b80504ae00 | |||
68f1887d66 | |||
5cadb9e2f9 | |||
201c8352e3 | |||
a0f0ffe566 | |||
4b4e8e2bfc | |||
4db4c172c1 | |||
08c54a910f | |||
00fee63ff3 | |||
6b80cd6590 | |||
840efbdc2f | |||
b91dc9e43e | |||
7470cd7af5 | |||
d3a6977938 | |||
7cc341ea40 | |||
5297133c07 | |||
7a6230f2f8 | |||
ffe66a3b8e | |||
936cd51f29 | |||
0c24671d8b | |||
6969593b38 | |||
0c351c0925 | |||
656c408034 | |||
74fb64bbb9 | |||
3af85f9fba | |||
3c282460b2 | |||
68b7e6e5ab | |||
9594157f3e | |||
b6ed6ad61e | |||
3fc68ffc50 | |||
0613e1115d | |||
6567c3bddf | |||
b7115d8862 | |||
83899bebc8 | |||
06803519e6 | |||
3a6b2084bc | |||
2235069e78 | |||
15698c5036 | |||
6ac8e057b0 | |||
375412b45d | |||
e47530dc71 | |||
93150199a4 | |||
900f69f336 | |||
c556820646 | |||
18fbe82535 | |||
7ae73d1b62 | |||
cf7834bfc3 | |||
9f82e2d836 | |||
f20af1f5f8 | |||
8343f8ea0d | |||
74c0dcd1f5 | |||
40696e4095 | |||
614a2558f5 | |||
56aec216c1 | |||
b359fb5f3b | |||
1fbbbab602 | |||
89697df85e | |||
37ee8148c6 | |||
9e55102816 | |||
b8fa5e8a89 | |||
3ba636f300 | |||
da3742f600 | |||
35f4d27ab0 | |||
cf123d1887 | |||
b3816bd828 | |||
7c7c9dea40 | |||
eabe406ab0 | |||
2ae617cda6 | |||
1b16066335 | |||
da251d3d2d | |||
818efe61f4 | |||
9f08b04c92 | |||
41d17c930a | |||
63f22c554a | |||
cba57cf317 | |||
9a28e5b4bc | |||
a2689002d3 | |||
e7a9b83877 | |||
813db9dbbc | |||
72d52c9941 | |||
4c2b9d4703 | |||
b1f7505f30 | |||
63e9d83ba4 | |||
1534a47adc | |||
c563548a1c | |||
a633a3534d | |||
992357cbc4 | |||
ffc3562709 | |||
f19db530b1 | |||
061a9c8583 | |||
b8fbc36b2d | |||
e364faaffd | |||
b3246778f2 | |||
74b76eda7e | |||
564367d5fd | |||
fd2966610c | |||
c23b291f25 | |||
67365e5480 | |||
4df205dea6 | |||
32928bf45c | |||
ea98f9be3c | |||
5085376f11 | |||
e2b4adb2e9 | |||
315810bd74 | |||
7e9ba3b6e2 | |||
08dd5174b3 | |||
e552be0a81 | |||
b63360813a | |||
5d8c4ad03f | |||
3e6206951e | |||
9d4ea2dcda | |||
61f767e895 | |||
efd5016977 | |||
0f043605d9 | |||
9ff0b7bc18 | |||
53502e22f4 | |||
d683e385ae | |||
4880cd84dc | |||
da5800c268 | |||
21439761c3 | |||
bef857a7dc | |||
59ab4bf7f9 | |||
d4bc92bd5b | |||
7efdbeb787 | |||
43daff29dc |
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@ -0,0 +1,10 @@
|
||||
backend/node_modules
|
||||
frontend/node_modules
|
||||
backend/frontend-build
|
||||
**/node_modules
|
||||
**/.next
|
||||
.dockerignore
|
||||
.git
|
||||
README.md
|
||||
.dockerignore
|
||||
**/Dockerfile
|
55
.env.example
55
.env.example
@ -1,23 +1,12 @@
|
||||
# Keys
|
||||
# Required key for platform encryption/decryption ops
|
||||
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||
|
||||
# JWT
|
||||
# Required secrets to sign JWT tokens
|
||||
JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
|
||||
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
|
||||
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
|
||||
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
|
||||
|
||||
# JWT lifetime
|
||||
# Optional lifetimes for JWT tokens expressed in seconds or a string
|
||||
# describing a time span (e.g. 60, "2 days", "10h", "7d")
|
||||
JWT_AUTH_LIFETIME=
|
||||
JWT_REFRESH_LIFETIME=
|
||||
JWT_SIGNUP_LIFETIME=
|
||||
|
||||
# Optional lifetimes for OTP expressed in seconds
|
||||
EMAIL_TOKEN_LIFETIME=
|
||||
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
|
||||
|
||||
# MongoDB
|
||||
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
|
||||
@ -25,6 +14,9 @@ EMAIL_TOKEN_LIFETIME=
|
||||
# Required
|
||||
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
# Optional credentials for MongoDB container instance and Mongo-Express
|
||||
MONGO_USERNAME=root
|
||||
MONGO_PASSWORD=example
|
||||
@ -33,14 +25,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
|
||||
@ -48,10 +38,14 @@ CLIENT_ID_HEROKU=
|
||||
CLIENT_ID_VERCEL=
|
||||
CLIENT_ID_NETLIFY=
|
||||
CLIENT_ID_GITHUB=
|
||||
CLIENT_ID_GITLAB=
|
||||
CLIENT_ID_BITBUCKET=
|
||||
CLIENT_SECRET_HEROKU=
|
||||
CLIENT_SECRET_VERCEL=
|
||||
CLIENT_SECRET_NETLIFY=
|
||||
CLIENT_SECRET_GITHUB=
|
||||
CLIENT_SECRET_GITLAB=
|
||||
CLIENT_SECRET_BITBUCKET=
|
||||
CLIENT_SLUG_VERCEL=
|
||||
|
||||
# Sentry (optional) for monitoring errors
|
||||
@ -61,10 +55,13 @@ SENTRY_DSN=
|
||||
# Ignore - Not applicable for self-hosted version
|
||||
POSTHOG_HOST=
|
||||
POSTHOG_PROJECT_API_KEY=
|
||||
STRIPE_SECRET_KEY=
|
||||
STRIPE_PUBLISHABLE_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_PRODUCT_CARD_AUTH=
|
||||
STRIPE_PRODUCT_PRO=
|
||||
STRIPE_PRODUCT_STARTER=
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
||||
|
||||
# SSO-specific variables
|
||||
CLIENT_ID_GOOGLE_LOGIN=
|
||||
CLIENT_SECRET_GOOGLE_LOGIN=
|
||||
|
||||
CLIENT_ID_GITHUB_LOGIN=
|
||||
CLIENT_SECRET_GITHUB_LOGIN=
|
||||
|
||||
CLIENT_ID_GITLAB_LOGIN=
|
||||
CLIENT_SECRET_GITLAB_LOGIN=
|
||||
|
@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
built
|
||||
healthcheck.js
|
||||
tailwind.config.js
|
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -8,7 +8,7 @@ assignees: ''
|
||||
---
|
||||
|
||||
### Feature description
|
||||
A clear and concise description of what the the feature should be.
|
||||
A clear and concise description of what the feature should be.
|
||||
|
||||
### Why would it be useful?
|
||||
Why would this feature be useful for Infisical users?
|
||||
|
BIN
.github/images/Deploy to AWS.png
vendored
Normal file
BIN
.github/images/Deploy to AWS.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
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/deploy-to-aws.png
vendored
Normal file
BIN
.github/images/deploy-to-aws.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 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 |
22
.github/pull_request_template.md
vendored
Normal file
22
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
# 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. -->
|
||||
|
||||
## Type ✨
|
||||
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Breaking change
|
||||
- [ ] Documentation
|
||||
|
||||
# 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 -->
|
||||
|
||||
```sh
|
||||
# Here's some code block to paste some code snippets
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
- [ ] I have read the [contributing guide](https://infisical.com/docs/contributing/overview), agreed and acknowledged the [code of conduct](https://infisical.com/docs/contributing/code-of-conduct). 📝
|
3
.github/resources/docker-compose.be-test.yml
vendored
3
.github/resources/docker-compose.be-test.yml
vendored
@ -6,13 +6,14 @@ services:
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- mongo
|
||||
image: infisical/backend:test
|
||||
image: infisical/infisical:test
|
||||
command: npm run start
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGO_URL=mongodb://test:example@mongo:27017/?authSource=admin
|
||||
- MONGO_USERNAME=test
|
||||
- MONGO_PASSWORD=example
|
||||
- ENCRYPTION_KEY=a984ecdf82ec779e55dbcc21303a900f
|
||||
networks:
|
||||
- infisical-test
|
||||
|
||||
|
67
.github/values.yaml
vendored
67
.github/values.yaml
vendored
@ -1,36 +1,49 @@
|
||||
frontend:
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository:
|
||||
pullPolicy: Always
|
||||
tag: "latest"
|
||||
kubeSecretRef: managed-secret-frontend
|
||||
|
||||
backend:
|
||||
replicaCount: 1
|
||||
enabled: true
|
||||
name: backend
|
||||
podAnnotations: {}
|
||||
deploymentAnnotations:
|
||||
secrets.infisical.com/auto-reload: "true"
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository:
|
||||
pullPolicy: Always
|
||||
repository: infisical/staging_infisical
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
kubeSecretRef: managed-backend-secret
|
||||
service:
|
||||
annotations: {}
|
||||
type: ClusterIP
|
||||
nodePort: ""
|
||||
resources:
|
||||
limits:
|
||||
memory: 300Mi
|
||||
|
||||
backendEnvironmentVariables: null
|
||||
|
||||
## Mongo DB persistence
|
||||
mongodb:
|
||||
enabled: false
|
||||
persistence:
|
||||
enabled: false
|
||||
|
||||
## By default the backend will be connected to a Mongo instance within the cluster
|
||||
## However, it is recommended to add a managed document DB connection string for production-use (DBaaS)
|
||||
## Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/
|
||||
## e.g. "mongodb://<user>:<pass>@<host>:<port>/<database-name>"
|
||||
mongodbConnection:
|
||||
externalMongoDBConnectionString: ""
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
hostName: gamma.infisical.com
|
||||
frontend:
|
||||
path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
path: /api
|
||||
pathType: Prefix
|
||||
# annotations:
|
||||
# kubernetes.io/ingress.class: "nginx"
|
||||
# cert-manager.io/issuer: letsencrypt-nginx
|
||||
hostName: gamma.infisical.com ## <- Replace with your own domain
|
||||
tls:
|
||||
- secretName: echo-tls
|
||||
hosts:
|
||||
- gamma.infisical.com
|
||||
[]
|
||||
# - secretName: letsencrypt-nginx
|
||||
# hosts:
|
||||
# - infisical.local
|
||||
|
||||
backendEnvironmentVariables:
|
||||
|
||||
frontendEnvironmentVariables:
|
||||
mailhog:
|
||||
enabled: false
|
||||
|
121
.github/workflows/build-docker-image-to-prod.yml
vendored
Normal file
121
.github/workflows/build-docker-image-to-prod.yml
vendored
Normal file
@ -0,0 +1,121 @@
|
||||
name: Release production images (frontend, backend)
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "infisical/v*.*.*"
|
||||
- "!infisical/v*.*.*-postgres"
|
||||
|
||||
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
|
||||
- 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 }}
|
||||
load: true
|
||||
context: backend
|
||||
tags: infisical/infisical:test
|
||||
- name: ⏻ Spawn backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||
- name: 🧪 Test backend image
|
||||
run: |
|
||||
./.github/resources/healthcheck.sh infisical-backend-test
|
||||
- name: ⏻ Shut down backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||
- name: 🏗️ Build backend and push
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
push: true
|
||||
context: backend
|
||||
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
|
||||
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 frontend and export to Docker
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
load: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
project: 64mmf0n610
|
||||
context: frontend
|
||||
tags: infisical/frontend:test
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||
- name: ⏻ Spawn frontend container
|
||||
run: |
|
||||
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
|
||||
- name: 🧪 Test frontend image
|
||||
run: |
|
||||
./.github/resources/healthcheck.sh infisical-frontend-test
|
||||
- name: ⏻ Shut down frontend container
|
||||
run: |
|
||||
docker stop infisical-frontend-test
|
||||
- name: 🏗️ Build frontend and push
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
push: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
context: frontend
|
||||
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 }}
|
||||
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
99
.github/workflows/build-staging-img.yml
vendored
Normal file
99
.github/workflows/build-staging-img.yml
vendored
Normal file
@ -0,0 +1,99 @@
|
||||
name: Build, Publish and Deploy to Gamma
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
infisical-image:
|
||||
name: Build backend image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: 📦 Install dependencies to test all dependencies
|
||||
run: npm ci --only-production
|
||||
working-directory: backend
|
||||
# - name: 🧪 Run tests
|
||||
# run: npm run test:ci
|
||||
# working-directory: backend
|
||||
- name: Save commit hashes for tag
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
- 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 }}
|
||||
load: true
|
||||
context: .
|
||||
file: Dockerfile.standalone-infisical
|
||||
tags: infisical/infisical:test
|
||||
- name: ⏻ Spawn backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||
- name: 🧪 Test backend image
|
||||
run: |
|
||||
./.github/resources/healthcheck.sh infisical-backend-test
|
||||
- name: ⏻ Shut down backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||
- name: 🏗️ Build backend and push
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
push: true
|
||||
context: .
|
||||
file: Dockerfile.standalone-infisical
|
||||
tags: |
|
||||
infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||
infisical/staging_infisical:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||
|
||||
gamma-deployment:
|
||||
name: Deploy to gamma
|
||||
runs-on: ubuntu-latest
|
||||
needs: [infisical-image]
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.10.0
|
||||
- name: Install infisical helm chart
|
||||
run: |
|
||||
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
|
||||
helm repo update
|
||||
- name: Install kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
- name: Install doctl
|
||||
uses: digitalocean/action-doctl@v2
|
||||
with:
|
||||
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
|
||||
run: kubectl config set-context --current --namespace=gamma
|
||||
- name: test kubectl
|
||||
run: kubectl get ingress
|
||||
- name: Download helm values to file and upgrade gamma deploy
|
||||
run: |
|
||||
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
|
||||
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --wait --install
|
||||
if [[ $(helm status infisical) == *"FAILED"* ]]; then
|
||||
echo "Helm upgrade failed"
|
||||
exit 1
|
||||
else
|
||||
echo "Helm upgrade was successful"
|
||||
fi
|
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
|
||||
|
132
.github/workflows/docker-image.yml
vendored
132
.github/workflows/docker-image.yml
vendored
@ -1,132 +0,0 @@
|
||||
name: Build, Publish and Deploy to Gamma
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
backend-image:
|
||||
name: Build backend image
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- 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 }}
|
||||
load: true
|
||||
context: backend
|
||||
tags: infisical/backend:test
|
||||
- name: ⏻ Spawn backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||
- name: 🧪 Test backend image
|
||||
run: |
|
||||
./.github/resources/healthcheck.sh infisical-backend-test
|
||||
- name: ⏻ Shut down backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||
- name: 🏗️ Build backend and push
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
push: true
|
||||
context: backend
|
||||
tags: infisical/backend:${{ steps.commit.outputs.short }},
|
||||
infisical/backend:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
frontend-image:
|
||||
name: Build frontend image
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- 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 frontend and export to Docker
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
load: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
project: 64mmf0n610
|
||||
context: frontend
|
||||
tags: infisical/frontend:test
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
- name: ⏻ Spawn frontend container
|
||||
run: |
|
||||
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
|
||||
- name: 🧪 Test frontend image
|
||||
run: |
|
||||
./.github/resources/healthcheck.sh infisical-frontend-test
|
||||
- name: ⏻ Shut down frontend container
|
||||
run: |
|
||||
docker stop infisical-frontend-test
|
||||
- name: 🏗️ Build frontend and push
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
push: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
context: frontend
|
||||
tags: infisical/frontend:${{ steps.commit.outputs.short }},
|
||||
infisical/frontend:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
gamma-deployment:
|
||||
name: Deploy to gamma
|
||||
runs-on: ubuntu-latest
|
||||
needs: [frontend-image, backend-image]
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.10.0
|
||||
- name: Install infisical helm chart
|
||||
run: |
|
||||
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
|
||||
helm repo update
|
||||
- name: Install kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
- name: Install doctl
|
||||
uses: digitalocean/action-doctl@v2
|
||||
with:
|
||||
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
|
||||
run: kubectl config set-context --current --namespace=gamma
|
||||
- name: test kubectl
|
||||
run: kubectl get ingress
|
||||
- name: Download helm values to file and upgrade gamma deploy
|
||||
run: |
|
||||
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
|
||||
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --recreate-pods
|
57
.github/workflows/release-standalone-docker-img-postgres-offical.yml
vendored
Normal file
57
.github/workflows/release-standalone-docker-img-postgres-offical.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
name: Release standalone docker image
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "infisical/v*.*.*-postgres"
|
||||
|
||||
jobs:
|
||||
infisical-standalone:
|
||||
name: Build infisical standalone image postgres
|
||||
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
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 📦 Install dependencies to test all dependencies
|
||||
run: npm ci --only-production
|
||||
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-postgres
|
||||
infisical/infisical:${{ steps.commit.outputs.short }}
|
||||
infisical/infisical:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: Dockerfile.standalone-infisical
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
79
.github/workflows/release-standalone-docker-img.yml
vendored
Normal file
79
.github/workflows/release-standalone-docker-img.yml
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
name: Release standalone docker image
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "infisical/v*.*.*"
|
||||
- "!infisical/v*.*.*-postgres"
|
||||
|
||||
jobs:
|
||||
infisical-standalone:
|
||||
name: Build infisical standalone 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
|
||||
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 }}
|
||||
infisical/infisical:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: Dockerfile.standalone-infisical
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
21
.github/workflows/release_build.yml
vendored
21
.github/workflows/release_build.yml
vendored
@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
# run only against tags
|
||||
tags:
|
||||
- 'v*'
|
||||
- "infisical-cli/v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@ -18,10 +18,16 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- run: git fetch --force --tags
|
||||
- run: echo "Ref name ${{github.ref_name}}"
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '>=1.19.3'
|
||||
go-version: ">=1.19.3"
|
||||
cache: true
|
||||
cache-dependency-path: cli/go.sum
|
||||
- name: libssl1.1 => libssl1.0-dev for OSXCross
|
||||
@ -33,19 +39,20 @@ jobs:
|
||||
run: |
|
||||
mkdir ../../osxcross
|
||||
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
|
||||
- uses: goreleaser/goreleaser-action@v2
|
||||
- uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
distribution: goreleaser
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
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
|
||||
- name: Publish to CloudSmith
|
||||
run: sh cli/upload_to_cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
|
||||
|
@ -1,10 +1,16 @@
|
||||
name: Release Docker image for K8 operator
|
||||
on: [workflow_dispatch]
|
||||
name: Release Docker image for K8 operator
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "infisical-k8-operator/v*.*.*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: extract_version
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical-k8-operator/}"
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: 🔧 Set up QEMU
|
||||
@ -26,4 +32,6 @@ jobs:
|
||||
context: k8-operator
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: infisical/kubernetes-operator:latest
|
||||
tags: |
|
||||
infisical/kubernetes-operator:latest
|
||||
infisical/kubernetes-operator:${{ steps.extract_version.outputs.version }}
|
||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,7 +1,9 @@
|
||||
# backend
|
||||
node_modules
|
||||
.env
|
||||
.env.test
|
||||
.env.dev
|
||||
.env.gamma
|
||||
.env.prod
|
||||
.env.infisical
|
||||
|
||||
@ -32,7 +34,7 @@ reports
|
||||
junit.xml
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
@ -56,3 +58,8 @@ yarn-error.log*
|
||||
|
||||
# Infisical init
|
||||
.infisical.json
|
||||
|
||||
# Editor specific
|
||||
.vscode/*
|
||||
|
||||
frontend-build
|
||||
|
147
.goreleaser.yaml
147
.goreleaser.yaml
@ -11,9 +11,18 @@ 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 }}
|
||||
- -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
|
||||
flags:
|
||||
- -trimpath
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=/home/runner/work/osxcross/target/bin/o64-clang
|
||||
@ -24,10 +33,16 @@ builds:
|
||||
- goos: darwin
|
||||
goarch: "386"
|
||||
dir: ./cli
|
||||
|
||||
- id: all-other-builds
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
binary: infisical
|
||||
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:
|
||||
- freebsd
|
||||
- linux
|
||||
@ -54,25 +69,20 @@ archives:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- README*
|
||||
- LICENSE*
|
||||
- manpages/*
|
||||
- completions/*
|
||||
- ../README*
|
||||
- ../LICENSE*
|
||||
- ../manpages/*
|
||||
- ../completions/*
|
||||
|
||||
release:
|
||||
replace_existing_draft: true
|
||||
mode: 'replace'
|
||||
mode: "replace"
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
name_template: "checksums.txt"
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
name_template: "{{ .Version }}-devel"
|
||||
|
||||
# publishers:
|
||||
# - name: fury.io
|
||||
@ -80,6 +90,7 @@ changelog:
|
||||
# - infisical
|
||||
# dir: "{{ dir .ArtifactPath }}"
|
||||
# cmd: curl -F package=@{{ .ArtifactName }} https://{{ .Env.FURY_TOKEN }}@push.fury.io/infisical/
|
||||
|
||||
brews:
|
||||
- name: infisical
|
||||
tap:
|
||||
@ -91,31 +102,55 @@ brews:
|
||||
folder: Formula
|
||||
homepage: "https://infisical.com"
|
||||
description: "The official Infisical CLI"
|
||||
install: |-
|
||||
bin.install "infisical"
|
||||
bash_completion.install "completions/infisical.bash" => "infisical"
|
||||
zsh_completion.install "completions/infisical.zsh" => "_infisical"
|
||||
fish_completion.install "completions/infisical.fish"
|
||||
man1.install "manpages/infisical.1.gz"
|
||||
- name: "infisical@{{.Version}}"
|
||||
tap:
|
||||
owner: Infisical
|
||||
name: homebrew-get-cli
|
||||
commit_author:
|
||||
name: "Infisical"
|
||||
email: ai@infisical.com
|
||||
folder: Formula
|
||||
homepage: "https://infisical.com"
|
||||
description: "The official Infisical CLI"
|
||||
install: |-
|
||||
bin.install "infisical"
|
||||
bash_completion.install "completions/infisical.bash" => "infisical"
|
||||
zsh_completion.install "completions/infisical.zsh" => "_infisical"
|
||||
fish_completion.install "completions/infisical.fish"
|
||||
man1.install "manpages/infisical.1.gz"
|
||||
|
||||
nfpms:
|
||||
- id: infisical
|
||||
package_name: infisical
|
||||
builds:
|
||||
- all-other-builds
|
||||
vendor: Infisical, Inc
|
||||
homepage: https://infisical.com/
|
||||
maintainer: Infisical, Inc
|
||||
description: The offical Infisical CLI
|
||||
license: MIT
|
||||
formats:
|
||||
- rpm
|
||||
- deb
|
||||
- apk
|
||||
- archlinux
|
||||
bindir: /usr/bin
|
||||
contents:
|
||||
- src: ./completions/infisical.bash
|
||||
dst: /etc/bash_completion.d/infisical
|
||||
- src: ./completions/infisical.fish
|
||||
dst: /usr/share/fish/vendor_completions.d/infisical.fish
|
||||
- src: ./completions/infisical.zsh
|
||||
dst: /usr/share/zsh/site-functions/_infisical
|
||||
- src: ./manpages/infisical.1.gz
|
||||
dst: /usr/share/man/man1/infisical.1.gz
|
||||
- id: infisical
|
||||
package_name: infisical
|
||||
builds:
|
||||
- all-other-builds
|
||||
vendor: Infisical, Inc
|
||||
homepage: https://infisical.com/
|
||||
maintainer: Infisical, Inc
|
||||
description: The offical Infisical CLI
|
||||
license: MIT
|
||||
formats:
|
||||
- rpm
|
||||
- deb
|
||||
- apk
|
||||
- archlinux
|
||||
bindir: /usr/bin
|
||||
contents:
|
||||
- src: ./completions/infisical.bash
|
||||
dst: /etc/bash_completion.d/infisical
|
||||
- src: ./completions/infisical.fish
|
||||
dst: /usr/share/fish/vendor_completions.d/infisical.fish
|
||||
- src: ./completions/infisical.zsh
|
||||
dst: /usr/share/zsh/site-functions/_infisical
|
||||
- src: ./manpages/infisical.1.gz
|
||||
dst: /usr/share/man/man1/infisical.1.gz
|
||||
|
||||
scoop:
|
||||
bucket:
|
||||
owner: Infisical
|
||||
@ -126,16 +161,16 @@ scoop:
|
||||
homepage: "https://infisical.com"
|
||||
description: "The official Infisical CLI"
|
||||
license: MIT
|
||||
|
||||
aurs:
|
||||
-
|
||||
name: infisical-bin
|
||||
- name: infisical-bin
|
||||
homepage: "https://infisical.com"
|
||||
description: "The official Infisical CLI"
|
||||
maintainers:
|
||||
- Infisical, Inc <support@infisical.com>
|
||||
license: MIT
|
||||
private_key: '{{ .Env.AUR_KEY }}'
|
||||
git_url: 'ssh://aur@aur.archlinux.org/infisical-bin.git'
|
||||
private_key: "{{ .Env.AUR_KEY }}"
|
||||
git_url: "ssh://aur@aur.archlinux.org/infisical-bin.git"
|
||||
package: |-
|
||||
# bin
|
||||
install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical"
|
||||
@ -146,23 +181,19 @@ 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"
|
||||
# dockers:
|
||||
# - dockerfile: goreleaser.dockerfile
|
||||
# goos: linux
|
||||
# goarch: amd64
|
||||
# ids:
|
||||
# - infisical
|
||||
# image_templates:
|
||||
# - "infisical/cli:{{ .Version }}"
|
||||
# - "infisical/cli:{{ .Major }}.{{ .Minor }}"
|
||||
# - "infisical/cli:{{ .Major }}"
|
||||
# - "infisical/cli:latest"
|
||||
# build_flag_templates:
|
||||
# - "--label=org.label-schema.schema-version=1.0"
|
||||
# - "--label=org.label-schema.version={{.Version}}"
|
||||
# - "--label=org.label-schema.name={{.ProjectName}}"
|
||||
# - "--platform=linux/amd64"
|
||||
|
||||
dockers:
|
||||
- dockerfile: docker/alpine
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
ids:
|
||||
- all-other-builds
|
||||
image_templates:
|
||||
- "infisical/cli:{{ .Version }}"
|
||||
- "infisical/cli:{{ .Major }}.{{ .Minor }}"
|
||||
- "infisical/cli:{{ .Major }}"
|
||||
- "infisical/cli:latest"
|
||||
|
@ -1,5 +1,6 @@
|
||||
|
||||
#!/usr/bin/env sh
|
||||
. "$(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
|
130
Dockerfile.standalone-infisical
Normal file
130
Dockerfile.standalone-infisical
Normal file
@ -0,0 +1,130 @@
|
||||
ARG POSTHOG_HOST=https://app.posthog.com
|
||||
ARG POSTHOG_API_KEY=posthog-api-key
|
||||
ARG INTERCOM_ID=intercom-id
|
||||
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
FROM base AS frontend-dependencies
|
||||
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
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 base 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
|
||||
ARG INFISICAL_PLATFORM_VERSION
|
||||
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||
|
||||
# Build
|
||||
RUN npm run build
|
||||
|
||||
# Production image
|
||||
FROM base AS frontend-runner
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 non-root-user
|
||||
|
||||
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
|
||||
VOLUME /app/.next/cache/images
|
||||
|
||||
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
|
||||
COPY --from=frontend-builder /app/public ./public
|
||||
RUN chown non-root-user:nodejs ./public/data
|
||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
|
||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER non-root-user
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
##
|
||||
## BACKEND
|
||||
##
|
||||
FROM base AS backend-build
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 non-root-user
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY backend/package*.json ./
|
||||
RUN npm ci --only-production
|
||||
|
||||
COPY /backend .
|
||||
COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh
|
||||
RUN npm i -D tsconfig-paths
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM base AS backend-runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY backend/package*.json ./
|
||||
RUN npm ci --only-production
|
||||
|
||||
COPY --from=backend-build /app .
|
||||
|
||||
RUN mkdir frontend-build
|
||||
|
||||
# Production stage
|
||||
FROM base AS production
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 non-root-user
|
||||
|
||||
## set pre baked keys
|
||||
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=intercom-id
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
||||
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY --from=backend-runner /app /backend
|
||||
COPY --from=backend-runner /app/dist/services/smtp/templates /backend/dist/templates
|
||||
|
||||
COPY --from=frontend-runner /app ./backend/frontend-build
|
||||
|
||||
|
||||
ENV PORT 8080
|
||||
ENV HOST=0.0.0.0
|
||||
ENV HTTPS_ENABLED false
|
||||
ENV NODE_ENV production
|
||||
ENV STANDALONE_BUILD true
|
||||
ENV STANDALONE_MODE true
|
||||
WORKDIR /backend
|
||||
|
||||
ENV TELEMETRY_ENABLED true
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||
CMD node healthcheck.js
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 443
|
||||
|
||||
USER non-root-user
|
||||
|
||||
CMD ["./standalone-entrypoint.sh"]
|
5
Makefile
5
Makefile
@ -7,8 +7,11 @@ push:
|
||||
up-dev:
|
||||
docker-compose -f docker-compose.dev.yml up --build
|
||||
|
||||
up-pg-dev:
|
||||
docker compose -f docker-compose.pg.yml up --build
|
||||
|
||||
i-dev:
|
||||
infisical export && infisical export > .env && docker-compose -f docker-compose.dev.yml up --build
|
||||
infisical run -- docker-compose -f docker-compose.dev.yml up --build
|
||||
|
||||
up-prod:
|
||||
docker-compose -f docker-compose.yml up --build
|
||||
|
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.
|
11
backend-mongo/.dockerignore
Normal file
11
backend-mongo/.dockerignore
Normal file
@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
.env
|
||||
.env.*
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.*
|
||||
.DS_Store
|
||||
*.swp
|
||||
*~
|
2
backend-mongo/.eslintignore
Normal file
2
backend-mongo/.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
built
|
41
backend-mongo/.eslintrc
Normal file
41
backend-mongo/.eslintrc
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"unused-imports"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"no-empty-function": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"no-console": 2,
|
||||
"quotes": [
|
||||
"error",
|
||||
"double",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"only-multiline"
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"@typescript-eslint/no-extra-semi": "off", // added to be able to push
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"sort-imports": 1
|
||||
}
|
||||
}
|
7
backend-mongo/.prettierrc
Normal file
7
backend-mongo/.prettierrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": false,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true
|
||||
}
|
33
backend-mongo/Dockerfile
Normal file
33
backend-mongo/Dockerfile
Normal file
@ -0,0 +1,33 @@
|
||||
# Build stage
|
||||
FROM node:16-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only-production
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:16-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV npm_config_cache /home/node/.npm
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only-production && npm cache clean --force
|
||||
|
||||
COPY --from=build /app .
|
||||
|
||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
||||
&& apk add infisical=0.8.1 && apk add --no-cache git
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||
CMD node healthcheck.js
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
CMD ["node", "build/index.js"]
|
46
backend-mongo/environment.d.ts
vendored
Normal file
46
backend-mongo/environment.d.ts
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
PORT: string;
|
||||
ENCRYPTION_KEY: string;
|
||||
SALT_ROUNDS: string;
|
||||
JWT_AUTH_LIFETIME: string;
|
||||
JWT_AUTH_SECRET: string;
|
||||
JWT_REFRESH_LIFETIME: string;
|
||||
JWT_REFRESH_SECRET: string;
|
||||
JWT_SERVICE_SECRET: string;
|
||||
JWT_SIGNUP_LIFETIME: string;
|
||||
JWT_SIGNUP_SECRET: string;
|
||||
MONGO_URL: string;
|
||||
NODE_ENV: "development" | "staging" | "testing" | "production";
|
||||
VERBOSE_ERROR_OUTPUT: string;
|
||||
LOKI_HOST: string;
|
||||
CLIENT_ID_HEROKU: string;
|
||||
CLIENT_ID_VERCEL: string;
|
||||
CLIENT_ID_NETLIFY: string;
|
||||
CLIENT_ID_GITHUB: string;
|
||||
CLIENT_ID_GITLAB: string;
|
||||
CLIENT_SECRET_HEROKU: string;
|
||||
CLIENT_SECRET_VERCEL: string;
|
||||
CLIENT_SECRET_NETLIFY: string;
|
||||
CLIENT_SECRET_GITHUB: string;
|
||||
CLIENT_SECRET_GITLAB: string;
|
||||
CLIENT_SLUG_VERCEL: string;
|
||||
POSTHOG_HOST: string;
|
||||
POSTHOG_PROJECT_API_KEY: string;
|
||||
SENTRY_DSN: string;
|
||||
SITE_URL: string;
|
||||
SMTP_HOST: string;
|
||||
SMTP_SECURE: string;
|
||||
SMTP_PORT: string;
|
||||
SMTP_USERNAME: string;
|
||||
SMTP_PASSWORD: string;
|
||||
SMTP_FROM_ADDRESS: string;
|
||||
SMTP_FROM_NAME: string;
|
||||
TELEMETRY_ENABLED: string;
|
||||
LICENSE_KEY: string;
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 493 KiB After Width: | Height: | Size: 493 KiB |
9
backend-mongo/jest.config.ts
Normal file
9
backend-mongo/jest.config.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export default {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
collectCoverageFrom: ["src/*.{js,ts}", "!**/node_modules/**"],
|
||||
modulePaths: ["<rootDir>/src"],
|
||||
testMatch: ["<rootDir>/tests/**/*.test.ts"],
|
||||
setupFiles: ["<rootDir>/test-resources/env-vars.js"],
|
||||
setupFilesAfterEnv: ["<rootDir>/tests/setupTests.ts"],
|
||||
};
|
6
backend-mongo/nodemon.json
Normal file
6
backend-mongo/nodemon.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": ".ts,.js",
|
||||
"ignore": [],
|
||||
"exec": "ts-node ./src/index.ts"
|
||||
}
|
32861
backend-mongo/package-lock.json
generated
Normal file
32861
backend-mongo/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
148
backend-mongo/package.json
Normal file
148
backend-mongo/package.json
Normal file
@ -0,0 +1,148 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-secrets-manager": "^3.319.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@casl/mongoose": "^7.2.1",
|
||||
"@godaddy/terminus": "^4.12.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"@sentry/node": "^7.77.0",
|
||||
"@sentry/tracing": "^7.48.0",
|
||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/libsodium-wrappers": "^0.7.10",
|
||||
"@ucast/mongo2js": "^1.3.4",
|
||||
"ajv": "^8.12.0",
|
||||
"argon2": "^0.30.3",
|
||||
"aws-sdk": "^2.1364.0",
|
||||
"axios": "^1.6.0",
|
||||
"axios-retry": "^3.4.0",
|
||||
"bcrypt": "^5.1.0",
|
||||
"bigint-conversion": "^2.4.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"crypto-js": "^4.2.0",
|
||||
"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.2.1",
|
||||
"ioredis": "^5.3.2",
|
||||
"jmespath": "^0.16.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jsrp": "^0.2.4",
|
||||
"libsodium-wrappers": "^0.7.10",
|
||||
"lodash": "^4.17.21",
|
||||
"mongoose": "^7.4.1",
|
||||
"mysql2": "^3.6.2",
|
||||
"nanoid": "^3.3.6",
|
||||
"node-cache": "^5.1.2",
|
||||
"nodemailer": "^6.8.0",
|
||||
"ora": "^5.4.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-github": "^1.1.0",
|
||||
"passport-gitlab2": "^5.0.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"pg": "^8.11.3",
|
||||
"pino": "^8.16.1",
|
||||
"pino-http": "^8.5.1",
|
||||
"posthog-node": "^2.6.0",
|
||||
"probot": "^12.3.3",
|
||||
"query-string": "^7.1.3",
|
||||
"rate-limit-mongo": "^2.3.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"swagger-ui-express": "^4.6.2",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"typescript": "^4.9.3",
|
||||
"utility-types": "^3.10.0",
|
||||
"zod": "^3.22.3"
|
||||
},
|
||||
"overrides": {
|
||||
"rate-limit-mongo": {
|
||||
"mongodb": "5.8.0"
|
||||
}
|
||||
},
|
||||
"name": "infisical-api",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node build/index.js",
|
||||
"dev": "nodemon index.js",
|
||||
"swagger-autogen": "node ./swagger/index.ts",
|
||||
"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 --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"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Infisical/infisical-api.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Infisical/infisical-api/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Infisical/infisical-api#readme",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.3.1",
|
||||
"@posthog/plugin-scaffold": "^1.3.4",
|
||||
"@swc/core": "^1.3.99",
|
||||
"@swc/helpers": "^0.5.3",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/bull": "^4.10.0",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/jmespath": "^0.15.1",
|
||||
"@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/pg": "^8.10.7",
|
||||
"@types/picomatch": "^2.3.0",
|
||||
"@types/pino": "^7.0.5",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@types/swagger-jsdoc": "^6.0.1",
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"install": "^0.13.0",
|
||||
"jest": "^29.3.1",
|
||||
"jest-junit": "^15.0.0",
|
||||
"nodemon": "^2.0.19",
|
||||
"npm": "^8.19.3",
|
||||
"pino-pretty": "^10.2.3",
|
||||
"regenerator-runtime": "^0.14.0",
|
||||
"smee-client": "^1.2.3",
|
||||
"supertest": "^6.3.3",
|
||||
"swagger-autogen": "^2.23.5",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-node": "^10.9.1"
|
||||
},
|
||||
"jest-junit": {
|
||||
"outputDirectory": "reports",
|
||||
"outputName": "jest-junit.xml",
|
||||
"ancestorSeparator": " › ",
|
||||
"uniqueOutputName": "false",
|
||||
"suiteNameTemplate": "{filepath}",
|
||||
"classNameTemplate": "{classname}",
|
||||
"titleTemplate": "{title}"
|
||||
}
|
||||
}
|
8047
backend-mongo/spec.json
Normal file
8047
backend-mongo/spec.json
Normal file
File diff suppressed because it is too large
Load Diff
43
backend-mongo/src/bootstrap.ts
Normal file
43
backend-mongo/src/bootstrap.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import ora from "ora";
|
||||
import nodemailer from "nodemailer";
|
||||
import { getSmtpHost, getSmtpPort } from "./config";
|
||||
import { logger } from "./utils/logging";
|
||||
import mongoose from "mongoose";
|
||||
import { redisClient } from "./services/RedisService";
|
||||
|
||||
type BootstrapOpt = {
|
||||
transporter: nodemailer.Transporter;
|
||||
};
|
||||
|
||||
export const bootstrap = async ({ transporter }: BootstrapOpt) => {
|
||||
const spinner = ora().start();
|
||||
spinner.info("Checking configurations...");
|
||||
spinner.info("Testing smtp connection");
|
||||
|
||||
await transporter
|
||||
.verify()
|
||||
.then(async () => {
|
||||
spinner.succeed("SMTP successfully connected");
|
||||
})
|
||||
.catch(async (err) => {
|
||||
spinner.fail(`SMTP - Failed to connect to ${await getSmtpHost()}:${await getSmtpPort()}`);
|
||||
logger.error(err);
|
||||
});
|
||||
|
||||
spinner.info("Testing mongodb connection");
|
||||
if (mongoose.connection.readyState !== mongoose.ConnectionStates.connected) {
|
||||
spinner.fail("Mongo DB - Failed to connect");
|
||||
} else {
|
||||
spinner.succeed("Mongodb successfully connected");
|
||||
}
|
||||
|
||||
spinner.info("Testing redis connection");
|
||||
const redisPing = await redisClient?.ping();
|
||||
if (!redisPing) {
|
||||
spinner.fail("Redis - Failed to connect");
|
||||
} else {
|
||||
spinner.succeed("Redis successfully connected");
|
||||
}
|
||||
|
||||
spinner.stop();
|
||||
};
|
176
backend-mongo/src/config/index.ts
Normal file
176
backend-mongo/src/config/index.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import { GITLAB_URL } from "../variables";
|
||||
|
||||
import InfisicalClient from "infisical-node";
|
||||
|
||||
export const client = new InfisicalClient({
|
||||
token: process.env.INFISICAL_TOKEN!
|
||||
});
|
||||
|
||||
export const getIsMigrationMode = async () =>
|
||||
(await client.getSecret("MIGRATION_MODE")).secretValue === "true";
|
||||
|
||||
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 getAuthSecret = async () =>
|
||||
(await client.getSecret("JWT_AUTH_SECRET")).secretValue ??
|
||||
(await client.getSecret("AUTH_SECRET")).secretValue;
|
||||
export const getJwtAuthLifetime = async () =>
|
||||
(await client.getSecret("JWT_AUTH_LIFETIME")).secretValue || "10d";
|
||||
export const getJwtMfaLifetime = async () =>
|
||||
(await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m";
|
||||
export const getJwtRefreshLifetime = async () =>
|
||||
(await client.getSecret("JWT_REFRESH_LIFETIME")).secretValue || "90d";
|
||||
export const getJwtServiceSecret = async () =>
|
||||
(await client.getSecret("JWT_SERVICE_SECRET")).secretValue; // TODO: deprecate (related to ST V1)
|
||||
export const getJwtSignupLifetime = async () =>
|
||||
(await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m";
|
||||
export const getJwtProviderAuthLifetime = async () =>
|
||||
(await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m";
|
||||
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 getClientIdBitBucket = async () =>
|
||||
(await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
|
||||
export const getClientIdGCPSecretManager = async () =>
|
||||
(await client.getSecret("CLIENT_ID_GCP_SECRET_MANAGER")).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 getClientSecretBitBucket = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
|
||||
export const getClientSecretGCPSecretManager = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_GCP_SECRET_MANAGER")).secretValue;
|
||||
export const getClientSlugVercel = async () =>
|
||||
(await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
|
||||
|
||||
export const getClientIdGoogleLogin = async () =>
|
||||
(await client.getSecret("CLIENT_ID_GOOGLE_LOGIN")).secretValue;
|
||||
export const getClientSecretGoogleLogin = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
|
||||
export const getClientIdGitHubLogin = async () =>
|
||||
(await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
|
||||
export const getClientSecretGitHubLogin = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue;
|
||||
export const getClientIdGitLabLogin = async () =>
|
||||
(await client.getSecret("CLIENT_ID_GITLAB_LOGIN")).secretValue;
|
||||
export const getClientSecretGitLabLogin = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_GITLAB_LOGIN")).secretValue;
|
||||
export const getUrlGitLabLogin = async () =>
|
||||
(await client.getSecret("URL_GITLAB_LOGIN")).secretValue || GITLAB_URL;
|
||||
|
||||
export const getAwsCloudWatchLog = async () => {
|
||||
const logGroupName =
|
||||
(await client.getSecret("AWS_CLOUDWATCH_LOG_GROUP_NAME")).secretValue || "infisical-log-stream";
|
||||
const region = (await client.getSecret("AWS_CLOUDWATCH_LOG_REGION")).secretValue;
|
||||
const accessKeyId = (await client.getSecret("AWS_CLOUDWATCH_LOG_ACCESS_KEY_ID")).secretValue;
|
||||
const accessKeySecret = (await client.getSecret("AWS_CLOUDWATCH_LOG_ACCESS_KEY_SECRET"))
|
||||
.secretValue;
|
||||
const interval = parseInt(
|
||||
(await client.getSecret("AWS_CLOUDWATCH_LOG_INTERVAL")).secretValue || 1000,
|
||||
10
|
||||
);
|
||||
if (!region || !accessKeyId || !accessKeySecret) return;
|
||||
return { logGroupName, region, accessKeySecret, accessKeyId, interval };
|
||||
};
|
||||
|
||||
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 getSecretScanningWebhookProxy = async () =>
|
||||
(await client.getSecret("SECRET_SCANNING_WEBHOOK_PROXY")).secretValue;
|
||||
export const getSecretScanningWebhookSecret = async () =>
|
||||
(await client.getSecret("SECRET_SCANNING_WEBHOOK_SECRET")).secretValue;
|
||||
export const getSecretScanningGitAppId = async () =>
|
||||
(await client.getSecret("SECRET_SCANNING_GIT_APP_ID")).secretValue;
|
||||
export const getSecretScanningPrivateKey = async () =>
|
||||
(await client.getSecret("SECRET_SCANNING_PRIVATE_KEY")).secretValue;
|
||||
|
||||
export const getRedisUrl = async () => (await client.getSecret("REDIS_URL")).secretValue;
|
||||
export const getIsInfisicalCloud = async () =>
|
||||
(await client.getSecret("INFISICAL_CLOUD")).secretValue === "true";
|
||||
|
||||
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";
|
||||
|
||||
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;
|
||||
};
|
124
backend-mongo/src/config/request.ts
Normal file
124
backend-mongo/src/config/request.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import axios from "axios";
|
||||
import axiosRetry from "axios-retry";
|
||||
import {
|
||||
getLicenseKeyAuthToken,
|
||||
getLicenseServerKeyAuthToken,
|
||||
setLicenseKeyAuthToken,
|
||||
setLicenseServerKeyAuthToken,
|
||||
} from "./storage";
|
||||
import {
|
||||
getLicenseKey,
|
||||
getLicenseServerKey,
|
||||
getLicenseServerUrl,
|
||||
} from "./index";
|
||||
|
||||
// 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(standardRequest, {
|
||||
retries: 3,
|
||||
retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries
|
||||
retryCondition: (error) => {
|
||||
// only retry if the error is a network error or a 5xx server error
|
||||
return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error);
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
24
backend-mongo/src/config/serverConfig.ts
Normal file
24
backend-mongo/src/config/serverConfig.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { IServerConfig, ServerConfig } from "../models/serverConfig";
|
||||
|
||||
let serverConfig: IServerConfig;
|
||||
|
||||
export const serverConfigInit = async () => {
|
||||
const cfg = await ServerConfig.findOne({}).lean();
|
||||
if (!cfg) {
|
||||
const cfg = new ServerConfig();
|
||||
await cfg.save();
|
||||
serverConfig = cfg.toObject();
|
||||
} else {
|
||||
serverConfig = cfg;
|
||||
}
|
||||
return serverConfig;
|
||||
};
|
||||
|
||||
export const getServerConfig = () => serverConfig;
|
||||
|
||||
export const updateServerConfig = async (data: Partial<IServerConfig>) => {
|
||||
const cfg = await ServerConfig.findByIdAndUpdate(serverConfig._id, data, { new: true });
|
||||
if (!cfg) throw new Error("Failed to update server config");
|
||||
serverConfig = cfg.toObject();
|
||||
return serverConfig;
|
||||
};
|
30
backend-mongo/src/config/storage.ts
Normal file
30
backend-mongo/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;
|
101
backend-mongo/src/controllers/v1/adminController.ts
Normal file
101
backend-mongo/src/controllers/v1/adminController.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getHttpsEnabled, getIsMigrationMode } from "../../config";
|
||||
import { getServerConfig, updateServerConfig as setServerConfig } from "../../config/serverConfig";
|
||||
import { initializeDefaultOrg, issueAuthTokens } from "../../helpers";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { User } from "../../models";
|
||||
import { TelemetryService } from "../../services";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import * as reqValidator from "../../validation/admin";
|
||||
|
||||
export const getServerConfigInfo = async (_req: Request, res: Response) => {
|
||||
const config = getServerConfig();
|
||||
const isMigrationModeOn = await getIsMigrationMode();
|
||||
return res.send({ config: { ...config, isMigrationModeOn } });
|
||||
};
|
||||
|
||||
export const updateServerConfig = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { allowSignUp }
|
||||
} = await validateRequest(reqValidator.UpdateServerConfigV1, req);
|
||||
const config = await setServerConfig({ allowSignUp });
|
||||
return res.send({ config });
|
||||
};
|
||||
|
||||
export const adminSignUp = async (req: Request, res: Response) => {
|
||||
const cfg = getServerConfig();
|
||||
if (cfg.initialized) throw UnauthorizedRequestError({ message: "Admin has been created" });
|
||||
const {
|
||||
body: {
|
||||
email,
|
||||
publicKey,
|
||||
salt,
|
||||
lastName,
|
||||
verifier,
|
||||
firstName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag
|
||||
}
|
||||
} = await validateRequest(reqValidator.SignupV1, req);
|
||||
let user = await User.findOne({ email });
|
||||
if (user) throw BadRequestError({ message: "User already exist" });
|
||||
user = new User({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
superAdmin: true
|
||||
});
|
||||
await user.save();
|
||||
await initializeDefaultOrg({ organizationName: "Admin Org", user });
|
||||
|
||||
await setServerConfig({ initialized: true });
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? ""
|
||||
});
|
||||
|
||||
const token = tokens.token;
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "admin initialization",
|
||||
properties: {
|
||||
email: user.email,
|
||||
lastName,
|
||||
firstName
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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 admin account",
|
||||
user,
|
||||
token
|
||||
});
|
||||
};
|
277
backend-mongo/src/controllers/v1/authController.ts
Normal file
277
backend-mongo/src/controllers/v1/authController.ts
Normal file
@ -0,0 +1,277 @@
|
||||
import { Request, Response } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import * as bigintConversion from "bigint-conversion";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const jsrp = require("jsrp");
|
||||
import {
|
||||
LoginSRPDetail,
|
||||
TokenVersion,
|
||||
User
|
||||
} from "../../models";
|
||||
import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth";
|
||||
import { checkUserDevice } from "../../helpers/user";
|
||||
import { AuthTokenType } from "../../variables";
|
||||
import {
|
||||
BadRequestError,
|
||||
UnauthorizedRequestError
|
||||
} from "../../utils/errors";
|
||||
import {
|
||||
getAuthSecret,
|
||||
getHttpsEnabled,
|
||||
getJwtAuthLifetime,
|
||||
} from "../../config";
|
||||
import { ActorType } from "../../ee/models";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/auth";
|
||||
|
||||
declare module "jsonwebtoken" {
|
||||
export interface AuthnJwtPayload extends jwt.JwtPayload {
|
||||
authTokenType: AuthTokenType;
|
||||
}
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
userId: string;
|
||||
refreshVersion?: number;
|
||||
}
|
||||
export interface IdentityAccessTokenJwtPayload extends jwt.JwtPayload {
|
||||
_id: string;
|
||||
clientSecretId: string;
|
||||
identityAccessTokenId: string;
|
||||
authTokenType: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
const {
|
||||
body: { email, clientPublicKey }
|
||||
} = await validateRequest(reqValidator.Login1V1, req);
|
||||
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select("+salt +verifier");
|
||||
|
||||
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();
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
const {
|
||||
body: { email, clientProof }
|
||||
} = await validateRequest(reqValidator.Login2V1, req);
|
||||
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag");
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
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"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
await checkUserDevice({
|
||||
user,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? ""
|
||||
});
|
||||
|
||||
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()
|
||||
});
|
||||
|
||||
// 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
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: "Failed to authenticate. Try again?"
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Log out user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const logout = async (req: Request, res: Response) => {
|
||||
if (req.authData.actor.type === ActorType.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
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully logged out."
|
||||
});
|
||||
};
|
||||
|
||||
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
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const checkAuth = async (req: Request, res: Response) => {
|
||||
return res.status(200).send({
|
||||
message: "Authenticated"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return new JWT access token by first validating the refresh token
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getNewToken = async (req: Request, res: Response) => {
|
||||
|
||||
const refreshToken = req.cookies.jid;
|
||||
|
||||
if (!refreshToken)
|
||||
throw BadRequestError({
|
||||
message: "Failed to find refresh token in request cookies"
|
||||
});
|
||||
|
||||
const decodedToken = <jwt.UserIDJwtPayload>jwt.verify(refreshToken, await getAuthSecret());
|
||||
|
||||
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) throw UnauthorizedRequestError();
|
||||
|
||||
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: {
|
||||
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||
userId: decodedToken.userId,
|
||||
tokenVersionId: tokenVersion._id.toString(),
|
||||
accessVersion: tokenVersion.refreshVersion
|
||||
},
|
||||
expiresIn: await getJwtAuthLifetime(),
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
token
|
||||
});
|
||||
};
|
||||
|
||||
export const handleAuthProviderCallback = (req: Request, res: Response) => {
|
||||
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
|
||||
};
|
135
backend-mongo/src/controllers/v1/botController.ts
Normal file
135
backend-mongo/src/controllers/v1/botController.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Bot, BotKey } from "../../models";
|
||||
import { createBot } from "../../helpers/bot";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/bot";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
|
||||
interface BotKey {
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return bot for workspace with id [workspaceId]. If a workspace bot doesn't exist,
|
||||
* then create and return a new bot.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetBotByWorkspaceIdV1, req);
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
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 bot with id [req.bot._id] with active state set to [isActive].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const setBotActiveState = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { botKey, isActive },
|
||||
params: { botId }
|
||||
} = await validateRequest(reqValidator.SetBotActiveStateV1, req);
|
||||
|
||||
const bot = await Bot.findById(botId);
|
||||
if (!bot) {
|
||||
throw BadRequestError({ message: "Bot not found" });
|
||||
}
|
||||
const userId = req.user._id;
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: bot.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
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: bot.workspace
|
||||
},
|
||||
{
|
||||
encryptedKey: botKey.encryptedKey,
|
||||
nonce: botKey.nonce,
|
||||
sender: userId,
|
||||
bot: bot._id,
|
||||
workspace: bot.workspace
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
new: true
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// case: bot state set to inactive -> delete bot's workspace key
|
||||
await BotKey.deleteOne({
|
||||
bot: bot._id
|
||||
});
|
||||
}
|
||||
|
||||
const updatedBot = await Bot.findOneAndUpdate(
|
||||
{
|
||||
_id: bot._id
|
||||
},
|
||||
{
|
||||
isActive
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (!updatedBot) throw new Error("Failed to update bot active state");
|
||||
|
||||
return res.status(200).send({
|
||||
bot
|
||||
});
|
||||
};
|
43
backend-mongo/src/controllers/v1/index.ts
Normal file
43
backend-mongo/src/controllers/v1/index.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import * as authController from "./authController";
|
||||
import * as universalAuthController from "./universalAuthController";
|
||||
import * as botController from "./botController";
|
||||
import * as integrationAuthController from "./integrationAuthController";
|
||||
import * as integrationController from "./integrationController";
|
||||
import * as keyController from "./keyController";
|
||||
import * as membershipController from "./membershipController";
|
||||
import * as membershipOrgController from "./membershipOrgController";
|
||||
import * as organizationController from "./organizationController";
|
||||
import * as passwordController from "./passwordController";
|
||||
import * as secretController from "./secretController";
|
||||
import * as serviceTokenController from "./serviceTokenController";
|
||||
import * as signupController from "./signupController";
|
||||
import * as userActionController from "./userActionController";
|
||||
import * as userController from "./userController";
|
||||
import * as workspaceController from "./workspaceController";
|
||||
import * as secretScanningController from "./secretScanningController";
|
||||
import * as webhookController from "./webhookController";
|
||||
import * as secretImpsController from "./secretImpsController";
|
||||
import * as adminController from "./adminController";
|
||||
|
||||
export {
|
||||
authController,
|
||||
universalAuthController,
|
||||
botController,
|
||||
integrationAuthController,
|
||||
integrationController,
|
||||
keyController,
|
||||
membershipController,
|
||||
membershipOrgController,
|
||||
organizationController,
|
||||
passwordController,
|
||||
secretController,
|
||||
serviceTokenController,
|
||||
signupController,
|
||||
userActionController,
|
||||
userController,
|
||||
workspaceController,
|
||||
secretScanningController,
|
||||
webhookController,
|
||||
secretImpsController,
|
||||
adminController
|
||||
};
|
1303
backend-mongo/src/controllers/v1/integrationAuthController.ts
Normal file
1303
backend-mongo/src/controllers/v1/integrationAuthController.ts
Normal file
File diff suppressed because it is too large
Load Diff
322
backend-mongo/src/controllers/v1/integrationController.ts
Normal file
322
backend-mongo/src/controllers/v1/integrationController.ts
Normal file
@ -0,0 +1,322 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Folder, IWorkspace, Integration, IntegrationAuth } from "../../models";
|
||||
import { EventService } from "../../services";
|
||||
import { eventStartIntegration } from "../../events";
|
||||
import { getFolderByPath } from "../../services/FolderService";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import { EventType } from "../../ee/models";
|
||||
import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/syncSecretsToThirdPartyServices";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/integration";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
/**
|
||||
* Create/initialize an (empty) integration for integration authorization
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createIntegration = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: {
|
||||
isActive,
|
||||
sourceEnvironment,
|
||||
secretPath,
|
||||
app,
|
||||
path,
|
||||
appId,
|
||||
owner,
|
||||
region,
|
||||
scope,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
integrationAuthId,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
metadata
|
||||
}
|
||||
} = await validateRequest(reqValidator.CreateIntegrationV1, req);
|
||||
|
||||
const integrationAuth = await IntegrationAuth.findById(integrationAuthId)
|
||||
.populate<{ workspace: IWorkspace }>("workspace")
|
||||
.select(
|
||||
"+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt"
|
||||
);
|
||||
|
||||
if (!integrationAuth) throw BadRequestError({ message: "Integration auth not found" });
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: integrationAuth.workspace._id
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: integrationAuth.workspace._id,
|
||||
environment: sourceEnvironment
|
||||
});
|
||||
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (!folder) {
|
||||
throw BadRequestError({
|
||||
message: "Folder path doesn't exist"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: validate [sourceEnvironment] and [targetEnvironment]
|
||||
|
||||
// initialize new integration after saving integration access token
|
||||
const integration = await new Integration({
|
||||
workspace: integrationAuth.workspace._id,
|
||||
environment: sourceEnvironment,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
scope,
|
||||
secretPath,
|
||||
integration: integrationAuth.integration,
|
||||
integrationAuth: new Types.ObjectId(integrationAuthId),
|
||||
metadata
|
||||
}).save();
|
||||
|
||||
if (integration) {
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventStartIntegration({
|
||||
workspaceId: integration.workspace,
|
||||
environment: sourceEnvironment
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_INTEGRATION,
|
||||
metadata: {
|
||||
integrationId: integration._id.toString(),
|
||||
integration: integration.integration,
|
||||
environment: integration.environment,
|
||||
secretPath,
|
||||
url: integration.url,
|
||||
app: integration.app,
|
||||
appId: integration.appId,
|
||||
targetEnvironment: integration.targetEnvironment,
|
||||
targetEnvironmentId: integration.targetEnvironmentId,
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: integration.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
integration
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change environment or name of integration with id [integrationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateIntegration = async (req: Request, res: Response) => {
|
||||
// TODO: add integration-specific validation to ensure that each
|
||||
// integration has the correct fields populated in [Integration]
|
||||
|
||||
const {
|
||||
body: {
|
||||
environment,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner, // github-specific integration param
|
||||
secretPath
|
||||
},
|
||||
params: { integrationId }
|
||||
} = await validateRequest(reqValidator.UpdateIntegrationV1, req);
|
||||
|
||||
const integration = await Integration.findById(integrationId);
|
||||
if (!integration) throw BadRequestError({ message: "Integration not found" });
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: integration.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: integration.workspace,
|
||||
environment
|
||||
});
|
||||
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (!folder) {
|
||||
throw BadRequestError({
|
||||
message: "Path for service token does not exist"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updatedIntegration = await Integration.findOneAndUpdate(
|
||||
{
|
||||
_id: integration._id
|
||||
},
|
||||
{
|
||||
environment,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner,
|
||||
secretPath
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (updatedIntegration) {
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventStartIntegration({
|
||||
workspaceId: updatedIntegration.workspace,
|
||||
environment
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
integration: updatedIntegration
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete integration with id [integrationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteIntegration = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { integrationId }
|
||||
} = await validateRequest(reqValidator.DeleteIntegrationV1, req);
|
||||
|
||||
const integration = await Integration.findById(integrationId);
|
||||
if (!integration) throw BadRequestError({ message: "Integration not found" });
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: integration.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
const deletedIntegration = await Integration.findOneAndDelete({
|
||||
_id: integrationId
|
||||
});
|
||||
|
||||
if (!deletedIntegration) throw new Error("Failed to find integration");
|
||||
|
||||
const numOtherIntegrationsUsingSameAuth = await Integration.countDocuments({
|
||||
integrationAuth: deletedIntegration.integrationAuth,
|
||||
_id: {
|
||||
$nin: [deletedIntegration._id]
|
||||
}
|
||||
});
|
||||
|
||||
if (numOtherIntegrationsUsingSameAuth === 0) {
|
||||
// no other integrations are using the same integration auth
|
||||
// -> delete integration auth associated with the integration being deleted
|
||||
await IntegrationAuth.deleteOne({
|
||||
_id: deletedIntegration.integrationAuth
|
||||
});
|
||||
}
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.DELETE_INTEGRATION,
|
||||
metadata: {
|
||||
integrationId: integration._id.toString(),
|
||||
integration: integration.integration,
|
||||
environment: integration.environment,
|
||||
secretPath: integration.secretPath,
|
||||
url: integration.url,
|
||||
app: integration.app,
|
||||
appId: integration.appId,
|
||||
targetEnvironment: integration.targetEnvironment,
|
||||
targetEnvironmentId: integration.targetEnvironmentId,
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: integration.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
integration
|
||||
});
|
||||
};
|
||||
|
||||
// Will trigger sync for all integrations within the given env and workspace id
|
||||
export const manualSync = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { workspaceId, environment }
|
||||
} = await validateRequest(reqValidator.ManualSyncV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
syncSecretsToActiveIntegrationsQueue({
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
|
||||
res.status(200).send();
|
||||
};
|
101
backend-mongo/src/controllers/v1/keyController.ts
Normal file
101
backend-mongo/src/controllers/v1/keyController.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { Types } from "mongoose";
|
||||
import { Request, Response } from "express";
|
||||
import { Key } from "../../models";
|
||||
import { findMembership } from "../../helpers/membership";
|
||||
import { EventType } from "../../ee/models";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/key";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
/**
|
||||
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
|
||||
* id [key.userId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const uploadKey = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { key }
|
||||
} = await validateRequest(reqValidator.UploadKeyV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
// validate membership of receiver
|
||||
const receiverMembership = await findMembership({
|
||||
user: key.userId,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully uploaded key to workspace"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return latest (encrypted) copy of workspace key for user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getLatestKey = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetLatestKeyV1, req);
|
||||
|
||||
// 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 = {};
|
||||
|
||||
if (latestKey.length > 0) {
|
||||
resObj["latestKey"] = latestKey[0];
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.GET_WORKSPACE_KEY,
|
||||
metadata: {
|
||||
keyId: latestKey[0]._id.toString()
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return res.status(200).send(resObj);
|
||||
};
|
286
backend-mongo/src/controllers/v1/membershipController.ts
Normal file
286
backend-mongo/src/controllers/v1/membershipController.ts
Normal file
@ -0,0 +1,286 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models";
|
||||
import { EventType, Role } from "../../ee/models";
|
||||
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
|
||||
import { sendMail } from "../../helpers/nodemailer";
|
||||
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
|
||||
import { getSiteURL } from "../../config";
|
||||
import { EEAuditLogService, EELicenseService } from "../../ee/services";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/membership";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { InviteUserToWorkspaceV1 } from "../../validation/workspace";
|
||||
|
||||
/**
|
||||
* Check that user is a member of workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const validateMembership = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.ValidateMembershipV1, req);
|
||||
|
||||
// validate membership
|
||||
const membership = await findMembership({
|
||||
user: req.user._id,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new Error("Failed to validate membership");
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Workspace membership confirmed"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete membership with id [membershipId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteMembership = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { membershipId }
|
||||
} = await validateRequest(reqValidator.DeleteMembershipV1, req);
|
||||
|
||||
// check if membership to delete exists
|
||||
const membershipToDelete = await Membership.findOne({
|
||||
_id: membershipId
|
||||
}).populate<{ user: IUser }>("user");
|
||||
|
||||
if (!membershipToDelete) {
|
||||
throw new Error("Failed to delete workspace membership that doesn't exist");
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: membershipToDelete.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
// delete workspace membership
|
||||
const deletedMembership = await deleteMember({
|
||||
membershipId: membershipToDelete._id.toString()
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.REMOVE_WORKSPACE_MEMBER,
|
||||
metadata: {
|
||||
userId: membershipToDelete.user._id.toString(),
|
||||
email: membershipToDelete.user.email
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: membershipToDelete.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
deletedMembership
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change and return workspace membership role
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { role },
|
||||
params: { membershipId }
|
||||
} = await validateRequest(reqValidator.ChangeMembershipRoleV1, req);
|
||||
|
||||
// validate target membership
|
||||
const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>(
|
||||
"user"
|
||||
);
|
||||
|
||||
if (!membershipToChangeRole) {
|
||||
throw new Error("Failed to find membership to change role");
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: membershipToChangeRole.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
|
||||
if (isCustomRole) {
|
||||
const wsRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: false,
|
||||
workspace: membershipToChangeRole.workspace
|
||||
});
|
||||
if (!wsRole) throw BadRequestError({ message: "Role not found" });
|
||||
|
||||
const plan = await EELicenseService.getPlan(wsRole.organization);
|
||||
|
||||
if (!plan.rbac) return res.status(400).send({
|
||||
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
||||
});
|
||||
|
||||
const membership = await Membership.findByIdAndUpdate(membershipId, {
|
||||
role: CUSTOM,
|
||||
customRole: wsRole
|
||||
});
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
}
|
||||
|
||||
const membership = await Membership.findByIdAndUpdate(
|
||||
membershipId,
|
||||
{
|
||||
$set: {
|
||||
role
|
||||
},
|
||||
$unset: {
|
||||
customRole: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.UPDATE_USER_WORKSPACE_ROLE,
|
||||
metadata: {
|
||||
userId: membershipToChangeRole.user._id.toString(),
|
||||
email: membershipToChangeRole.user.email,
|
||||
oldRole: membershipToChangeRole.role,
|
||||
newRole: role
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: membershipToChangeRole.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add user with email [email] to workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const inviteUserToWorkspace = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { email }
|
||||
} = await validateRequest(InviteUserToWorkspaceV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
const invitee = await User.findOne({
|
||||
email
|
||||
}).select("+publicKey");
|
||||
|
||||
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
|
||||
}).populate<{ user: IUser }>("user");
|
||||
|
||||
if (inviteeMembership) throw new Error("Failed to add existing member of workspace");
|
||||
|
||||
const workspace = await Workspace.findById(workspaceId);
|
||||
if (!workspace) throw new Error("Failed to find 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: workspace.organization,
|
||||
status: ACCEPTED
|
||||
});
|
||||
|
||||
if (!membershipOrg) throw new Error("Failed to validate invitee's organization membership");
|
||||
|
||||
// get latest key
|
||||
const latestKey = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.populate("sender", "+publicKey");
|
||||
|
||||
// create new workspace membership
|
||||
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: workspace.name,
|
||||
callback_url: (await getSiteURL()) + "/login"
|
||||
}
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.ADD_WORKSPACE_MEMBER,
|
||||
metadata: {
|
||||
userId: invitee._id.toString(),
|
||||
email: invitee.email
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
invitee,
|
||||
latestKey
|
||||
});
|
||||
};
|
292
backend-mongo/src/controllers/v1/membershipOrgController.ts
Normal file
292
backend-mongo/src/controllers/v1/membershipOrgController.ts
Normal file
@ -0,0 +1,292 @@
|
||||
import { Types } from "mongoose";
|
||||
import { Request, Response } from "express";
|
||||
import { MembershipOrg, Organization, User } from "../../models";
|
||||
import { SSOConfig } from "../../ee/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 { ACCEPTED, AuthTokenType, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables";
|
||||
import * as reqValidator from "../../validation/membershipOrg";
|
||||
import {
|
||||
getAuthSecret,
|
||||
getJwtSignupLifetime,
|
||||
getSiteURL,
|
||||
getSmtpConfigured
|
||||
} from "../../config";
|
||||
import { validateUserEmail } from "../../validation";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
getAuthDataOrgPermissions
|
||||
} from "../../ee/services/RoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
/**
|
||||
* Delete organization membership with id [membershipOrgId] from organization
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteMembershipOrg = async (req: Request, _res: Response) => {
|
||||
const {
|
||||
params: { membershipOrgId }
|
||||
} = await validateRequest(reqValidator.DelOrgMembershipv1, req);
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: membershipOrgToDelete.organization
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
// delete organization membership
|
||||
await deleteMemberFromOrg({
|
||||
membershipOrgId: membershipOrgToDelete._id.toString()
|
||||
});
|
||||
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId: membershipOrgToDelete.organization.toString()
|
||||
});
|
||||
|
||||
return membershipOrgToDelete;
|
||||
};
|
||||
|
||||
/**
|
||||
* Change and return organization membership role
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
||||
// change role for (target) organization membership with id
|
||||
// [membershipOrgId]
|
||||
|
||||
let membershipToChangeRole;
|
||||
|
||||
return res.status(200).send({
|
||||
membershipOrg: membershipToChangeRole
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Organization invitation step 1: Send email invitation to user with email [email]
|
||||
* for organization with id [organizationId] containing magic link
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||
let inviteeMembershipOrg, completeInviteLink;
|
||||
const {
|
||||
body: { inviteeEmail, organizationId }
|
||||
} = await validateRequest(reqValidator.InviteUserToOrgv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
const host = req.headers.host;
|
||||
const siteUrl = `${req.protocol}://${host}`;
|
||||
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
|
||||
|
||||
const ssoConfig = await SSOConfig.findOne({
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
if (ssoConfig && ssoConfig.isActive) {
|
||||
// case: SAML SSO is enabled for the organization
|
||||
return res.status(400).send({
|
||||
message: "Failed to invite member due to SAML SSO configured for organization"
|
||||
});
|
||||
}
|
||||
|
||||
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."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const invitee = await User.findOne({
|
||||
email: inviteeEmail
|
||||
}).select("+publicKey");
|
||||
|
||||
if (invitee) {
|
||||
// case: invitee is an existing user
|
||||
|
||||
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) {
|
||||
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
|
||||
|
||||
// validate that email is not disposable
|
||||
validateUserEmail(inviteeEmail);
|
||||
|
||||
await new MembershipOrg({
|
||||
inviteEmail: inviteeEmail,
|
||||
organization: organizationId,
|
||||
role: MEMBER,
|
||||
status: INVITED
|
||||
}).save();
|
||||
}
|
||||
}
|
||||
|
||||
const organization = await Organization.findOne({ _id: organizationId });
|
||||
|
||||
if (organization) {
|
||||
const token = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||
email: inviteeEmail,
|
||||
organizationId: organization._id
|
||||
});
|
||||
|
||||
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"
|
||||
}
|
||||
});
|
||||
|
||||
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}`,
|
||||
completeInviteLink
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Organization invitation step 2: Verify that code [code] was sent to email [email] as part of
|
||||
* magic link and issue a temporary signup token for user to complete setting up their account
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
let user;
|
||||
|
||||
const {
|
||||
body: { organizationId, email, code }
|
||||
} = await validateRequest(reqValidator.VerifyUserToOrgv1, req);
|
||||
|
||||
user = await User.findOne({ email }).select("+publicKey");
|
||||
|
||||
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");
|
||||
|
||||
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();
|
||||
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully verified email",
|
||||
user
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
// initialize user account
|
||||
user = await new User({
|
||||
email
|
||||
}).save();
|
||||
}
|
||||
|
||||
// generate temporary signup token
|
||||
const token = createToken({
|
||||
payload: {
|
||||
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: await getJwtSignupLifetime(),
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully verified email",
|
||||
user,
|
||||
token
|
||||
});
|
||||
};
|
387
backend-mongo/src/controllers/v1/organizationController.ts
Normal file
387
backend-mongo/src/controllers/v1/organizationController.ts
Normal file
@ -0,0 +1,387 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
IncidentContactOrg,
|
||||
Membership,
|
||||
MembershipOrg,
|
||||
Organization,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { getLicenseServerUrl, getSiteURL } from "../../config";
|
||||
import { licenseServerKeyRequest } from "../../config/request";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/organization";
|
||||
import { ACCEPTED } from "../../variables";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
getAuthDataOrgPermissions
|
||||
} from "../../ee/services/RoleService";
|
||||
import { OrganizationNotFoundError } from "../../utils/errors";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
export const getOrganizations = async (req: Request, res: Response) => {
|
||||
const organizations = (
|
||||
await MembershipOrg.find({
|
||||
user: req.user._id,
|
||||
status: ACCEPTED
|
||||
}).populate("organization")
|
||||
).map((m) => m.organization);
|
||||
|
||||
return res.status(200).send({
|
||||
organizations
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganization = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgv1, req);
|
||||
|
||||
// ensure user has membership
|
||||
await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
})
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
organization
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return organization memberships for organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationMembers = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
const users = await MembershipOrg.find({
|
||||
organization: organizationId
|
||||
}).populate("user", "+publicKey");
|
||||
|
||||
return res.status(200).send({
|
||||
users
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return workspaces that user is part of in organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgWorkspacesv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
})
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Workspace
|
||||
);
|
||||
|
||||
const workspacesSet = new Set(
|
||||
(
|
||||
await Workspace.find(
|
||||
{
|
||||
organization: organizationId
|
||||
},
|
||||
"_id"
|
||||
)
|
||||
).map((w) => w._id.toString())
|
||||
);
|
||||
|
||||
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
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change name of organization with id [organizationId] to [name]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const changeOrganizationName = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId },
|
||||
body: { name }
|
||||
} = await validateRequest(reqValidator.ChangeOrgNamev1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Settings
|
||||
);
|
||||
|
||||
const organization = await Organization.findOneAndUpdate(
|
||||
{
|
||||
_id: organizationId
|
||||
},
|
||||
{
|
||||
name
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully changed organization name",
|
||||
organization
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return incident contacts of organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationIncidentContacts = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgIncidentContactv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.IncidentAccount
|
||||
);
|
||||
|
||||
const incidentContactsOrg = await IncidentContactOrg.find({
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
incidentContactsOrg
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add and return new incident contact with email [email] for organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const addOrganizationIncidentContact = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId },
|
||||
body: { email }
|
||||
} = await validateRequest(reqValidator.CreateOrgIncideContact, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.IncidentAccount
|
||||
);
|
||||
|
||||
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
|
||||
{ email, organization: organizationId },
|
||||
{ email, organization: organizationId },
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
incidentContactOrg
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete incident contact with email [email] for organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteOrganizationIncidentContact = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId },
|
||||
body: { email }
|
||||
} = await validateRequest(reqValidator.DelOrgIncideContact, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.IncidentAccount
|
||||
);
|
||||
|
||||
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
|
||||
email,
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully deleted organization incident contact",
|
||||
incidentContactOrg
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Redirect user to billing portal or add card page depending on
|
||||
* if there is a card on file
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createOrganizationPortalSession = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { pmtMethods }
|
||||
} = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/payment-methods`
|
||||
);
|
||||
|
||||
if (pmtMethods.length < 1) {
|
||||
// case: organization has no payment method on file
|
||||
// -> redirect to add payment method portal
|
||||
const {
|
||||
data: { url }
|
||||
} = await licenseServerKeyRequest.post(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/payment-methods`,
|
||||
{
|
||||
success_url: (await getSiteURL()) + "/dashboard",
|
||||
cancel_url: (await getSiteURL()) + "/dashboard"
|
||||
}
|
||||
);
|
||||
return res.status(200).send({ url });
|
||||
} else {
|
||||
// case: organization has payment method on file
|
||||
// -> redirect to billing portal
|
||||
const {
|
||||
data: { url }
|
||||
} = await licenseServerKeyRequest.post(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/billing-portal`,
|
||||
{
|
||||
return_url: (await getSiteURL()) + "/dashboard"
|
||||
}
|
||||
);
|
||||
return res.status(200).send({ url });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a org id, return the projects each member of the org belongs to
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationMembersAndTheirWorkspaces = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Workspace
|
||||
);
|
||||
|
||||
const workspacesSet = (
|
||||
await Workspace.find(
|
||||
{
|
||||
organization: organizationId
|
||||
},
|
||||
"_id"
|
||||
)
|
||||
).map((w) => w._id.toString());
|
||||
|
||||
const memberships = await Membership.find({
|
||||
workspace: { $in: workspacesSet }
|
||||
}).populate("workspace");
|
||||
const userToWorkspaceIds: any = {};
|
||||
|
||||
memberships.forEach((membership) => {
|
||||
const user = membership.user.toString();
|
||||
if (userToWorkspaceIds[user]) {
|
||||
userToWorkspaceIds[user].push(membership.workspace);
|
||||
} else {
|
||||
userToWorkspaceIds[user] = [membership.workspace];
|
||||
}
|
||||
});
|
||||
|
||||
return res.json(userToWorkspaceIds);
|
||||
};
|
370
backend-mongo/src/controllers/v1/passwordController.ts
Normal file
370
backend-mongo/src/controllers/v1/passwordController.ts
Normal file
@ -0,0 +1,370 @@
|
||||
import { Request, Response } from "express";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const jsrp = require("jsrp");
|
||||
import * as bigintConversion from "bigint-conversion";
|
||||
import { BackupPrivateKey, LoginSRPDetail, User } from "../../models";
|
||||
import { clearTokens, createToken, sendMail } from "../../helpers";
|
||||
import { TokenService } from "../../services";
|
||||
import { AuthTokenType, TOKEN_EMAIL_PASSWORD_RESET } from "../../variables";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import {
|
||||
getAuthSecret,
|
||||
getHttpsEnabled,
|
||||
getJwtSignupLifetime,
|
||||
getSiteURL
|
||||
} from "../../config";
|
||||
import { ActorType } from "../../ee/models";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/auth";
|
||||
|
||||
/**
|
||||
* Password reset step 1: Send email verification link to email [email]
|
||||
* for account recovery.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const emailPasswordReset = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { email }
|
||||
} = await validateRequest(reqValidator.EmailPasswordResetV1, req);
|
||||
|
||||
const user = await User.findOne({ email }).select("+publicKey");
|
||||
if (!user || !user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
|
||||
return res.status(200).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: "If an account exists with this email, a password reset link has been sent"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Password reset step 2: Verify email verification link sent to email [email]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { email, code }
|
||||
} = await validateRequest(reqValidator.EmailPasswordResetVerifyV1, req);
|
||||
|
||||
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
|
||||
const token = createToken({
|
||||
payload: {
|
||||
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: await getJwtSignupLifetime(),
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully verified email",
|
||||
user,
|
||||
token
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const srp1 = async (req: Request, res: Response) => {
|
||||
// return salt, serverPublicKey as part of first step of SRP protocol
|
||||
const {
|
||||
body: { clientPublicKey }
|
||||
} = await validateRequest(reqValidator.Srp1V1, req);
|
||||
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).select("+salt +verifier");
|
||||
|
||||
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();
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Change account SRP authentication information for user
|
||||
* Requires verifying [clientProof] as part of step 2 of SRP protocol
|
||||
* as initiated in POST /srp1
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const changePassword = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: {
|
||||
clientProof,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
}
|
||||
} = await validateRequest(reqValidator.ChangePasswordV1, req);
|
||||
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).select("+salt +verifier");
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
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"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (req.authData.actor.type === ActorType.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
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully changed password"
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
error: "Failed to change password. Try again?"
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create or change backup private key for user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createBackupPrivateKey = async (req: Request, res: Response) => {
|
||||
// create/change backup private key
|
||||
// requires verifying [clientProof] as part of second step of SRP protocol
|
||||
// as initiated in /srp1
|
||||
const {
|
||||
body: { clientProof, encryptedPrivateKey, salt, verifier, iv, tag }
|
||||
} = await validateRequest(reqValidator.CreateBackupPrivateKeyV1, req);
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).select("+salt +verifier");
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
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"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: "Failed to update backup private key"
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return backup private key for user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
||||
const backupPrivateKey = await BackupPrivateKey.findOne({
|
||||
user: req.user._id
|
||||
}).select("+encryptedPrivateKey +iv +tag");
|
||||
|
||||
if (!backupPrivateKey) throw new Error("Failed to find backup private key");
|
||||
|
||||
return res.status(200).send({
|
||||
backupPrivateKey
|
||||
});
|
||||
};
|
||||
|
||||
export const resetPassword = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: {
|
||||
encryptedPrivateKey,
|
||||
protectedKeyTag,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
salt,
|
||||
verifier,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag
|
||||
}
|
||||
} = await validateRequest(reqValidator.ResetPasswordV1, req);
|
||||
|
||||
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"
|
||||
});
|
||||
};
|
209
backend-mongo/src/controllers/v1/secretController.ts
Normal file
209
backend-mongo/src/controllers/v1/secretController.ts
Normal file
@ -0,0 +1,209 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Key } from "../../models";
|
||||
import {
|
||||
pullSecrets as pull,
|
||||
v1PushSecrets as push,
|
||||
reformatPullSecrets
|
||||
} from "../../helpers/secret";
|
||||
import { pushKeys } from "../../helpers/key";
|
||||
import { eventPushSecrets } from "../../events";
|
||||
import { EventService } from "../../services";
|
||||
import { TelemetryService } from "../../services";
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
ivKey: string;
|
||||
tagKey: string;
|
||||
hashKey: string;
|
||||
ciphertextValue: string;
|
||||
ivValue: string;
|
||||
tagValue: string;
|
||||
hashValue: string;
|
||||
ciphertextComment: string;
|
||||
ivComment: string;
|
||||
tagComment: string;
|
||||
hashComment: string;
|
||||
type: "shared" | "personal";
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
* for environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
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;
|
||||
|
||||
// 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 !== "");
|
||||
|
||||
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"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath: "/"
|
||||
})
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully uploaded workspace secrets"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return (encrypted) secrets for workspace with id [workspaceId]
|
||||
* for environment [environment] and (encrypted) workspace key
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const pullSecrets = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
|
||||
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;
|
||||
|
||||
// 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: req.user._id.toString(),
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: channel ? channel : "cli",
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
const 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"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets,
|
||||
key
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return (encrypted) secrets for workspace with id [workspaceId]
|
||||
* for environment [environment] and (encrypted) workspace key
|
||||
* via service token
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
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;
|
||||
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error("Failed to validate environment");
|
||||
}
|
||||
|
||||
const secrets = await pull({
|
||||
userId: req.serviceToken.user._id.toString(),
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: "cli",
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
const 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"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: reformatPullSecrets({ secrets }),
|
||||
key
|
||||
});
|
||||
};
|
734
backend-mongo/src/controllers/v1/secretImpsController.ts
Normal file
734
backend-mongo/src/controllers/v1/secretImpsController.ts
Normal file
@ -0,0 +1,734 @@
|
||||
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { isValidScope } from "../../helpers";
|
||||
import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models";
|
||||
import { getAllImportedSecrets } from "../../services/SecretImportService";
|
||||
import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
|
||||
import {
|
||||
BadRequestError,
|
||||
ResourceNotFoundError,
|
||||
UnauthorizedRequestError
|
||||
} from "../../utils/errors";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import { EventType } from "../../ee/models";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/secretImports";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
export const createSecretImp = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Create secret import'
|
||||
#swagger.description = 'Create secret import'
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaceId": {
|
||||
"type": "string",
|
||||
"description": "ID of workspace where to create secret import",
|
||||
"example": "someWorkspaceId"
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment where to create secret import",
|
||||
"example": "dev"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "Path where to create secret import like / or /foo/bar. Default is /",
|
||||
"example": "/foo/bar"
|
||||
},
|
||||
"secretImport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment to import from",
|
||||
"example": "development"
|
||||
},
|
||||
"secretPath": {
|
||||
"type": "string",
|
||||
"description": "Path where to import from like / or /foo/bar.",
|
||||
"example": "/user/oauth"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "environment", "directory", "secretImport"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "successfully created secret import"
|
||||
}
|
||||
},
|
||||
"description": "Confirmation of secret import creation"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[400] = {
|
||||
description: "Bad Request. For example, 'Secret import already exist'"
|
||||
}
|
||||
#swagger.responses[401] = {
|
||||
description: "Unauthorized request. For example, 'Folder Permission Denied'"
|
||||
}
|
||||
#swagger.responses[404] = {
|
||||
description: "Resource Not Found. For example, 'Failed to find folder'"
|
||||
}
|
||||
*/
|
||||
|
||||
const {
|
||||
body: { workspaceId, environment, directory, secretImport }
|
||||
} = await validateRequest(reqValidator.CreateSecretImportV1, req);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// root check
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: secretImport.environment, secretPath: secretImport.secretPath })
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
|
||||
if (!folders && directory !== "/")
|
||||
throw ResourceNotFoundError({ message: "Failed to find folder" });
|
||||
|
||||
let folderId = "root";
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
if (!importSecDoc) {
|
||||
const doc = new SecretImport({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId,
|
||||
imports: [{ environment: secretImport.environment, secretPath: secretImport.secretPath }]
|
||||
});
|
||||
|
||||
await doc.save();
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_SECRET_IMPORT,
|
||||
metadata: {
|
||||
secretImportId: doc._id.toString(),
|
||||
folderId: doc.folderId.toString(),
|
||||
importFromEnvironment: secretImport.environment,
|
||||
importFromSecretPath: secretImport.secretPath,
|
||||
importToEnvironment: environment,
|
||||
importToSecretPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: doc.workspace
|
||||
}
|
||||
);
|
||||
return res.status(200).json({ message: "successfully created secret import" });
|
||||
}
|
||||
|
||||
const doesImportExist = importSecDoc.imports.find(
|
||||
(el) => el.environment === secretImport.environment && el.secretPath === secretImport.secretPath
|
||||
);
|
||||
if (doesImportExist) {
|
||||
throw BadRequestError({ message: "Secret import already exist" });
|
||||
}
|
||||
|
||||
importSecDoc.imports.push({
|
||||
environment: secretImport.environment,
|
||||
secretPath: secretImport.secretPath
|
||||
});
|
||||
await importSecDoc.save();
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_SECRET_IMPORT,
|
||||
metadata: {
|
||||
secretImportId: importSecDoc._id.toString(),
|
||||
folderId: importSecDoc.folderId.toString(),
|
||||
importFromEnvironment: secretImport.environment,
|
||||
importFromSecretPath: secretImport.secretPath,
|
||||
importToEnvironment: environment,
|
||||
importToSecretPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: importSecDoc.workspace
|
||||
}
|
||||
);
|
||||
return res.status(200).json({ message: "successfully created secret import" });
|
||||
};
|
||||
|
||||
// to keep the ordering, you must pass all the imports in here not the only updated one
|
||||
// this is because the order decide which import gets overriden
|
||||
|
||||
/**
|
||||
* Update secret import
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateSecretImport = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Update secret import'
|
||||
#swagger.description = 'Update secret import'
|
||||
|
||||
#swagger.parameters['id'] = {
|
||||
in: 'path',
|
||||
description: 'ID of secret import to update',
|
||||
required: true,
|
||||
type: 'string',
|
||||
example: 'import12345'
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secretImports": {
|
||||
"type": "array",
|
||||
"description": "List of secret imports to update to",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment to import from",
|
||||
"example": "dev"
|
||||
},
|
||||
"secretPath": {
|
||||
"type": "string",
|
||||
"description": "Path where to import secrets from like / or /foo/bar",
|
||||
"example": "/foo/bar"
|
||||
}
|
||||
},
|
||||
"required": ["environment", "secretPath"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["secretImports"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
description: 'Successfully updated the secret import',
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "successfully updated secret import"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[400] = {
|
||||
description: 'Bad Request - Import not found',
|
||||
}
|
||||
|
||||
#swagger.responses[403] = {
|
||||
description: 'Forbidden access due to insufficient permissions',
|
||||
}
|
||||
|
||||
#swagger.responses[401] = {
|
||||
description: 'Unauthorized access due to invalid token or scope',
|
||||
}
|
||||
*/
|
||||
const {
|
||||
body: { secretImports },
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.UpdateSecretImportV1, req);
|
||||
|
||||
const importSecDoc = await SecretImport.findById(id);
|
||||
if (!importSecDoc) {
|
||||
throw BadRequestError({ message: "Import not found" });
|
||||
}
|
||||
|
||||
// check for service token validity
|
||||
const folders = await Folder.findOne({
|
||||
workspace: importSecDoc.workspace,
|
||||
environment: importSecDoc.environment
|
||||
}).lean();
|
||||
|
||||
let secretPath = "/";
|
||||
if (folders) {
|
||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
|
||||
secretPath = folderPath;
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// token permission check
|
||||
const isValidScopeAccess = isValidScope(
|
||||
req.authData.authPayload,
|
||||
importSecDoc.environment,
|
||||
secretPath
|
||||
);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
// non token entry check
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: importSecDoc.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: importSecDoc.environment,
|
||||
secretPath
|
||||
})
|
||||
);
|
||||
|
||||
secretImports.forEach(({ environment, secretPath }) => {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
const orderBefore = importSecDoc.imports;
|
||||
importSecDoc.imports = secretImports;
|
||||
|
||||
await importSecDoc.save();
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.UPDATE_SECRET_IMPORT,
|
||||
metadata: {
|
||||
importToEnvironment: importSecDoc.environment,
|
||||
importToSecretPath: secretPath,
|
||||
secretImportId: importSecDoc._id.toString(),
|
||||
folderId: importSecDoc.folderId.toString(),
|
||||
orderBefore,
|
||||
orderAfter: secretImports
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: importSecDoc.workspace
|
||||
}
|
||||
);
|
||||
return res.status(200).json({ message: "successfully updated secret import" });
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete secret import
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteSecretImport = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Delete secret import'
|
||||
#swagger.description = 'Delete secret import'
|
||||
|
||||
#swagger.parameters['id'] = {
|
||||
in: 'path',
|
||||
description: 'ID of parent secret import document from which to delete secret import',
|
||||
required: true,
|
||||
type: 'string',
|
||||
example: '12345abcde'
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secretImportEnv": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment of import to delete",
|
||||
"example": "someWorkspaceId"
|
||||
},
|
||||
"secretImportPath": {
|
||||
"type": "string",
|
||||
"description": "Path like / or /foo/bar of import to delete",
|
||||
"example": "production"
|
||||
}
|
||||
},
|
||||
"required": ["id", "secretImportEnv", "secretImportPath"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "successfully delete secret import"
|
||||
}
|
||||
},
|
||||
"description": "Confirmation of secret import deletion"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { id },
|
||||
body: { secretImportEnv, secretImportPath }
|
||||
} = await validateRequest(reqValidator.DeleteSecretImportV1, req);
|
||||
|
||||
const importSecDoc = await SecretImport.findById(id);
|
||||
if (!importSecDoc) {
|
||||
throw BadRequestError({ message: "Import not found" });
|
||||
}
|
||||
|
||||
// check for service token validity
|
||||
const folders = await Folder.findOne({
|
||||
workspace: importSecDoc.workspace,
|
||||
environment: importSecDoc.environment
|
||||
}).lean();
|
||||
|
||||
let secretPath = "/";
|
||||
if (folders) {
|
||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
|
||||
secretPath = folderPath;
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(
|
||||
req.authData.authPayload,
|
||||
importSecDoc.environment,
|
||||
secretPath
|
||||
);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: importSecDoc.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: importSecDoc.environment,
|
||||
secretPath
|
||||
})
|
||||
);
|
||||
}
|
||||
importSecDoc.imports = importSecDoc.imports.filter(
|
||||
({ environment, secretPath }) =>
|
||||
!(environment === secretImportEnv && secretPath === secretImportPath)
|
||||
);
|
||||
await importSecDoc.save();
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.DELETE_SECRET_IMPORT,
|
||||
metadata: {
|
||||
secretImportId: importSecDoc._id.toString(),
|
||||
folderId: importSecDoc.folderId.toString(),
|
||||
importFromEnvironment: secretImportEnv,
|
||||
importFromSecretPath: secretImportPath,
|
||||
importToEnvironment: importSecDoc.environment,
|
||||
importToSecretPath: secretPath
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: importSecDoc.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).json({ message: "successfully delete secret import" });
|
||||
};
|
||||
|
||||
/**
|
||||
* Get secret imports
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getSecretImports = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Get secret imports'
|
||||
#swagger.description = 'Get secret imports'
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
in: 'query',
|
||||
description: 'ID of workspace where to get secret imports from',
|
||||
required: true,
|
||||
type: 'string',
|
||||
example: 'workspace12345'
|
||||
}
|
||||
|
||||
#swagger.parameters['environment'] = {
|
||||
in: 'query',
|
||||
description: 'Slug of environment where to get secret imports from',
|
||||
required: true,
|
||||
type: 'string',
|
||||
example: 'production'
|
||||
}
|
||||
|
||||
#swagger.parameters['directory'] = {
|
||||
in: 'query',
|
||||
description: 'Path where to get secret imports from like / or /foo/bar. Default is /',
|
||||
required: false,
|
||||
type: 'string',
|
||||
example: 'folder12345'
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
description: 'Successfully retrieved secret import',
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secretImport": {
|
||||
$ref: '#/definitions/SecretImport'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[403] = {
|
||||
description: 'Forbidden access due to insufficient permissions',
|
||||
}
|
||||
|
||||
#swagger.responses[401] = {
|
||||
description: 'Unauthorized access due to invalid token or scope',
|
||||
}
|
||||
*/
|
||||
const {
|
||||
query: { workspaceId, environment, directory }
|
||||
} = await validateRequest(reqValidator.GetSecretImportsV1, req);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath: directory
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
let folderId = "root";
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
if (!importSecDoc) {
|
||||
return res.status(200).json({ secretImport: {} });
|
||||
}
|
||||
|
||||
return res.status(200).json({ secretImport: importSecDoc });
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all secret imports
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getAllSecretsFromImport = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId, environment, directory }
|
||||
} = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// check for service token validity
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath: directory
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
let folderId = "root";
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
if (!importSecDoc) {
|
||||
return res.status(200).json({ secrets: [] });
|
||||
}
|
||||
|
||||
let secretPath = "/";
|
||||
if (folders) {
|
||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
|
||||
secretPath = folderPath;
|
||||
}
|
||||
|
||||
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// check for service token validity
|
||||
const isValidScopeAccess = isValidScope(
|
||||
req.authData.authPayload,
|
||||
importSecDoc.environment,
|
||||
secretPath
|
||||
);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
permissionCheckFn = (env: string, secPath: string) =>
|
||||
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: importSecDoc.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: importSecDoc.environment,
|
||||
secretPath
|
||||
})
|
||||
);
|
||||
permissionCheckFn = (env: string, secPath: string) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: env,
|
||||
secretPath: secPath
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.GET_SECRET_IMPORTS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretImportId: importSecDoc._id.toString(),
|
||||
folderId,
|
||||
numberOfImports: importSecDoc.imports.length
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: importSecDoc.workspace
|
||||
}
|
||||
);
|
||||
|
||||
const secrets = await getAllImportedSecrets(
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId,
|
||||
permissionCheckFn
|
||||
);
|
||||
return res.status(200).json({ secrets });
|
||||
};
|
193
backend-mongo/src/controllers/v1/secretScanningController.ts
Normal file
193
backend-mongo/src/controllers/v1/secretScanningController.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import { Request, Response } from "express";
|
||||
import {
|
||||
GitAppInstallationSession,
|
||||
GitAppOrganizationInstallation,
|
||||
GitRisks
|
||||
} from "../../ee/models";
|
||||
import crypto from "crypto";
|
||||
import { Types } from "mongoose";
|
||||
import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository";
|
||||
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
|
||||
import {
|
||||
STATUS_RESOLVED_FALSE_POSITIVE,
|
||||
STATUS_RESOLVED_NOT_REVOKED,
|
||||
STATUS_RESOLVED_REVOKED
|
||||
} from "../../ee/models/gitRisks";
|
||||
import { ProbotOctokit } from "probot";
|
||||
import { Organization } from "../../models";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/secretScanning";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
getAuthDataOrgPermissions
|
||||
} from "../../ee/services/RoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
export const createInstallationSession = async (req: Request, res: Response) => {
|
||||
const sessionId = crypto.randomBytes(16).toString("hex");
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.CreateInstalLSessionv1, req);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.SecretScanning
|
||||
);
|
||||
|
||||
await GitAppInstallationSession.findByIdAndUpdate(
|
||||
organization,
|
||||
{
|
||||
organization: organization.id,
|
||||
sessionId: sessionId,
|
||||
user: new Types.ObjectId(req.user._id)
|
||||
},
|
||||
{ upsert: true }
|
||||
).lean();
|
||||
|
||||
res.send({
|
||||
sessionId: sessionId
|
||||
});
|
||||
};
|
||||
|
||||
export const linkInstallationToOrganization = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { sessionId, installationId }
|
||||
} = await validateRequest(reqValidator.LinkInstallationToOrgv1, req);
|
||||
|
||||
const installationSession = await GitAppInstallationSession.findOneAndDelete({
|
||||
sessionId: sessionId
|
||||
});
|
||||
if (!installationSession) {
|
||||
throw UnauthorizedRequestError();
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: installationSession.organization
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.SecretScanning
|
||||
);
|
||||
|
||||
const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate(
|
||||
{
|
||||
organizationId: installationSession.organization
|
||||
},
|
||||
{
|
||||
installationId: installationId,
|
||||
organizationId: installationSession.organization,
|
||||
user: installationSession.user
|
||||
},
|
||||
{
|
||||
upsert: true
|
||||
}
|
||||
).lean();
|
||||
|
||||
const octokit = new ProbotOctokit({
|
||||
auth: {
|
||||
appId: await getSecretScanningGitAppId(),
|
||||
privateKey: await getSecretScanningPrivateKey(),
|
||||
installationId: installationId.toString()
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
data: { repositories }
|
||||
} = await octokit.apps.listReposAccessibleToInstallation();
|
||||
for (const repository of repositories) {
|
||||
scanGithubFullRepoForSecretLeaks({
|
||||
organizationId: installationSession.organization.toString(),
|
||||
installationId,
|
||||
repository: { id: repository.id, fullName: repository.full_name }
|
||||
});
|
||||
}
|
||||
res.json(installationLink);
|
||||
};
|
||||
|
||||
export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => {
|
||||
const { organizationId } = req.params;
|
||||
try {
|
||||
const appInstallation = await GitAppOrganizationInstallation.findOne({
|
||||
organizationId: organizationId
|
||||
}).lean();
|
||||
if (!appInstallation) {
|
||||
res.json({
|
||||
appInstallationComplete: false
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
appInstallationComplete: true
|
||||
});
|
||||
} catch {
|
||||
res.json({
|
||||
appInstallationComplete: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getRisksForOrganization = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgRisksv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.SecretScanning
|
||||
);
|
||||
|
||||
const risks = await GitRisks.find({ organization: organizationId })
|
||||
.sort({ createdAt: -1 })
|
||||
.lean();
|
||||
res.json({
|
||||
risks: risks
|
||||
});
|
||||
};
|
||||
|
||||
export const updateRisksStatus = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId, riskId },
|
||||
body: { status }
|
||||
} = await validateRequest(reqValidator.UpdateRiskStatusv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.SecretScanning
|
||||
);
|
||||
|
||||
const isRiskResolved =
|
||||
status == STATUS_RESOLVED_FALSE_POSITIVE ||
|
||||
status == STATUS_RESOLVED_REVOKED ||
|
||||
status == STATUS_RESOLVED_NOT_REVOKED
|
||||
? true
|
||||
: false;
|
||||
const risk = await GitRisks.findByIdAndUpdate(riskId, {
|
||||
status: status,
|
||||
isResolved: isRiskResolved
|
||||
}).lean();
|
||||
|
||||
res.json(risk);
|
||||
};
|
680
backend-mongo/src/controllers/v1/secretsFolderController.ts
Normal file
680
backend-mongo/src/controllers/v1/secretsFolderController.ts
Normal file
@ -0,0 +1,680 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { EventType, FolderVersion } from "../../ee/models";
|
||||
import { EEAuditLogService, EESecretService } from "../../ee/services";
|
||||
import { isValidScope } from "../../helpers/secrets";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { Secret, ServiceTokenData } from "../../models";
|
||||
import { Folder } from "../../models/folder";
|
||||
import {
|
||||
appendFolder,
|
||||
getAllFolderIds,
|
||||
getFolderByPath,
|
||||
getFolderWithPathFromId,
|
||||
validateFolderName
|
||||
} from "../../services/FolderService";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import * as reqValidator from "../../validation/folders";
|
||||
|
||||
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "The folder doesn't exist" });
|
||||
|
||||
// verify workspace id/environment
|
||||
export const createFolder = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Create folder'
|
||||
#swagger.description = 'Create folder'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaceId": {
|
||||
"type": "string",
|
||||
"description": "ID of the workspace where to create folder",
|
||||
"example": "someWorkspaceId"
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment where to create folder",
|
||||
"example": "production"
|
||||
},
|
||||
"folderName": {
|
||||
"type": "string",
|
||||
"description": "Name of folder to create",
|
||||
"example": "my_folder"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "Path where to create folder like / or /foo/bar. Default is /",
|
||||
"example": "/foo/bar"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "environment", "folderName"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"folder": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of folder",
|
||||
"example": "someFolderId"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of folder",
|
||||
"example": "my_folder"
|
||||
},
|
||||
"version": {
|
||||
"type": "number",
|
||||
"description": "Version of folder",
|
||||
"example": 1
|
||||
}
|
||||
},
|
||||
"description": "Details of created folder"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[400] = {
|
||||
description: "Bad Request. For example, 'Folder name cannot contain spaces. Only underscore and dashes'"
|
||||
}
|
||||
#swagger.responses[401] = {
|
||||
description: "Unauthorized request. For example, 'Folder Permission Denied'"
|
||||
}
|
||||
*/
|
||||
const {
|
||||
body: { workspaceId, environment, folderName, directory }
|
||||
} = await validateRequest(reqValidator.CreateFolderV1, req);
|
||||
|
||||
if (!validateFolderName(folderName)) {
|
||||
throw BadRequestError({
|
||||
message: "Folder name cannot contain spaces. Only underscore and dashes"
|
||||
});
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// token check
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
// user check
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
|
||||
// space has no folders initialized
|
||||
if (!folders) {
|
||||
const folder = new Folder({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: {
|
||||
id: "root",
|
||||
name: "root",
|
||||
version: 1,
|
||||
children: []
|
||||
}
|
||||
});
|
||||
const { parent, child } = appendFolder(folder.nodes, { folderName, directory });
|
||||
await folder.save();
|
||||
const folderVersion = new FolderVersion({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: parent
|
||||
});
|
||||
await folderVersion.save();
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_FOLDER,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId: child.id,
|
||||
folderName,
|
||||
folderPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.json({ folder: { id: child.id, name: folderName } });
|
||||
}
|
||||
|
||||
const { parent, child, hasCreated } = appendFolder(folders.nodes, { folderName, directory });
|
||||
|
||||
if (!hasCreated) return res.json({ folder: child });
|
||||
|
||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||
|
||||
const folderVersion = new FolderVersion({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: parent
|
||||
});
|
||||
await folderVersion.save();
|
||||
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folderId: child.id
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_FOLDER,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId: child.id,
|
||||
folderName,
|
||||
folderPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.json({ folder: child });
|
||||
};
|
||||
|
||||
/**
|
||||
* Update folder with id [folderId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateFolderById = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Update folder'
|
||||
#swagger.description = 'Update folder'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['folderName'] = {
|
||||
"description": "Name of folder to update",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaceId": {
|
||||
"type": "string",
|
||||
"description": "ID of workspace where to update folder",
|
||||
"example": "someWorkspaceId"
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment where to update folder",
|
||||
"example": "production"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of folder to update to",
|
||||
"example": "updated_folder_name"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "Path where to update folder like / or /foo/bar. Default is /",
|
||||
"example": "/foo/bar"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "environment", "name"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Success message",
|
||||
"example": "Successfully updated folder"
|
||||
},
|
||||
"folder": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of updated folder",
|
||||
"example": "updated_folder_name"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of created folder",
|
||||
"example": "abc123"
|
||||
}
|
||||
},
|
||||
"description": "Details of the updated folder"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[400] = {
|
||||
description: "Bad Request. Reasons can include 'The folder doesn't exist' or 'Folder name cannot contain spaces. Only underscore and dashes'"
|
||||
}
|
||||
|
||||
#swagger.responses[401] = {
|
||||
description: "Unauthorized request. For example, 'Folder Permission Denied'"
|
||||
}
|
||||
*/
|
||||
const {
|
||||
body: { workspaceId, environment, name, directory },
|
||||
params: { folderName }
|
||||
} = await validateRequest(reqValidator.UpdateFolderV1, req);
|
||||
|
||||
if (!validateFolderName(name)) {
|
||||
throw BadRequestError({
|
||||
message: "Folder name cannot contain spaces. Only underscore and dashes"
|
||||
});
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
const parentFolder = getFolderByPath(folders.nodes, directory);
|
||||
if (!parentFolder) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
const folder = parentFolder.children.find(({ name }) => name === folderName);
|
||||
if (!folder) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const oldFolderName = folder.name;
|
||||
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: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folderId: parentFolder.id
|
||||
});
|
||||
|
||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.UPDATE_FOLDER,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId: folder.id,
|
||||
oldFolderName,
|
||||
newFolderName: name,
|
||||
folderPath
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.json({
|
||||
message: "Successfully updated folder",
|
||||
folder: { name: folder.name, id: folder.id }
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete folder with id [folderId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteFolder = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Delete folder'
|
||||
#swagger.description = 'Delete folder'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['folderName'] = {
|
||||
"description": "Name of folder to delete",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaceId": {
|
||||
"type": "string",
|
||||
"description": "ID of the workspace where to delete folder",
|
||||
"example": "someWorkspaceId"
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment where to delete folder",
|
||||
"example": "production"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "Path where to delete folder like / or /foo/bar. Default is /",
|
||||
"example": "/foo/bar"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "environment"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Success message",
|
||||
"example": "successfully deleted folders"
|
||||
},
|
||||
"folders": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of deleted folder",
|
||||
"example": "abc123"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of deleted folder",
|
||||
"example": "someFolderName"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "List of IDs and names of deleted folders"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[400] = {
|
||||
description: "Bad Request. Reasons can include 'The folder doesn't exist'"
|
||||
}
|
||||
|
||||
#swagger.responses[401] = {
|
||||
description: "Unauthorized request. For example, 'Folder Permission Denied'"
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { folderName },
|
||||
body: { environment, workspaceId, directory }
|
||||
} = await validateRequest(reqValidator.DeleteFolderV1, req);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
// check that user is a member of the workspace
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const parentFolder = getFolderByPath(folders.nodes, directory);
|
||||
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const index = parentFolder.children.findIndex(({ name }) => name === folderName);
|
||||
if (index === -1) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const deletedFolder = parentFolder.children.splice(index, 1)[0];
|
||||
|
||||
parentFolder.version += 1;
|
||||
const delFolderIds = getAllFolderIds(deletedFolder);
|
||||
|
||||
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: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folderId: parentFolder.id
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.DELETE_FOLDER,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId: deletedFolder.id,
|
||||
folderName: deletedFolder.name,
|
||||
folderPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.send({ message: "successfully deleted folders", folders: delFolderIds });
|
||||
};
|
||||
|
||||
/**
|
||||
* Get folders for workspace with id [workspaceId] and environment [environment]
|
||||
* considering directory/path [directory]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getFolders = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Get folders'
|
||||
#swagger.description = 'Get folders'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of the workspace where to get folders from",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "query"
|
||||
}
|
||||
|
||||
#swagger.parameters['environment'] = {
|
||||
"description": "Slug of environment where to get folders from",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "query"
|
||||
}
|
||||
|
||||
#swagger.parameters['directory'] = {
|
||||
"description": "Path where to get fodlers from like / or /foo/bar. Default is /",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"in": "query"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"folders": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"example": "someFolderId"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "someFolderName"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "List of folders"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[400] = {
|
||||
description: "Bad Request. For instance, 'The folder doesn't exist'"
|
||||
}
|
||||
|
||||
#swagger.responses[401] = {
|
||||
description: "Unauthorized request. For example, 'Folder Permission Denied'"
|
||||
}
|
||||
*/
|
||||
const {
|
||||
query: { workspaceId, environment, directory }
|
||||
} = await validateRequest(reqValidator.GetFoldersV1, req);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
// check that user is a member of the workspace
|
||||
await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) {
|
||||
return res.send({ folders: [], dir: [] });
|
||||
}
|
||||
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
|
||||
return res.send({
|
||||
folders: folder?.children?.map(({ id, name }) => ({ id, name })) || []
|
||||
});
|
||||
};
|
75
backend-mongo/src/controllers/v1/serviceTokenController.ts
Normal file
75
backend-mongo/src/controllers/v1/serviceTokenController.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { Request, Response } from "express";
|
||||
import { ServiceToken } from "../../models";
|
||||
import { createToken } from "../../helpers/auth";
|
||||
import { getJwtServiceSecret } from "../../config";
|
||||
|
||||
/**
|
||||
* Return service token on request
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getServiceToken = async (req: Request, res: Response) => {
|
||||
return res.status(200).send({
|
||||
serviceToken: req.serviceToken,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create and return a new service token
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createServiceToken = async (req: Request, res: Response) => {
|
||||
let token;
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
workspaceId,
|
||||
environment,
|
||||
expiresIn,
|
||||
publicKey,
|
||||
encryptedKey,
|
||||
nonce,
|
||||
} = req.body;
|
||||
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error("Failed to validate environment");
|
||||
}
|
||||
|
||||
// compute access token expiration date
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
|
||||
const serviceToken = await new ServiceToken({
|
||||
name,
|
||||
user: req.user._id,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
expiresAt,
|
||||
publicKey,
|
||||
encryptedKey,
|
||||
nonce,
|
||||
}).save();
|
||||
|
||||
token = createToken({
|
||||
payload: {
|
||||
serviceTokenId: serviceToken._id.toString(),
|
||||
workspaceId,
|
||||
},
|
||||
expiresIn: expiresIn,
|
||||
secret: await getJwtServiceSecret(),
|
||||
});
|
||||
} catch (err) {
|
||||
return res.status(400).send({
|
||||
message: "Failed to create service token",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
token,
|
||||
});
|
||||
};
|
99
backend-mongo/src/controllers/v1/signupController.ts
Normal file
99
backend-mongo/src/controllers/v1/signupController.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { Request, Response } from "express";
|
||||
import { AuthMethod, User } from "../../models";
|
||||
import { checkEmailVerification, sendEmailVerification } from "../../helpers/signup";
|
||||
import { createToken } from "../../helpers/auth";
|
||||
import {
|
||||
getAuthSecret,
|
||||
getJwtSignupLifetime,
|
||||
getSmtpConfigured
|
||||
} from "../../config";
|
||||
import { validateUserEmail } from "../../validation";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/auth";
|
||||
import { AuthTokenType } from "../../variables";
|
||||
|
||||
/**
|
||||
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
||||
* to that email
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const beginEmailSignup = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { email }
|
||||
} = await validateRequest(reqValidator.BeginEmailSignUpV1, req);
|
||||
|
||||
// validate that email is not disposable
|
||||
validateUserEmail(email);
|
||||
|
||||
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"
|
||||
});
|
||||
}
|
||||
|
||||
// send send verification email
|
||||
await sendEmailVerification({ email });
|
||||
|
||||
return res.status(200).send({
|
||||
message: `Sent an email verification code to ${email}`
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Signup step 2: Verify that code [code] was sent to email [email] and issue
|
||||
* a temporary signup token for user to complete setting up their account
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const verifyEmailSignup = async (req: Request, res: Response) => {
|
||||
let user;
|
||||
const {
|
||||
body: { email, code }
|
||||
} = await validateRequest(reqValidator.VerifyEmailSignUpV1, req);
|
||||
|
||||
// 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 (await getSmtpConfigured()) {
|
||||
await checkEmailVerification({
|
||||
email,
|
||||
code
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
user = await new User({
|
||||
email,
|
||||
authMethods: [AuthMethod.EMAIL]
|
||||
}).save();
|
||||
}
|
||||
|
||||
// generate temporary signup token
|
||||
const token = createToken({
|
||||
payload: {
|
||||
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: await getJwtSignupLifetime(),
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfuly verified email",
|
||||
user,
|
||||
token
|
||||
});
|
||||
};
|
1269
backend-mongo/src/controllers/v1/universalAuthController.ts
Normal file
1269
backend-mongo/src/controllers/v1/universalAuthController.ts
Normal file
File diff suppressed because it is too large
Load Diff
56
backend-mongo/src/controllers/v1/userActionController.ts
Normal file
56
backend-mongo/src/controllers/v1/userActionController.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { Request, Response } from "express";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { UserAction } from "../../models";
|
||||
import * as reqValidator from "../../validation/action";
|
||||
|
||||
/**
|
||||
* Add user action [action]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const addUserAction = async (req: Request, res: Response) => {
|
||||
// add/record new action [action] for user with id [req.user._id]
|
||||
const {
|
||||
body: { action }
|
||||
} = await validateRequest(reqValidator.AddUserActionV1, req);
|
||||
|
||||
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",
|
||||
userAction
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return user action [action] for user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getUserAction = async (req: Request, res: Response) => {
|
||||
// get user action [action] for user with id [req.user._id]
|
||||
const {
|
||||
query: { action }
|
||||
} = await validateRequest(reqValidator.GetUserActionV1, req);
|
||||
|
||||
const userAction = await UserAction.findOne({
|
||||
user: req.user._id,
|
||||
action
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
userAction
|
||||
});
|
||||
};
|
13
backend-mongo/src/controllers/v1/userController.ts
Normal file
13
backend-mongo/src/controllers/v1/userController.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Request, Response } from "express";
|
||||
|
||||
/**
|
||||
* Return user on request
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getUser = async (req: Request, res: Response) => {
|
||||
return res.status(200).send({
|
||||
user: req.user,
|
||||
});
|
||||
};
|
268
backend-mongo/src/controllers/v1/webhookController.ts
Normal file
268
backend-mongo/src/controllers/v1/webhookController.ts
Normal file
@ -0,0 +1,268 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
|
||||
import { Webhook } from "../../models";
|
||||
import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService";
|
||||
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import { EventType } from "../../ee/models";
|
||||
import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8
|
||||
} from "../../variables";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/webhooks";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
|
||||
|
||||
export const createWebhook = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath }
|
||||
} = await validateRequest(reqValidator.CreateWebhookV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Webhooks
|
||||
);
|
||||
|
||||
const webhook = new Webhook({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
url: webhookUrl
|
||||
});
|
||||
|
||||
if (webhookSecretKey) {
|
||||
const encryptionKey = await getEncryptionKey();
|
||||
const rootEncryptionKey = await getRootEncryptionKey();
|
||||
|
||||
if (rootEncryptionKey) {
|
||||
const { ciphertext, iv, tag } = client.encryptSymmetric(webhookSecretKey, rootEncryptionKey);
|
||||
webhook.iv = iv;
|
||||
webhook.tag = tag;
|
||||
webhook.encryptedSecretKey = ciphertext;
|
||||
webhook.algorithm = ALGORITHM_AES_256_GCM;
|
||||
webhook.keyEncoding = ENCODING_SCHEME_BASE64;
|
||||
} else if (encryptionKey) {
|
||||
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: webhookSecretKey,
|
||||
key: encryptionKey
|
||||
});
|
||||
webhook.iv = iv;
|
||||
webhook.tag = tag;
|
||||
webhook.encryptedSecretKey = ciphertext;
|
||||
webhook.algorithm = ALGORITHM_AES_256_GCM;
|
||||
webhook.keyEncoding = ENCODING_SCHEME_UTF8;
|
||||
}
|
||||
}
|
||||
|
||||
await webhook.save();
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_WEBHOOK,
|
||||
metadata: {
|
||||
webhookId: webhook._id.toString(),
|
||||
environment,
|
||||
secretPath,
|
||||
webhookUrl,
|
||||
isDisabled: false
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
webhook,
|
||||
message: "successfully created webhook"
|
||||
});
|
||||
};
|
||||
|
||||
export const updateWebhook = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { isDisabled },
|
||||
params: { webhookId }
|
||||
} = await validateRequest(reqValidator.UpdateWebhookV1, req);
|
||||
|
||||
const webhook = await Webhook.findById(webhookId);
|
||||
if (!webhook) {
|
||||
throw BadRequestError({ message: "Webhook not found!!" });
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: webhook.workspace
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Webhooks
|
||||
);
|
||||
|
||||
if (typeof isDisabled !== undefined) {
|
||||
webhook.isDisabled = isDisabled;
|
||||
}
|
||||
await webhook.save();
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.UPDATE_WEBHOOK_STATUS,
|
||||
metadata: {
|
||||
webhookId: webhook._id.toString(),
|
||||
environment: webhook.environment,
|
||||
secretPath: webhook.secretPath,
|
||||
webhookUrl: webhook.url,
|
||||
isDisabled
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: webhook.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
webhook,
|
||||
message: "successfully updated webhook"
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteWebhook = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { webhookId }
|
||||
} = await validateRequest(reqValidator.DeleteWebhookV1, req);
|
||||
let webhook = await Webhook.findById(webhookId);
|
||||
|
||||
if (!webhook) {
|
||||
throw ResourceNotFoundError({ message: "Webhook not found!!" });
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: webhook.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Webhooks
|
||||
);
|
||||
|
||||
webhook = await Webhook.findByIdAndDelete(webhookId);
|
||||
|
||||
if (!webhook) {
|
||||
throw ResourceNotFoundError({ message: "Webhook not found!!" });
|
||||
}
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.DELETE_WEBHOOK,
|
||||
metadata: {
|
||||
webhookId: webhook._id.toString(),
|
||||
environment: webhook.environment,
|
||||
secretPath: webhook.secretPath,
|
||||
webhookUrl: webhook.url,
|
||||
isDisabled: webhook.isDisabled
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: webhook.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "successfully removed webhook"
|
||||
});
|
||||
};
|
||||
|
||||
export const testWebhook = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { webhookId }
|
||||
} = await validateRequest(reqValidator.TestWebhookV1, req);
|
||||
|
||||
const webhook = await Webhook.findById(webhookId);
|
||||
if (!webhook) {
|
||||
throw BadRequestError({ message: "Webhook not found!!" });
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: webhook.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Webhooks
|
||||
);
|
||||
|
||||
try {
|
||||
await triggerWebhookRequest(
|
||||
webhook,
|
||||
getWebhookPayload(
|
||||
"test",
|
||||
webhook.workspace.toString(),
|
||||
webhook.environment,
|
||||
webhook.secretPath
|
||||
)
|
||||
);
|
||||
await Webhook.findByIdAndUpdate(webhookId, {
|
||||
lastStatus: "success",
|
||||
lastRunErrorMessage: null
|
||||
});
|
||||
} catch (err) {
|
||||
await Webhook.findByIdAndUpdate(webhookId, {
|
||||
lastStatus: "failed",
|
||||
lastRunErrorMessage: (err as Error).message
|
||||
});
|
||||
return res.status(400).send({
|
||||
message: "Failed to receive response",
|
||||
error: (err as Error).message
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully received response"
|
||||
});
|
||||
};
|
||||
|
||||
export const listWebhooks = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { environment, workspaceId, secretPath }
|
||||
} = await validateRequest(reqValidator.ListWebhooksV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Webhooks
|
||||
);
|
||||
|
||||
const optionalFilters: Record<string, string> = {};
|
||||
if (environment) optionalFilters.environment = environment as string;
|
||||
if (secretPath) optionalFilters.secretPath = secretPath as string;
|
||||
|
||||
const webhooks = await Webhook.find({
|
||||
workspace: new Types.ObjectId(workspaceId as string),
|
||||
...optionalFilters
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
webhooks
|
||||
});
|
||||
};
|
359
backend-mongo/src/controllers/v1/workspaceController.ts
Normal file
359
backend-mongo/src/controllers/v1/workspaceController.ts
Normal file
@ -0,0 +1,359 @@
|
||||
import { Types } from "mongoose";
|
||||
import { Request, Response } from "express";
|
||||
import {
|
||||
IUser,
|
||||
Integration,
|
||||
IntegrationAuth,
|
||||
Membership,
|
||||
Organization,
|
||||
ServiceToken,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { createWorkspace as create, deleteWorkspace as deleteWork } from "../../helpers/workspace";
|
||||
import { EELicenseService } from "../../ee/services";
|
||||
import { addMemberships } from "../../helpers/membership";
|
||||
import { ADMIN } from "../../variables";
|
||||
import { OrganizationNotFoundError } from "../../utils/errors";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
getAuthDataOrgPermissions
|
||||
} from "../../ee/services/RoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
|
||||
/**
|
||||
* Return public keys of members of workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspacePublicKeysV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
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
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return memberships for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
const users = await Membership.find({
|
||||
workspace: workspaceId
|
||||
}).populate("user", "+publicKey");
|
||||
|
||||
return res.status(200).send({
|
||||
users
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return workspaces that user is part of
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaces = async (req: Request, res: Response) => {
|
||||
const workspaces = (
|
||||
await Membership.find({
|
||||
user: req.user._id
|
||||
}).populate("workspace")
|
||||
).map((m) => m.workspace);
|
||||
|
||||
return res.status(200).send({
|
||||
workspaces
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspace = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceV1, req);
|
||||
|
||||
const workspace = await Workspace.findOne({
|
||||
_id: workspaceId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new workspace named [workspaceName] under organization with id
|
||||
* [organizationId] and add user as admin
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createWorkspace = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { organizationId, workspaceName }
|
||||
} = await validateRequest(reqValidator.CreateWorkspaceV1, req);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Workspace
|
||||
);
|
||||
|
||||
const plan = await EELicenseService.getPlan(new Types.ObjectId(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: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
await addMemberships({
|
||||
userIds: [req.user._id],
|
||||
workspaceId: workspace._id.toString(),
|
||||
roles: [ADMIN]
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteWorkspace = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.DeleteWorkspaceV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Workspace
|
||||
);
|
||||
|
||||
// delete workspace
|
||||
const workspace = await deleteWork({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change name of workspace with id [workspaceId] to [name]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const changeWorkspaceName = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { name }
|
||||
} = await validateRequest(reqValidator.ChangeWorkspaceNameV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Workspace
|
||||
);
|
||||
|
||||
const workspace = await Workspace.findOneAndUpdate(
|
||||
{
|
||||
_id: workspaceId
|
||||
},
|
||||
{
|
||||
name
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully changed workspace name",
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return integrations for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceIntegrationsV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
const integrations = await Integration.find({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
integrations
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return (integration) authorizations for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceIntegrationAuthorizationsV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
const authorizations = await IntegrationAuth.find({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
authorizations
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return service service tokens for workspace [workspaceId] belonging to user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceServiceTokens = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceServiceTokensV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
// ?? FIX.
|
||||
const serviceTokens = await ServiceToken.find({
|
||||
user: req.user._id,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokens
|
||||
});
|
||||
};
|
315
backend-mongo/src/controllers/v2/authController.ts
Normal file
315
backend-mongo/src/controllers/v2/authController.ts
Normal file
@ -0,0 +1,315 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { Request, Response } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import * as bigintConversion from "bigint-conversion";
|
||||
const jsrp = require("jsrp");
|
||||
import { LoginSRPDetail, User } from "../../models";
|
||||
import { createToken, issueAuthTokens } from "../../helpers/auth";
|
||||
import { checkUserDevice } from "../../helpers/user";
|
||||
import { sendMail } from "../../helpers/nodemailer";
|
||||
import { TokenService } from "../../services";
|
||||
import { BadRequestError, InternalServerError } from "../../utils/errors";
|
||||
import { AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables";
|
||||
import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/auth";
|
||||
|
||||
declare module "jsonwebtoken" {
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
userId: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body;
|
||||
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select("+salt +verifier");
|
||||
|
||||
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();
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
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"
|
||||
);
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
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: {
|
||||
authTokenType: AuthTokenType.MFA_TOKEN,
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: await getJwtMfaLifetime(),
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
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.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;
|
||||
}
|
||||
|
||||
return res.status(200).send(response);
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: "Failed to authenticate. Try again?"
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Send MFA token to email [email]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const sendMfaToken = async (req: Request, res: Response) => {
|
||||
const code = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email: req.user.email
|
||||
});
|
||||
|
||||
// send MFA code [code] to [email]
|
||||
await sendMail({
|
||||
template: "emailMfa.handlebars",
|
||||
subjectLine: "Infisical MFA code",
|
||||
recipients: [req.user.email],
|
||||
substitutions: {
|
||||
code
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully sent new MFA code"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
|
||||
* MFA token [mfaToken] is valid
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const verifyMfaToken = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { mfaToken }
|
||||
} = await validateRequest(reqValidator.VerifyMfaTokenV2, req);
|
||||
|
||||
await TokenService.validateToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email: req.user.email,
|
||||
token: mfaToken
|
||||
});
|
||||
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).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.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()
|
||||
});
|
||||
|
||||
interface VerifyMfaTokenRes {
|
||||
encryptionVersion: number;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
protectedKeyTag?: string;
|
||||
token: string;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
interface VerifyMfaTokenRes {
|
||||
encryptionVersion: number;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
protectedKeyTag?: string;
|
||||
token: string;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
const resObj: VerifyMfaTokenRes = {
|
||||
encryptionVersion: user.encryptionVersion,
|
||||
token: tokens.token,
|
||||
publicKey: user.publicKey as string,
|
||||
encryptedPrivateKey: user.encryptedPrivateKey as string,
|
||||
iv: user.iv as string,
|
||||
tag: user.tag as string
|
||||
};
|
||||
|
||||
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
|
||||
resObj.protectedKey = user.protectedKey;
|
||||
resObj.protectedKeyIV = user.protectedKeyIV;
|
||||
resObj.protectedKeyTag = user.protectedKeyTag;
|
||||
}
|
||||
|
||||
return res.status(200).send(resObj);
|
||||
};
|
604
backend-mongo/src/controllers/v2/environmentController.ts
Normal file
604
backend-mongo/src/controllers/v2/environmentController.ts
Normal file
@ -0,0 +1,604 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
Folder,
|
||||
Integration,
|
||||
Membership,
|
||||
Secret,
|
||||
ServiceToken,
|
||||
ServiceTokenData,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { EventType, SecretVersion } from "../../ee/models";
|
||||
import { EEAuditLogService, EELicenseService } from "../../ee/services";
|
||||
import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/environments";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { SecretImport } from "../../models";
|
||||
import { Webhook } from "../../models";
|
||||
|
||||
/**
|
||||
* Create new workspace environment named [environmentName]
|
||||
* with slug [environmentSlug] under workspace with id
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createWorkspaceEnvironment = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Create environment'
|
||||
#swagger.description = 'Create environment'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of workspace where to create environment",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"environmentName": {
|
||||
"type": "string",
|
||||
"description": "Name of the environment to create",
|
||||
"example": "development"
|
||||
},
|
||||
"environmentSlug": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment to create",
|
||||
"example": "dev-environment"
|
||||
}
|
||||
},
|
||||
"required": ["environmentName", "environmentSlug"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Sucess message",
|
||||
"example": "Successfully created environment"
|
||||
},
|
||||
"workspace": {
|
||||
"type": "string",
|
||||
"description": "ID of workspace where environment was created",
|
||||
"example": "abc123"
|
||||
},
|
||||
"environment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of created environment",
|
||||
"example": "Staging"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"description": "Slug of created environment",
|
||||
"example": "staging"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Details of the created environment"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { environmentName, environmentSlug }
|
||||
} = await validateRequest(reqValidator.CreateWorkspaceEnvironmentV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Environments
|
||||
);
|
||||
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
|
||||
if (!workspace) throw WorkspaceNotFoundError();
|
||||
|
||||
const plan = await EELicenseService.getPlan(workspace.organization);
|
||||
|
||||
if (plan.environmentLimit !== null) {
|
||||
// case: limit imposed on number of environments allowed
|
||||
if (workspace.environments.length >= plan.environmentLimit) {
|
||||
// case: number of environments used exceeds the number of environments allowed
|
||||
|
||||
return res.status(400).send({
|
||||
message:
|
||||
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId));
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_ENVIRONMENT,
|
||||
metadata: {
|
||||
name: environmentName,
|
||||
slug: environmentSlug
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: workspace._id
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully created new environment",
|
||||
workspace: workspaceId,
|
||||
environment: {
|
||||
name: environmentName,
|
||||
slug: environmentSlug
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Swaps the ordering of two environments in the database. This is purely for aesthetic purposes.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const reorderWorkspaceEnvironments = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { environmentName, environmentSlug, otherEnvironmentSlug, otherEnvironmentName }
|
||||
} = await validateRequest(reqValidator.ReorderWorkspaceEnvironmentsV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Environments
|
||||
);
|
||||
|
||||
// atomic update the env to avoid conflict
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
if (!workspace) {
|
||||
throw BadRequestError({ message: "Couldn't load workspace" });
|
||||
}
|
||||
|
||||
const environmentIndex = workspace.environments.findIndex(
|
||||
(env) => env.name === environmentName && env.slug === environmentSlug
|
||||
);
|
||||
const otherEnvironmentIndex = workspace.environments.findIndex(
|
||||
(env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug
|
||||
);
|
||||
|
||||
if (environmentIndex === -1 || otherEnvironmentIndex === -1) {
|
||||
throw BadRequestError({ message: "environment or otherEnvironment couldn't be found" });
|
||||
}
|
||||
|
||||
// swap the order of the environments
|
||||
[workspace.environments[environmentIndex], workspace.environments[otherEnvironmentIndex]] = [
|
||||
workspace.environments[otherEnvironmentIndex],
|
||||
workspace.environments[environmentIndex]
|
||||
];
|
||||
|
||||
await workspace.save();
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully reordered environments",
|
||||
workspace: workspaceId
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Rename workspace environment with new name and slug of a workspace with [workspaceId]
|
||||
* Old slug [oldEnvironmentSlug] must be provided
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const renameWorkspaceEnvironment = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Update environment'
|
||||
#swagger.description = 'Update environment'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of workspace where to update environment",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"environmentName": {
|
||||
"type": "string",
|
||||
"description": "Name of environment to update to",
|
||||
"example": "Staging-Renamed"
|
||||
},
|
||||
"environmentSlug": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment to update to",
|
||||
"example": "staging-renamed"
|
||||
},
|
||||
"oldEnvironmentSlug": {
|
||||
"type": "string",
|
||||
"description": "Current slug of environment",
|
||||
"example": "staging-old"
|
||||
}
|
||||
},
|
||||
"required": ["environmentName", "environmentSlug", "oldEnvironmentSlug"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Success message",
|
||||
"example": "Successfully update environment"
|
||||
},
|
||||
"workspace": {
|
||||
"type": "string",
|
||||
"description": "ID of workspace where environment was updated",
|
||||
"example": "abc123"
|
||||
},
|
||||
"environment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of updated environment",
|
||||
"example": "Staging-Renamed"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"description": "Slug of updated environment",
|
||||
"example": "staging-renamed"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Details of the renamed environment"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { environmentName, environmentSlug, oldEnvironmentSlug }
|
||||
} = await validateRequest(reqValidator.UpdateWorkspaceEnvironmentV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Environments
|
||||
);
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
const oldEnvironment = workspace.environments[envIndex];
|
||||
|
||||
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,
|
||||
"scopes.environment": oldEnvironmentSlug
|
||||
},
|
||||
{ $set: { "scopes.$[element].environment": environmentSlug } },
|
||||
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] }
|
||||
);
|
||||
await Integration.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await Folder.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await SecretImport.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await SecretImport.updateMany(
|
||||
{ workspace: workspaceId, "imports.environment": oldEnvironmentSlug },
|
||||
{ $set: { "imports.$[element].environment": environmentSlug } },
|
||||
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] },
|
||||
);
|
||||
|
||||
await Webhook.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await Membership.updateMany(
|
||||
{
|
||||
workspace: workspaceId,
|
||||
"deniedPermissions.environmentSlug": oldEnvironmentSlug
|
||||
},
|
||||
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
|
||||
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
|
||||
);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.UPDATE_ENVIRONMENT,
|
||||
metadata: {
|
||||
oldName: oldEnvironment.name,
|
||||
newName: environmentName,
|
||||
oldSlug: oldEnvironment.slug,
|
||||
newSlug: environmentSlug.toLowerCase()
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: workspace._id
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully update environment",
|
||||
workspace: workspaceId,
|
||||
environment: {
|
||||
name: environmentName,
|
||||
slug: environmentSlug
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete workspace environment by [environmentSlug] of workspace [workspaceId] and do the clean up
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Delete environment'
|
||||
#swagger.description = 'Delete environment'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of workspace where to delete environment",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"environmentSlug": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment to delete",
|
||||
"example": "dev"
|
||||
}
|
||||
},
|
||||
"required": ["environmentSlug"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Success message",
|
||||
"example": "Successfully deleted environment"
|
||||
},
|
||||
"workspace": {
|
||||
"type": "string",
|
||||
"description": "ID of workspace where environment was deleted",
|
||||
"example": "abc123"
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Slug of deleted environment",
|
||||
"example": "dev"
|
||||
}
|
||||
},
|
||||
"description": "Response after deleting an environment from a workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { environmentSlug }
|
||||
} = await validateRequest(reqValidator.DeleteWorkspaceEnvironmentV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Environments
|
||||
);
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
const oldEnvironment = workspace.environments[envIndex];
|
||||
|
||||
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,
|
||||
// });
|
||||
|
||||
const result = await ServiceTokenData.updateMany(
|
||||
{ workspace: workspaceId },
|
||||
{ $pull: { scopes: { environment: environmentSlug } } }
|
||||
);
|
||||
|
||||
if (result.modifiedCount > 0) {
|
||||
await ServiceTokenData.deleteMany({ workspace: workspaceId, scopes: { $size: 0 } });
|
||||
}
|
||||
|
||||
await Integration.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug
|
||||
});
|
||||
await Membership.updateMany(
|
||||
{ workspace: workspaceId },
|
||||
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
|
||||
);
|
||||
|
||||
await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId));
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.DELETE_ENVIRONMENT,
|
||||
metadata: {
|
||||
name: oldEnvironment.name,
|
||||
slug: oldEnvironment.slug
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: workspace._id
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully deleted environment",
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug
|
||||
});
|
||||
};
|
25
backend-mongo/src/controllers/v2/index.ts
Normal file
25
backend-mongo/src/controllers/v2/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import * as authController from "./authController";
|
||||
import * as signupController from "./signupController";
|
||||
import * as usersController from "./usersController";
|
||||
import * as organizationsController from "./organizationsController";
|
||||
import * as workspaceController from "./workspaceController";
|
||||
import * as serviceTokenDataController from "./serviceTokenDataController";
|
||||
import * as secretController from "./secretController";
|
||||
import * as secretsController from "./secretsController";
|
||||
import * as environmentController from "./environmentController";
|
||||
import * as tagController from "./tagController";
|
||||
import * as membershipController from "./membershipController";
|
||||
|
||||
export {
|
||||
authController,
|
||||
signupController,
|
||||
usersController,
|
||||
organizationsController,
|
||||
workspaceController,
|
||||
serviceTokenDataController,
|
||||
secretController,
|
||||
secretsController,
|
||||
environmentController,
|
||||
tagController,
|
||||
membershipController
|
||||
};
|
107
backend-mongo/src/controllers/v2/membershipController.ts
Normal file
107
backend-mongo/src/controllers/v2/membershipController.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
|
||||
import { getSiteURL } from "../../config";
|
||||
import { EventType } from "../../ee/models";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { sendMail } from "../../helpers";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { IUser, Key, Membership, MembershipOrg, Workspace } from "../../models";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import * as reqValidator from "../../validation/membership";
|
||||
import { ACCEPTED, MEMBER } from "../../variables";
|
||||
|
||||
export const addUserToWorkspace = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { members }
|
||||
} = await validateRequest(reqValidator.AddUserToWorkspaceV2, req);
|
||||
// check workspace
|
||||
const workspace = await Workspace.findById(workspaceId);
|
||||
if (!workspace) throw new Error("Failed to find workspace");
|
||||
|
||||
// check permission
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
// validate members are part of the organization
|
||||
const orgMembers = await MembershipOrg.find({
|
||||
status: ACCEPTED,
|
||||
_id: { $in: members.map(({ orgMembershipId }) => orgMembershipId) },
|
||||
organization: workspace.organization
|
||||
})
|
||||
.populate<{ user: IUser }>("user")
|
||||
.select({ _id: 1, user: 1 })
|
||||
.lean();
|
||||
if (orgMembers.length !== members.length)
|
||||
throw BadRequestError({ message: "Org member not found" });
|
||||
|
||||
const existingMember = await Membership.find({
|
||||
workspace: workspaceId,
|
||||
user: { $in: orgMembers.map(({ user }) => user) }
|
||||
});
|
||||
if (existingMember?.length)
|
||||
throw BadRequestError({ message: "Some users are already part of workspace" });
|
||||
|
||||
await Membership.insertMany(
|
||||
orgMembers.map(({ user }) => ({ user: user._id, workspace: workspaceId, role: MEMBER }))
|
||||
);
|
||||
|
||||
const encKeyGroupedByOrgMemberId = members.reduce<Record<string, (typeof members)[number]>>(
|
||||
(prev, curr) => ({ ...prev, [curr.orgMembershipId]: curr }),
|
||||
{}
|
||||
);
|
||||
await Key.insertMany(
|
||||
orgMembers.map(({ user, _id: id }) => ({
|
||||
encryptedKey: encKeyGroupedByOrgMemberId[id.toString()].workspaceEncryptedKey,
|
||||
nonce: encKeyGroupedByOrgMemberId[id.toString()].workspaceEncryptedNonce,
|
||||
sender: req.user._id,
|
||||
receiver: user._id,
|
||||
workspace: workspaceId
|
||||
}))
|
||||
);
|
||||
|
||||
await sendMail({
|
||||
template: "workspaceInvitation.handlebars",
|
||||
subjectLine: "Infisical workspace invitation",
|
||||
recipients: orgMembers.map(({ user }) => user.email),
|
||||
substitutions: {
|
||||
inviterFirstName: req.user.firstName,
|
||||
inviterEmail: req.user.email,
|
||||
workspaceName: workspace.name,
|
||||
callback_url: (await getSiteURL()) + "/login"
|
||||
}
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.ADD_BATCH_WORKSPACE_MEMBER,
|
||||
metadata: orgMembers.map(({ user }) => ({
|
||||
userId: user._id.toString(),
|
||||
email: user.email
|
||||
}))
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
success: true,
|
||||
data: orgMembers
|
||||
});
|
||||
};
|
505
backend-mongo/src/controllers/v2/organizationsController.ts
Normal file
505
backend-mongo/src/controllers/v2/organizationsController.ts
Normal file
@ -0,0 +1,505 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
IWorkspace,
|
||||
Identity,
|
||||
IdentityMembership,
|
||||
IdentityMembershipOrg,
|
||||
Membership,
|
||||
MembershipOrg,
|
||||
User,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { Role } from "../../ee/models";
|
||||
import { deleteMembershipOrg } from "../../helpers/membershipOrg";
|
||||
import {
|
||||
createOrganization as create,
|
||||
deleteOrganization,
|
||||
updateSubscriptionOrgQuantity
|
||||
} from "../../helpers/organization";
|
||||
import { addMembershipsOrg } from "../../helpers/membershipOrg";
|
||||
import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS } from "../../variables";
|
||||
import * as reqValidator from "../../validation/organization";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
getAuthDataOrgPermissions
|
||||
} from "../../ee/services/RoleService";
|
||||
import { EELicenseService } from "../../ee/services";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
/**
|
||||
* Return memberships for organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getOrganizationMemberships = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return organization user memberships'
|
||||
#swagger.description = 'Return organization user memberships'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['organizationId'] = {
|
||||
"description": "ID of organization",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memberships": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/MembershipOrg"
|
||||
},
|
||||
"description": "Memberships of organization"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgMembersv2, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
const memberships = await MembershipOrg.find({
|
||||
organization: organizationId
|
||||
}).populate("user", "+publicKey");
|
||||
|
||||
return res.status(200).send({
|
||||
memberships
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update role of membership with id [membershipId] to role [role]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const updateOrganizationMembership = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Update organization user membership'
|
||||
#swagger.description = 'Update organization user membership'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['organizationId'] = {
|
||||
"description": "ID of organization",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['membershipId'] = {
|
||||
"description": "ID of organization membership to update",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "Role of organization membership - either owner, admin, or member",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"membership": {
|
||||
$ref: "#/components/schemas/MembershipOrg",
|
||||
"description": "Updated organization membership"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { organizationId, membershipId },
|
||||
body: { role }
|
||||
} = await validateRequest(reqValidator.UpdateOrgMemberv2, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
|
||||
if (isCustomRole) {
|
||||
const orgRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: true,
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
if (!orgRole) throw BadRequestError({ message: "Role not found" });
|
||||
|
||||
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
|
||||
|
||||
if (!plan.rbac) return res.status(400).send({
|
||||
message:
|
||||
"Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
||||
});
|
||||
|
||||
const membership = await MembershipOrg.findByIdAndUpdate(membershipId, {
|
||||
role: CUSTOM,
|
||||
customRole: orgRole
|
||||
});
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
}
|
||||
|
||||
const membership = await MembershipOrg.findByIdAndUpdate(
|
||||
membershipId,
|
||||
{
|
||||
$set: {
|
||||
role
|
||||
},
|
||||
$unset: {
|
||||
customRole: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete organization membership with id [membershipId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteOrganizationMembership = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Delete organization user membership'
|
||||
#swagger.description = 'Delete organization user membership'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['organizationId'] = {
|
||||
"description": "ID of organization",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['membershipId'] = {
|
||||
"description": "ID of organization membership to delete",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"membership": {
|
||||
$ref: "#/components/schemas/MembershipOrg",
|
||||
"description": "Deleted organization membership"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { organizationId, membershipId }
|
||||
} = await validateRequest(reqValidator.DeleteOrgMemberv2, req);
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
_id: new Types.ObjectId(membershipId),
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
if (!membershipOrg) throw ResourceNotFoundError();
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: membershipOrg.organization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
// delete organization membership
|
||||
const membership = await deleteMembershipOrg({
|
||||
membershipOrgId: membershipId
|
||||
});
|
||||
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId: membership.organization.toString()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return workspaces for organization with id [organizationId] that user has
|
||||
* access to
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return projects in organization that user is part of'
|
||||
#swagger.description = 'Return projects in organization that user is part of'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['organizationId'] = {
|
||||
"description": "ID of organization",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/Project"
|
||||
},
|
||||
"description": "Projects of organization"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgWorkspacesv2, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Workspace
|
||||
);
|
||||
|
||||
const workspacesSet = new Set(
|
||||
(
|
||||
await Workspace.find(
|
||||
{
|
||||
organization: organizationId
|
||||
},
|
||||
"_id"
|
||||
)
|
||||
).map((w) => w._id.toString())
|
||||
);
|
||||
|
||||
let workspaces: IWorkspace[] = [];
|
||||
|
||||
if (req.authData.authPayload instanceof Identity) {
|
||||
workspaces = (
|
||||
await IdentityMembership.find({
|
||||
identity: req.authData.authPayload._id
|
||||
}).populate<{ workspace: IWorkspace }>("workspace")
|
||||
)
|
||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||
.map((m) => m.workspace);
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof User) {
|
||||
workspaces = (
|
||||
await Membership.find({
|
||||
user: req.authData.authPayload._id
|
||||
}).populate<{ workspace: IWorkspace }>("workspace")
|
||||
)
|
||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||
.map((m) => m.workspace);
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
workspaces
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new organization named [organizationName]
|
||||
* and add user as owner
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createOrganization = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { name }
|
||||
} = await validateRequest(reqValidator.CreateOrgv2, req);
|
||||
|
||||
// create organization and add user as member
|
||||
const organization = await create({
|
||||
email: req.user.email,
|
||||
name
|
||||
});
|
||||
|
||||
await addMembershipsOrg({
|
||||
userIds: [req.user._id.toString()],
|
||||
organizationId: organization._id.toString(),
|
||||
roles: [ADMIN],
|
||||
statuses: [ACCEPTED]
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
organization
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteOrganizationById = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.DeleteOrgv2, req);
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: req.user._id,
|
||||
organization: new Types.ObjectId(organizationId),
|
||||
role: ADMIN
|
||||
});
|
||||
|
||||
if (!membershipOrg) throw UnauthorizedRequestError();
|
||||
|
||||
const organization = await deleteOrganization({
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
organization
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of identity memberships for organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationIdentityMemberships = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return organization identity memberships'
|
||||
#swagger.description = 'Return organization identity memberships'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['organizationId'] = {
|
||||
"description": "ID of organization",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identityMemberships": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/IdentityMembershipOrg"
|
||||
},
|
||||
"description": "Identity memberships of organization"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgIdentityMembershipsV2, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Identity
|
||||
);
|
||||
|
||||
const identityMemberships = await IdentityMembershipOrg.find({
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
}).populate("identity customRole");
|
||||
|
||||
return res.status(200).send({
|
||||
identityMemberships
|
||||
});
|
||||
}
|
419
backend-mongo/src/controllers/v2/secretController.ts
Normal file
419
backend-mongo/src/controllers/v2/secretController.ts
Normal file
@ -0,0 +1,419 @@
|
||||
import { Request, Response } from "express";
|
||||
import mongoose, { Types } from "mongoose";
|
||||
import {
|
||||
CreateSecretRequestBody,
|
||||
ModifySecretRequestBody,
|
||||
SanitizedSecretForCreate,
|
||||
SanitizedSecretModify
|
||||
} from "../../types/secret";
|
||||
const { ValidationError } = mongoose.Error;
|
||||
import {
|
||||
ValidationError as RouteValidationError,
|
||||
UnauthorizedRequestError
|
||||
} from "../../utils/errors";
|
||||
import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED
|
||||
} from "../../variables";
|
||||
import { TelemetryService } from "../../services";
|
||||
import { Secret, User } from "../../models";
|
||||
import { AccountNotFoundError } from "../../utils/errors";
|
||||
|
||||
/**
|
||||
* Create secret for workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const createSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const secretToCreate: CreateSecretRequestBody = req.body.secret;
|
||||
const { workspaceId, environment } = req.params;
|
||||
const sanitizedSecret: SanitizedSecretForCreate = {
|
||||
secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
|
||||
secretKeyIV: secretToCreate.secretKeyIV,
|
||||
secretKeyTag: secretToCreate.secretKeyTag,
|
||||
secretKeyHash: secretToCreate.secretKeyHash,
|
||||
secretValueCiphertext: secretToCreate.secretValueCiphertext,
|
||||
secretValueIV: secretToCreate.secretValueIV,
|
||||
secretValueTag: secretToCreate.secretValueTag,
|
||||
secretValueHash: secretToCreate.secretValueHash,
|
||||
secretCommentCiphertext: secretToCreate.secretCommentCiphertext,
|
||||
secretCommentIV: secretToCreate.secretCommentIV,
|
||||
secretCommentTag: secretToCreate.secretCommentTag,
|
||||
secretCommentHash: secretToCreate.secretCommentHash,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type: secretToCreate.type,
|
||||
user: new Types.ObjectId(req.user._id),
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
};
|
||||
|
||||
const secret = await new Secret(sanitizedSecret).save();
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets added",
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send({
|
||||
secret
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create many secrets for workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const createSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
|
||||
const { workspaceId, environment } = req.params;
|
||||
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [];
|
||||
|
||||
secretsToCreate.forEach((rawSecret) => {
|
||||
const safeUpdateFields: SanitizedSecretForCreate = {
|
||||
secretKeyCiphertext: rawSecret.secretKeyCiphertext,
|
||||
secretKeyIV: rawSecret.secretKeyIV,
|
||||
secretKeyTag: rawSecret.secretKeyTag,
|
||||
secretKeyHash: rawSecret.secretKeyHash,
|
||||
secretValueCiphertext: rawSecret.secretValueCiphertext,
|
||||
secretValueIV: rawSecret.secretValueIV,
|
||||
secretValueTag: rawSecret.secretValueTag,
|
||||
secretValueHash: rawSecret.secretValueHash,
|
||||
secretCommentCiphertext: rawSecret.secretCommentCiphertext,
|
||||
secretCommentIV: rawSecret.secretCommentIV,
|
||||
secretCommentTag: rawSecret.secretCommentTag,
|
||||
secretCommentHash: rawSecret.secretCommentHash,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type: rawSecret.type,
|
||||
user: new Types.ObjectId(req.user._id),
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
};
|
||||
|
||||
sanitizedSecretesToCreate.push(safeUpdateFields);
|
||||
});
|
||||
|
||||
const secrets = await Secret.insertMany(sanitizedSecretesToCreate);
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets added",
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: (secretsToCreate ?? []).length,
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send({
|
||||
secrets
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete secrets in workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const { workspaceId, environmentName } = req.params;
|
||||
const secretIdsToDelete: string[] = req.body.secretIds;
|
||||
|
||||
const secretIdsUserCanDelete = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
|
||||
|
||||
const secretsUserCanDeleteSet: Set<string> = new Set(
|
||||
secretIdsUserCanDelete.map((objectId) => objectId._id.toString())
|
||||
);
|
||||
|
||||
// Filter out IDs that user can delete and then map them to delete operations
|
||||
const deleteOperationsToPerform = secretIdsToDelete
|
||||
.filter(secretIdToDelete => {
|
||||
if (!secretsUserCanDeleteSet.has(secretIdToDelete)) {
|
||||
throw RouteValidationError({
|
||||
message: "You cannot delete secrets that you do not have access to"
|
||||
});
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(secretIdToDelete => ({
|
||||
deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } }
|
||||
}));
|
||||
|
||||
const numSecretsDeleted = deleteOperationsToPerform.length;
|
||||
|
||||
await Secret.bulkWrite(deleteOperationsToPerform);
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets deleted",
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: numSecretsDeleted,
|
||||
environment: environmentName,
|
||||
workspaceId,
|
||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send();
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete secret with id [secretId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
await Secret.findByIdAndDelete(req._secret._id);
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets deleted",
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId: req._secret.workspace.toString(),
|
||||
environment: req._secret.environment,
|
||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send({
|
||||
secret: req._secret
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update secrets for workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const { workspaceId, environmentName } = req.params;
|
||||
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
|
||||
const secretIdsUserCanModify = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
|
||||
|
||||
const secretsUserCanModifySet: Set<string> = new Set(
|
||||
secretIdsUserCanModify.map((objectId) => objectId._id.toString())
|
||||
);
|
||||
const updateOperationsToPerform: any = [];
|
||||
|
||||
secretsModificationsRequested.forEach((userModifiedSecret) => {
|
||||
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
|
||||
const sanitizedSecret: SanitizedSecretModify = {
|
||||
secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext,
|
||||
secretKeyIV: userModifiedSecret.secretKeyIV,
|
||||
secretKeyTag: userModifiedSecret.secretKeyTag,
|
||||
secretKeyHash: userModifiedSecret.secretKeyHash,
|
||||
secretValueCiphertext: userModifiedSecret.secretValueCiphertext,
|
||||
secretValueIV: userModifiedSecret.secretValueIV,
|
||||
secretValueTag: userModifiedSecret.secretValueTag,
|
||||
secretValueHash: userModifiedSecret.secretValueHash,
|
||||
secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext,
|
||||
secretCommentIV: userModifiedSecret.secretCommentIV,
|
||||
secretCommentTag: userModifiedSecret.secretCommentTag,
|
||||
secretCommentHash: userModifiedSecret.secretCommentHash
|
||||
};
|
||||
|
||||
const updateOperation = {
|
||||
updateOne: {
|
||||
filter: { _id: userModifiedSecret._id, workspace: workspaceId },
|
||||
update: { $inc: { version: 1 }, $set: sanitizedSecret }
|
||||
}
|
||||
};
|
||||
updateOperationsToPerform.push(updateOperation);
|
||||
} else {
|
||||
throw UnauthorizedRequestError({
|
||||
message: "You do not have permission to modify one or more of the requested secrets"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await Secret.bulkWrite(updateOperationsToPerform);
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets modified",
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: (secretsModificationsRequested ?? []).length,
|
||||
environment: environmentName,
|
||||
workspaceId,
|
||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send();
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a secret within workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const { workspaceId, environmentName } = req.params;
|
||||
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
|
||||
|
||||
await Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
|
||||
|
||||
const sanitizedSecret: SanitizedSecretModify = {
|
||||
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
|
||||
secretKeyIV: secretModificationsRequested.secretKeyIV,
|
||||
secretKeyTag: secretModificationsRequested.secretKeyTag,
|
||||
secretKeyHash: secretModificationsRequested.secretKeyHash,
|
||||
secretValueCiphertext: secretModificationsRequested.secretValueCiphertext,
|
||||
secretValueIV: secretModificationsRequested.secretValueIV,
|
||||
secretValueTag: secretModificationsRequested.secretValueTag,
|
||||
secretValueHash: secretModificationsRequested.secretValueHash,
|
||||
secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext,
|
||||
secretCommentIV: secretModificationsRequested.secretCommentIV,
|
||||
secretCommentTag: secretModificationsRequested.secretCommentTag,
|
||||
secretCommentHash: secretModificationsRequested.secretCommentHash
|
||||
};
|
||||
|
||||
const singleModificationUpdate = await Secret.updateOne(
|
||||
{ _id: secretModificationsRequested._id, workspace: workspaceId },
|
||||
{ $inc: { version: 1 }, $set: sanitizedSecret }
|
||||
)
|
||||
.catch((error) => {
|
||||
if (error instanceof ValidationError) {
|
||||
throw RouteValidationError({
|
||||
message: "Unable to apply modifications, please try again",
|
||||
stack: error.stack
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets modified",
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
environment: environmentName,
|
||||
workspaceId,
|
||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send(singleModificationUpdate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return secrets for workspace with id [workspaceId], environment [environment] and user
|
||||
* with id [req.user._id]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getSecrets = async (req: Request, res: Response) => {
|
||||
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: string | undefined = undefined; // used for posthog
|
||||
if (req.user) {
|
||||
userId = req.user._id;
|
||||
userEmail = req.user.email;
|
||||
}
|
||||
|
||||
if (req.serviceTokenData) {
|
||||
userId = req.serviceTokenData.user;
|
||||
|
||||
const user = await User.findById(req.serviceTokenData.user, "email");
|
||||
if (!user) throw AccountNotFoundError();
|
||||
userEmail = user.email;
|
||||
}
|
||||
|
||||
const secrets = await Secret.find({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
$or: [{ user: userId }, { user: { $exists: false } }],
|
||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||
})
|
||||
.catch((err) => {
|
||||
throw RouteValidationError({
|
||||
message: "Failed to get secrets, please try again",
|
||||
stack: err.stack
|
||||
});
|
||||
})
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets pulled",
|
||||
distinctId: userEmail,
|
||||
properties: {
|
||||
numberOfSecrets: (secrets ?? []).length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.json(secrets);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return secret with id [secretId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getSecret = async (req: Request, res: Response) => {
|
||||
// if (postHogClient) {
|
||||
// postHogClient.capture({
|
||||
// event: 'secrets pulled',
|
||||
// distinctId: req.user.email,
|
||||
// properties: {
|
||||
// numberOfSecrets: 1,
|
||||
// workspaceId: req._secret.workspace.toString(),
|
||||
// environment: req._secret.environment,
|
||||
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||
// userAgent: req.headers?.['user-agent']
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
return res.status(200).send({
|
||||
secret: req._secret
|
||||
});
|
||||
};
|
1300
backend-mongo/src/controllers/v2/secretsController.ts
Normal file
1300
backend-mongo/src/controllers/v2/secretsController.ts
Normal file
File diff suppressed because it is too large
Load Diff
201
backend-mongo/src/controllers/v2/serviceTokenDataController.ts
Normal file
201
backend-mongo/src/controllers/v2/serviceTokenDataController.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import { Request, Response } from "express";
|
||||
import crypto from "crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import { ServiceTokenData } from "../../models";
|
||||
import { getSaltRounds } from "../../config";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { ActorType, EventType } from "../../ee/models";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/serviceTokenData";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { Types } from "mongoose";
|
||||
|
||||
/**
|
||||
* Return service token data associated with service token on request
|
||||
* @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'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"serviceTokenData": {
|
||||
"type": "object",
|
||||
$ref: "#/components/schemas/ServiceTokenData",
|
||||
"description": "Details of service token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
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")
|
||||
.lean();
|
||||
|
||||
return res.status(200).json(serviceTokenData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new service token data for workspace with id [workspaceId] and
|
||||
* environment [environment].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
let serviceTokenData;
|
||||
|
||||
const {
|
||||
body: { workspaceId, permissions, tag, encryptedKey, scopes, name, expiresIn, iv }
|
||||
} = await validateRequest(reqValidator.CreateServiceTokenV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
scopes.forEach(({ environment, secretPath }) => {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: secretPath })
|
||||
);
|
||||
})
|
||||
|
||||
|
||||
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;
|
||||
|
||||
if (req.authData.actor.type === ActorType.USER) {
|
||||
user = req.authData.authPayload._id;
|
||||
}
|
||||
|
||||
serviceTokenData = await new ServiceTokenData({
|
||||
name,
|
||||
workspace: workspaceId,
|
||||
user,
|
||||
scopes,
|
||||
lastUsed: new Date(),
|
||||
expiresAt,
|
||||
secretHash,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
permissions
|
||||
}).save();
|
||||
|
||||
// return service token data without sensitive data
|
||||
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
|
||||
|
||||
if (!serviceTokenData) throw new Error("Failed to find service token data");
|
||||
|
||||
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_SERVICE_TOKEN,
|
||||
metadata: {
|
||||
name,
|
||||
scopes
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceToken,
|
||||
serviceTokenData
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete service token data with id [serviceTokenDataId].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { serviceTokenDataId }
|
||||
} = await validateRequest(reqValidator.DeleteServiceTokenV2, req);
|
||||
|
||||
let serviceTokenData = await ServiceTokenData.findById(serviceTokenDataId);
|
||||
if (!serviceTokenData) throw BadRequestError({ message: "Service token not found" });
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: serviceTokenData.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
|
||||
|
||||
if (!serviceTokenData)
|
||||
return res.status(200).send({
|
||||
message: "Failed to delete service token"
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.DELETE_SERVICE_TOKEN,
|
||||
metadata: {
|
||||
name: serviceTokenData.name,
|
||||
scopes: serviceTokenData?.scopes
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: serviceTokenData.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
};
|
262
backend-mongo/src/controllers/v2/signupController.ts
Normal file
262
backend-mongo/src/controllers/v2/signupController.ts
Normal file
@ -0,0 +1,262 @@
|
||||
import { Request, Response } from "express";
|
||||
import { MembershipOrg, User } from "../../models";
|
||||
import { completeAccount } from "../../helpers/user";
|
||||
import {
|
||||
initializeDefaultOrg,
|
||||
} from "../../helpers/signup";
|
||||
import { issueAuthTokens } from "../../helpers/auth";
|
||||
import { ACCEPTED, INVITED } from "../../variables";
|
||||
import { standardRequest } from "../../config/request";
|
||||
import { getHttpsEnabled, getLoopsApiKey } from "../../config";
|
||||
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
|
||||
|
||||
/**
|
||||
* 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;
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
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,
|
||||
}
|
||||
);
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? "",
|
||||
});
|
||||
|
||||
const 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",
|
||||
user,
|
||||
token,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete setting up user by adding their personal and auth information as part of the
|
||||
* invite flow
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const completeAccountInvite = async (req: Request, res: Response) => {
|
||||
let user;
|
||||
const {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
} = 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",
|
||||
});
|
||||
}
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: email,
|
||||
status: INVITED,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
if (!user)
|
||||
throw new Error("Failed to complete account for non-existent user");
|
||||
|
||||
// update organization membership statuses that are
|
||||
// invited to completed with user attached
|
||||
const membershipsToUpdate = await MembershipOrg.find({
|
||||
inviteEmail: email,
|
||||
status: INVITED,
|
||||
});
|
||||
|
||||
membershipsToUpdate.forEach(async (membership) => {
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId: membership.organization.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
await MembershipOrg.updateMany(
|
||||
{
|
||||
inviteEmail: email,
|
||||
status: INVITED,
|
||||
},
|
||||
{
|
||||
user,
|
||||
status: ACCEPTED,
|
||||
}
|
||||
);
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? "",
|
||||
});
|
||||
|
||||
const token = tokens.token;
|
||||
|
||||
// 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",
|
||||
user,
|
||||
token,
|
||||
});
|
||||
};
|
92
backend-mongo/src/controllers/v2/tagController.ts
Normal file
92
backend-mongo/src/controllers/v2/tagController.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Secret, Tag } from "../../models";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import * as reqValidator from "../../validation/tags";
|
||||
|
||||
export const createWorkspaceTag = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { name, slug },
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.CreateWorkspaceTagsV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Tags
|
||||
);
|
||||
|
||||
const tagToCreate = {
|
||||
name,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
slug,
|
||||
user: new Types.ObjectId(req.user._id)
|
||||
};
|
||||
|
||||
const createdTag = await new Tag(tagToCreate).save();
|
||||
|
||||
res.json(createdTag);
|
||||
};
|
||||
|
||||
export const deleteWorkspaceTag = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { tagId }
|
||||
} = await validateRequest(reqValidator.DeleteWorkspaceTagsV2, req);
|
||||
|
||||
const tagFromDB = await Tag.findById(tagId);
|
||||
if (!tagFromDB) {
|
||||
throw BadRequestError();
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: tagFromDB.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Tags
|
||||
);
|
||||
|
||||
const result = await Tag.findByIdAndDelete(tagId);
|
||||
|
||||
// remove the tag from secrets
|
||||
await Secret.updateMany({ tags: { $in: [tagId] } }, { $pull: { tags: tagId } });
|
||||
|
||||
res.json(result);
|
||||
};
|
||||
|
||||
export const getWorkspaceTags = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceTagsV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Tags
|
||||
);
|
||||
|
||||
const workspaceTags = await Tag.find({
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.json({
|
||||
workspaceTags
|
||||
});
|
||||
};
|
314
backend-mongo/src/controllers/v2/usersController.ts
Normal file
314
backend-mongo/src/controllers/v2/usersController.ts
Normal file
@ -0,0 +1,314 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import crypto from "crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import { APIKeyData, AuthMethod, MembershipOrg, TokenVersion, User } from "../../models";
|
||||
import { getSaltRounds } from "../../config";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { deleteUser } from "../../helpers/user";
|
||||
import * as reqValidator from "../../validation";
|
||||
|
||||
/**
|
||||
* Update the current user's MFA-enabled status [isMfaEnabled].
|
||||
* Note: Infisical currently only supports email-based 2FA only; this will expand to
|
||||
* include SMS and authenticator app modes of authentication in the future.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { isMfaEnabled }
|
||||
} = await validateRequest(reqValidator.UpdateMyMfaEnabledV2, req);
|
||||
|
||||
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
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update name of the current user to [firstName, lastName].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateName = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { lastName, firstName }
|
||||
} = await validateRequest(reqValidator.UpdateNameV2, req);
|
||||
|
||||
const user = await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
firstName,
|
||||
lastName: lastName ?? ""
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update auth method of the current user to [authMethods]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateAuthMethods = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { authMethods }
|
||||
} = await validateRequest(reqValidator.UpdateAuthMethodsV2, req);
|
||||
|
||||
const hasSamlEnabled = req.user.authMethods.some((authMethod: AuthMethod) =>
|
||||
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod)
|
||||
);
|
||||
|
||||
if (hasSamlEnabled) {
|
||||
return res.status(400).send({
|
||||
message: "Failed to update user authentication method because SAML SSO is enforced"
|
||||
});
|
||||
}
|
||||
|
||||
const user = await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
authMethods
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return organizations that the current user is part of.
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getMyOrganizations = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return organizations that current user is part of'
|
||||
#swagger.description = 'Return organizations that current user is part of'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"organizations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/Organization"
|
||||
},
|
||||
"description": "Organizations that user is part of"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const organizations = (
|
||||
await MembershipOrg.find({
|
||||
user: req.user._id
|
||||
}).populate("organization")
|
||||
).map((m) => m.organization);
|
||||
|
||||
return res.status(200).send({
|
||||
organizations
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return API keys belonging to current user.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getMyAPIKeys = async (req: Request, res: Response) => {
|
||||
const apiKeyData = await APIKeyData.find({
|
||||
user: req.user._id
|
||||
});
|
||||
|
||||
return res.status(200).send(apiKeyData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new API key for current user.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createAPIKey = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { name, expiresIn }
|
||||
} = await validateRequest(reqValidator.CreateApiKeyV2, req);
|
||||
|
||||
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
|
||||
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
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete API key with id [apiKeyDataId] belonging to current user
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteAPIKey = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { apiKeyDataId }
|
||||
} = await validateRequest(reqValidator.DeleteApiKeyV2, req);
|
||||
|
||||
const apiKeyData = await APIKeyData.findOneAndDelete({
|
||||
_id: new Types.ObjectId(apiKeyDataId),
|
||||
user: req.user._id
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
apiKeyData
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return active sessions (TokenVersion) belonging to user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getMySessions = async (req: Request, res: Response) => {
|
||||
const tokenVersions = await TokenVersion.find({
|
||||
user: req.user._id
|
||||
});
|
||||
|
||||
return res.status(200).send(tokenVersions);
|
||||
};
|
||||
|
||||
/**
|
||||
* Revoke all active sessions belong to user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteMySessions = 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 the current user.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getMe = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = "Retrieve the current user on the request"
|
||||
#swagger.description = "Retrieve the current user on the request"
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
$ref: "#/components/schemas/CurrentUser",
|
||||
"description": "Current user on request"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const user = await User.findById(req.user._id).select(
|
||||
"+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete the current user.
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteMe = async (req: Request, res: Response) => {
|
||||
const user = await deleteUser({
|
||||
userId: req.user._id
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
});
|
||||
}
|
883
backend-mongo/src/controllers/v2/workspaceController.ts
Normal file
883
backend-mongo/src/controllers/v2/workspaceController.ts
Normal file
@ -0,0 +1,883 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
IIdentity,
|
||||
IdentityMembership,
|
||||
IdentityMembershipOrg,
|
||||
Key,
|
||||
Membership,
|
||||
ServiceTokenData,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { IRole, Role } from "../../ee/models";
|
||||
import {
|
||||
pullSecrets as pull,
|
||||
v2PushSecrets as push,
|
||||
reformatPullSecrets
|
||||
} from "../../helpers/secret";
|
||||
import { pushKeys } from "../../helpers/key";
|
||||
import { EventService, TelemetryService } from "../../services";
|
||||
import { eventPushSecrets } from "../../events";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import { EventType } from "../../ee/models";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions,
|
||||
getWorkspaceRolePermissions,
|
||||
isAtLeastAsPrivilegedWorkspace
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError } from "../../utils/errors";
|
||||
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
|
||||
|
||||
interface V2PushSecret {
|
||||
type: string; // personal or shared
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentHash?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
* for environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
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");
|
||||
}
|
||||
|
||||
// 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.realIP
|
||||
});
|
||||
|
||||
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: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath: "/"
|
||||
})
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully uploaded workspace secrets"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return (encrypted) secrets for workspace with id [workspaceId]
|
||||
* for environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const pullSecrets = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
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.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.realIP
|
||||
});
|
||||
|
||||
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"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets
|
||||
});
|
||||
};
|
||||
|
||||
export const getWorkspaceKey = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return encrypted project key'
|
||||
#swagger.description = 'Return encrypted project key'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/ProjectKey"
|
||||
},
|
||||
"description": "Encrypted project key for the given project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceKeyV2, req);
|
||||
|
||||
const key = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
}).populate("sender", "+publicKey");
|
||||
|
||||
if (!key) throw new Error(`getWorkspaceKey: Failed to find workspace key [workspaceId=${workspaceId}] [receiver=${req.user._id}]`);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.GET_WORKSPACE_KEY,
|
||||
metadata: {
|
||||
keyId: key._id.toString()
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).json(key);
|
||||
};
|
||||
|
||||
export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
const serviceTokenData = await ServiceTokenData.find({
|
||||
workspace: workspaceId
|
||||
}).select("+encryptedKey +iv +tag");
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return memberships for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return project user memberships'
|
||||
#swagger.description = 'Return project user memberships'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memberships": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/Membership"
|
||||
},
|
||||
"description": "Memberships of project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
const memberships = await Membership.find({
|
||||
workspace: workspaceId
|
||||
}).populate("user", "+publicKey");
|
||||
|
||||
return res.status(200).send({
|
||||
memberships
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update role of membership with id [membershipId] to role [role]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateWorkspaceMembership = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Update project user membership'
|
||||
#swagger.description = 'Update project user membership'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['membershipId'] = {
|
||||
"description": "ID of project membership to update",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "Role to update to for project membership",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"membership": {
|
||||
$ref: "#/components/schemas/Membership",
|
||||
"description": "Updated membership"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId, membershipId },
|
||||
body: { role }
|
||||
} = await validateRequest(reqValidator.UpdateWorkspaceMembershipsV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
const membership = await Membership.findByIdAndUpdate(
|
||||
membershipId,
|
||||
{
|
||||
role
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete workspace membership with id [membershipId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Delete project user membership'
|
||||
#swagger.description = 'Delete project user membership'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['membershipId'] = {
|
||||
"description": "ID of project membership to delete",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"membership": {
|
||||
$ref: "#/components/schemas/Membership",
|
||||
"description": "Deleted membership"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId, membershipId }
|
||||
} = await validateRequest(reqValidator.DeleteWorkspaceMembershipsV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
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
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change autoCapitilzation Rule of workspace
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const toggleAutoCapitalization = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { autoCapitalization }
|
||||
} = await validateRequest(reqValidator.ToggleAutoCapitalizationV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Settings
|
||||
);
|
||||
|
||||
const workspace = await Workspace.findOneAndUpdate(
|
||||
{
|
||||
_id: workspaceId
|
||||
},
|
||||
{
|
||||
autoCapitalization
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully changed autoCapitalization setting",
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add identity with id [identityId] to workspace
|
||||
* with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const addIdentityToWorkspace = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId, identityId },
|
||||
body: {
|
||||
role
|
||||
}
|
||||
} = await validateRequest(reqValidator.AddIdentityToWorkspaceV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
let identityMembership = await IdentityMembership.findOne({
|
||||
identity: new Types.ObjectId(identityId),
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (identityMembership) throw BadRequestError({
|
||||
message: `Identity with id ${identityId} already exists in project with id ${workspaceId}`
|
||||
});
|
||||
|
||||
|
||||
const workspace = await Workspace.findById(workspaceId);
|
||||
if (!workspace) throw ResourceNotFoundError();
|
||||
|
||||
const identityMembershipOrg = await IdentityMembershipOrg.findOne({
|
||||
identity: new Types.ObjectId(identityId),
|
||||
organization: workspace.organization
|
||||
});
|
||||
|
||||
if (!identityMembershipOrg) throw ResourceNotFoundError({
|
||||
message: `Failed to find identity with id ${identityId}`
|
||||
});
|
||||
|
||||
if (!identityMembershipOrg.organization.equals(workspace.organization)) throw BadRequestError({
|
||||
message: "Failed to add identity to project in another organization"
|
||||
});
|
||||
|
||||
const rolePermission = await getWorkspaceRolePermissions(role, workspaceId);
|
||||
const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
|
||||
|
||||
if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({
|
||||
message: "Failed to add identity to project with more privileged role"
|
||||
});
|
||||
|
||||
let customRole;
|
||||
if (role) {
|
||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
|
||||
if (isCustomRole) {
|
||||
customRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: false,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
}
|
||||
|
||||
identityMembership = await new IdentityMembership({
|
||||
identity: identityMembershipOrg.identity,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
role: customRole ? CUSTOM : role,
|
||||
customRole
|
||||
}).save();
|
||||
|
||||
return res.status(200).send({
|
||||
identityMembership
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update role of identity with id [identityId] in workspace
|
||||
* with id [workspaceId] to [role]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const updateIdentityWorkspaceRole = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Update project identity membership'
|
||||
#swagger.description = 'Update project identity membership'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['identityId'] = {
|
||||
"description": "ID of identity whose membership to update in project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "Role to update to for identity project membership",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identityMembership": {
|
||||
$ref: "#/components/schemas/IdentityMembership",
|
||||
"description": "Updated identity membership"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId, identityId },
|
||||
body: {
|
||||
role
|
||||
}
|
||||
} = await validateRequest(reqValidator.UpdateIdentityWorkspaceRoleV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
let identityMembership = await IdentityMembership
|
||||
.findOne({
|
||||
identity: new Types.ObjectId(identityId),
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
})
|
||||
.populate<{
|
||||
identity: IIdentity,
|
||||
customRole: IRole
|
||||
}>("identity customRole");
|
||||
|
||||
if (!identityMembership) throw BadRequestError({
|
||||
message: `Identity with id ${identityId} does not exist in project with id ${workspaceId}`
|
||||
});
|
||||
|
||||
const identityRolePermission = await getWorkspaceRolePermissions(
|
||||
identityMembership?.customRole?.slug ?? identityMembership.role,
|
||||
identityMembership.workspace.toString()
|
||||
);
|
||||
const isAsPrivilegedAsIdentity = isAtLeastAsPrivilegedWorkspace(permission, identityRolePermission);
|
||||
if (!isAsPrivilegedAsIdentity) throw ForbiddenRequestError({
|
||||
message: "Failed to update role of more privileged identity"
|
||||
});
|
||||
|
||||
const rolePermission = await getWorkspaceRolePermissions(role, workspaceId);
|
||||
const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
|
||||
|
||||
if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({
|
||||
message: "Failed to update identity to a more privileged role"
|
||||
});
|
||||
|
||||
let customRole;
|
||||
if (role) {
|
||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
|
||||
if (isCustomRole) {
|
||||
customRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: false,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
}
|
||||
|
||||
identityMembership = await IdentityMembership.findOneAndUpdate(
|
||||
{
|
||||
identity: identityMembership.identity._id,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
},
|
||||
{
|
||||
role: customRole ? CUSTOM : role,
|
||||
customRole
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
identityMembership
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete identity with id [identityId] from workspace
|
||||
* with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteIdentityFromWorkspace = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Delete project identity membership'
|
||||
#swagger.description = 'Delete project identity membership'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['identityId'] = {
|
||||
"description": "ID of identity whose membership to delete in project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identityMembership": {
|
||||
$ref: "#/components/schemas/IdentityMembership",
|
||||
"description": "Deleted identity membership"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId, identityId }
|
||||
} = await validateRequest(reqValidator.DeleteIdentityFromWorkspaceV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
const identityMembership = await IdentityMembership
|
||||
.findOne({
|
||||
identity: new Types.ObjectId(identityId),
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
})
|
||||
.populate<{
|
||||
identity: IIdentity,
|
||||
customRole: IRole
|
||||
}>("identity customRole");
|
||||
|
||||
if (!identityMembership) throw ResourceNotFoundError({
|
||||
message: `Identity with id ${identityId} does not exist in project with id ${workspaceId}`
|
||||
});
|
||||
|
||||
const identityRolePermission = await getWorkspaceRolePermissions(
|
||||
identityMembership?.customRole?.slug ?? identityMembership.role,
|
||||
identityMembership.workspace.toString()
|
||||
);
|
||||
const isAsPrivilegedAsIdentity = isAtLeastAsPrivilegedWorkspace(permission, identityRolePermission);
|
||||
if (!isAsPrivilegedAsIdentity) throw ForbiddenRequestError({
|
||||
message: "Failed to remove more privileged identity from project"
|
||||
});
|
||||
|
||||
await IdentityMembership.findByIdAndDelete(identityMembership._id);
|
||||
|
||||
return res.status(200).send({
|
||||
identityMembership
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of identity memberships for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceIdentityMemberships = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return project identity memberships'
|
||||
#swagger.description = 'Return project identity memberships'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identityMemberships": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/IdentityMembership"
|
||||
},
|
||||
"description": "Identity memberships of project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceIdentityMembersV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
const identityMemberships = await IdentityMembership.find({
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
}).populate("identity customRole");
|
||||
|
||||
return res.status(200).send({
|
||||
identityMemberships
|
||||
});
|
||||
}
|
224
backend-mongo/src/controllers/v3/authController.ts
Normal file
224
backend-mongo/src/controllers/v3/authController.ts
Normal file
@ -0,0 +1,224 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { Request, Response } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import * as bigintConversion from "bigint-conversion";
|
||||
const jsrp = require("jsrp");
|
||||
import { LoginSRPDetail, User } from "../../models";
|
||||
import { createToken, issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth";
|
||||
import { checkUserDevice } from "../../helpers/user";
|
||||
import { sendMail } from "../../helpers/nodemailer";
|
||||
import { TokenService } from "../../services";
|
||||
import { BadRequestError, InternalServerError } from "../../utils/errors";
|
||||
import { AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables";
|
||||
import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config";
|
||||
import { AuthMethod } from "../../models/user";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/auth";
|
||||
|
||||
declare module "jsonwebtoken" {
|
||||
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
authProvider: AuthMethod;
|
||||
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) => {
|
||||
const {
|
||||
body: { email, clientPublicKey, providerAuthToken }
|
||||
} = await validateRequest(reqValidator.Login1V3, req);
|
||||
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select("+salt +verifier");
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
|
||||
await validateProviderAuthToken({
|
||||
email,
|
||||
providerAuthToken
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
if (!req.headers["user-agent"])
|
||||
throw InternalServerError({ message: "User-Agent header is required" });
|
||||
|
||||
const {
|
||||
body: { email, providerAuthToken, clientProof }
|
||||
} = await validateRequest(reqValidator.Login2V3, req);
|
||||
|
||||
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.authMethods.includes(AuthMethod.EMAIL)) {
|
||||
await validateProviderAuthToken({
|
||||
email,
|
||||
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: {
|
||||
authTokenType: AuthTokenType.MFA_TOKEN,
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: await getJwtMfaLifetime(),
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return res.status(200).send(response);
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: "Failed to authenticate. Try again?"
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
13
backend-mongo/src/controllers/v3/index.ts
Normal file
13
backend-mongo/src/controllers/v3/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import * as usersController from "./usersController";
|
||||
import * as secretsController from "./secretsController";
|
||||
import * as workspacesController from "./workspacesController";
|
||||
import * as authController from "./authController";
|
||||
import * as signupController from "./signupController";
|
||||
|
||||
export {
|
||||
usersController,
|
||||
authController,
|
||||
secretsController,
|
||||
signupController,
|
||||
workspacesController
|
||||
}
|
1461
backend-mongo/src/controllers/v3/secretsController.ts
Normal file
1461
backend-mongo/src/controllers/v3/secretsController.ts
Normal file
File diff suppressed because it is too large
Load Diff
193
backend-mongo/src/controllers/v3/signupController.ts
Normal file
193
backend-mongo/src/controllers/v3/signupController.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Request, Response } from "express";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { MembershipOrg, User } from "../../models";
|
||||
import { completeAccount } from "../../helpers/user";
|
||||
import { initializeDefaultOrg } from "../../helpers/signup";
|
||||
import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth";
|
||||
import { ACCEPTED, AuthTokenType, INVITED } from "../../variables";
|
||||
import { standardRequest } from "../../config/request";
|
||||
import { getAuthSecret, getHttpsEnabled, getLoopsApiKey } from "../../config";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import { TelemetryService } from "../../services";
|
||||
import { AuthMethod } from "../../models";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/auth";
|
||||
|
||||
/**
|
||||
* 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;
|
||||
try {
|
||||
const {
|
||||
body: {
|
||||
email,
|
||||
publicKey,
|
||||
salt,
|
||||
lastName,
|
||||
verifier,
|
||||
firstName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
organizationName,
|
||||
providerAuthToken,
|
||||
attributionSource,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag
|
||||
}
|
||||
} = await validateRequest(reqValidator.CompletedAccountSignupV3, req);
|
||||
|
||||
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
|
||||
});
|
||||
} 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 getAuthSecret())
|
||||
);
|
||||
|
||||
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw UnauthorizedRequestError();
|
||||
if (decodedToken.userId !== user.id) throw UnauthorizedRequestError();
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) =>
|
||||
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod)
|
||||
);
|
||||
|
||||
if (!hasSamlEnabled) {
|
||||
// TODO: modify this part
|
||||
// initialize default organization and workspace
|
||||
await initializeDefaultOrg({
|
||||
organizationName,
|
||||
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 ? { 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
|
||||
});
|
||||
};
|
18
backend-mongo/src/controllers/v3/usersController.ts
Normal file
18
backend-mongo/src/controllers/v3/usersController.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Request, Response } from "express";
|
||||
import { APIKeyDataV2 } from "../../models";
|
||||
|
||||
/**
|
||||
* Return API keys belonging to current user.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getMyAPIKeys = async (req: Request, res: Response) => {
|
||||
const apiKeyData = await APIKeyDataV2.find({
|
||||
user: req.user._id
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
apiKeyData
|
||||
});
|
||||
}
|
142
backend-mongo/src/controllers/v3/workspacesController.ts
Normal file
142
backend-mongo/src/controllers/v3/workspacesController.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { Membership, Secret, User } from "../../models";
|
||||
import { SecretService } from "../../services";
|
||||
import { getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService";
|
||||
import { UnauthorizedRequestError } from "../../utils/errors";
|
||||
import * as reqValidator from "../../validation/workspace";
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceBlinkIndexStatusV3, req);
|
||||
|
||||
await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (req.authData.authPayload instanceof User) {
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
if (membership.role !== "admin")
|
||||
throw UnauthorizedRequestError({ message: "User must be an admin" });
|
||||
}
|
||||
|
||||
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 {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceSecretsV3, req);
|
||||
|
||||
await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (req.authData.authPayload instanceof User) {
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
if (membership.role !== "admin")
|
||||
throw UnauthorizedRequestError({ message: "User must be an admin" });
|
||||
}
|
||||
|
||||
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) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { secretsToUpdate }
|
||||
} = await validateRequest(reqValidator.NameWorkspaceSecretsV3, req);
|
||||
|
||||
await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (req.authData.authPayload instanceof User) {
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
if (membership.role !== "admin")
|
||||
throw UnauthorizedRequestError({ message: "User must be an admin" });
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
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"
|
||||
});
|
||||
};
|
3519
backend-mongo/src/data/disposable_emails.txt
Normal file
3519
backend-mongo/src/data/disposable_emails.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,32 @@
|
||||
import { Request, Response } from "express";
|
||||
import { EELicenseService } from "../../services";
|
||||
import { getLicenseServerUrl } from "../../../config";
|
||||
import { licenseServerKeyRequest } from "../../../config/request";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../../validation/cloudProducts";
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
query: { "billing-cycle": billingCycle }
|
||||
} = await validateRequest(reqValidator.GetCloudProductsV1, req);
|
||||
|
||||
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: []
|
||||
});
|
||||
};
|
460
backend-mongo/src/ee/controllers/v1/identitiesController.ts
Normal file
460
backend-mongo/src/ee/controllers/v1/identitiesController.ts
Normal file
@ -0,0 +1,460 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
IIdentity,
|
||||
Identity,
|
||||
IdentityAccessToken,
|
||||
IdentityMembership,
|
||||
IdentityMembershipOrg,
|
||||
IdentityUniversalAuth,
|
||||
IdentityUniversalAuthClientSecret,
|
||||
Organization
|
||||
} from "../../../models";
|
||||
import {
|
||||
EventType,
|
||||
IRole,
|
||||
Role
|
||||
} from "../../models";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../../validation/identities";
|
||||
import {
|
||||
getAuthDataOrgPermissions,
|
||||
getOrgRolePermissions,
|
||||
isAtLeastAsPrivilegedOrg
|
||||
} from "../../services/RoleService";
|
||||
import {
|
||||
BadRequestError,
|
||||
ForbiddenRequestError,
|
||||
ResourceNotFoundError,
|
||||
} from "../../../utils/errors";
|
||||
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS } from "../../../variables";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "../../services/RoleService";
|
||||
import { EEAuditLogService } from "../../services";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
/**
|
||||
* Create identity
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createIdentity = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Create identity'
|
||||
#swagger.description = 'Create identity'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of entity to create",
|
||||
"example": "development"
|
||||
},
|
||||
"organizationId": {
|
||||
"type": "string",
|
||||
"description": "ID of organization where to create identity",
|
||||
"example": "dev-environment"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "Role to assume for organization membership",
|
||||
"example": "no-access"
|
||||
}
|
||||
},
|
||||
"required": ["name", "organizationId", "role"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identity": {
|
||||
$ref: '#/definitions/Identity'
|
||||
}
|
||||
},
|
||||
"description": "Details of the created identity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
body: {
|
||||
name,
|
||||
organizationId,
|
||||
role
|
||||
}
|
||||
} = await validateRequest(reqValidator.CreateIdentityV1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Identity
|
||||
);
|
||||
|
||||
const rolePermission = await getOrgRolePermissions(role, organizationId);
|
||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
||||
|
||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||
message: "Failed to create a more privileged identity"
|
||||
});
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) throw BadRequestError({ message: `Organization with id ${organizationId} not found` });
|
||||
|
||||
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
|
||||
|
||||
let customRole;
|
||||
if (isCustomRole) {
|
||||
customRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: true,
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
|
||||
const identity = await new Identity({
|
||||
name
|
||||
}).save();
|
||||
|
||||
await new IdentityMembershipOrg({
|
||||
identity: identity._id,
|
||||
organization: new Types.ObjectId(organizationId),
|
||||
role: isCustomRole ? CUSTOM : role,
|
||||
customRole
|
||||
}).save();
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_IDENTITY,
|
||||
metadata: {
|
||||
identityId: identity._id.toString(),
|
||||
name
|
||||
}
|
||||
},
|
||||
{
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
identity
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update identity with id [identityId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateIdentity = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Update identity'
|
||||
#swagger.description = 'Update identity'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['identityId'] = {
|
||||
"description": "ID of identity to update",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of entity to update to",
|
||||
"example": "development"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "Role to update to for organization membership",
|
||||
"example": "no-access"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identity": {
|
||||
$ref: '#/definitions/Identity'
|
||||
}
|
||||
},
|
||||
"description": "Details of the updated identity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { identityId },
|
||||
body: {
|
||||
name,
|
||||
role
|
||||
}
|
||||
} = await validateRequest(reqValidator.UpdateIdentityV1, req);
|
||||
|
||||
const identityMembershipOrg = await IdentityMembershipOrg
|
||||
.findOne({
|
||||
identity: new Types.ObjectId(identityId)
|
||||
})
|
||||
.populate<{
|
||||
identity: IIdentity,
|
||||
customRole: IRole
|
||||
}>("identity customRole");
|
||||
|
||||
if (!identityMembershipOrg) throw ResourceNotFoundError({
|
||||
message: `Failed to find identity with id ${identityId}`
|
||||
});
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: identityMembershipOrg.organization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Identity
|
||||
);
|
||||
|
||||
const identityRolePermission = await getOrgRolePermissions(
|
||||
identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role,
|
||||
identityMembershipOrg.organization.toString()
|
||||
);
|
||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, identityRolePermission);
|
||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||
message: "Failed to update more privileged identity"
|
||||
});
|
||||
|
||||
if (role) {
|
||||
const rolePermission = await getOrgRolePermissions(role, identityMembershipOrg.organization.toString());
|
||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
||||
|
||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||
message: "Failed to update identity to a more privileged role"
|
||||
});
|
||||
}
|
||||
|
||||
let customRole;
|
||||
if (role) {
|
||||
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
|
||||
if (isCustomRole) {
|
||||
customRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: true,
|
||||
organization: identityMembershipOrg.organization
|
||||
});
|
||||
|
||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
}
|
||||
|
||||
const identity = await Identity.findByIdAndUpdate(
|
||||
identityId,
|
||||
{
|
||||
name,
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (!identity) throw BadRequestError({
|
||||
message: `Failed to update identity with id ${identityId}`
|
||||
});
|
||||
|
||||
await IdentityMembershipOrg.findOneAndUpdate(
|
||||
{
|
||||
identity: identity._id
|
||||
},
|
||||
{
|
||||
role: customRole ? CUSTOM : role,
|
||||
...(customRole ? {
|
||||
customRole
|
||||
} : {}),
|
||||
...(role && !customRole ? { // non-custom role
|
||||
$unset: {
|
||||
customRole: 1
|
||||
}
|
||||
} : {})
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.UPDATE_IDENTITY,
|
||||
metadata: {
|
||||
identityId: identity._id.toString(),
|
||||
name: identity.name,
|
||||
}
|
||||
},
|
||||
{
|
||||
organizationId: identityMembershipOrg.organization
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
identity
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete identity with id [identityId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteIdentity = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Delete identity'
|
||||
#swagger.description = 'Delete identity'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['identityId'] = {
|
||||
"description": "ID of identity",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identity": {
|
||||
$ref: '#/definitions/Identity'
|
||||
}
|
||||
},
|
||||
"description": "Details of the deleted identity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { identityId }
|
||||
} = await validateRequest(reqValidator.DeleteIdentityV1, req);
|
||||
|
||||
const identityMembershipOrg = await IdentityMembershipOrg
|
||||
.findOne({
|
||||
identity: new Types.ObjectId(identityId)
|
||||
})
|
||||
.populate<{
|
||||
identity: IIdentity,
|
||||
customRole: IRole
|
||||
}>("identity customRole");
|
||||
|
||||
if (!identityMembershipOrg) throw ResourceNotFoundError({
|
||||
message: `Failed to find identity with id ${identityId}`
|
||||
});
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: identityMembershipOrg.organization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.Identity
|
||||
);
|
||||
|
||||
const identityRolePermission = await getOrgRolePermissions(
|
||||
identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role,
|
||||
identityMembershipOrg.organization.toString()
|
||||
);
|
||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, identityRolePermission);
|
||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||
message: "Failed to delete more privileged identity"
|
||||
});
|
||||
|
||||
const identity = await Identity.findByIdAndDelete(identityMembershipOrg.identity);
|
||||
if (!identity) throw ResourceNotFoundError({
|
||||
message: `Identity with id ${identityId} not found`
|
||||
});
|
||||
|
||||
await IdentityMembershipOrg.findByIdAndDelete(identityMembershipOrg._id);
|
||||
|
||||
await IdentityMembership.deleteMany({
|
||||
identity: identityMembershipOrg.identity
|
||||
});
|
||||
|
||||
await IdentityUniversalAuth.deleteMany({
|
||||
identity: identityMembershipOrg.identity
|
||||
});
|
||||
|
||||
await IdentityUniversalAuthClientSecret.deleteMany({
|
||||
identity: identityMembershipOrg.identity
|
||||
});
|
||||
|
||||
await IdentityAccessToken.deleteMany({
|
||||
identity: identityMembershipOrg.identity
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.DELETE_IDENTITY,
|
||||
metadata: {
|
||||
identityId: identity._id.toString()
|
||||
}
|
||||
},
|
||||
{
|
||||
organizationId: identityMembershipOrg.organization
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
identity
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
31
backend-mongo/src/ee/controllers/v1/index.ts
Normal file
31
backend-mongo/src/ee/controllers/v1/index.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import * as identitiesController from "./identitiesController";
|
||||
import * as secretController from "./secretController";
|
||||
import * as secretSnapshotController from "./secretSnapshotController";
|
||||
import * as organizationsController from "./organizationsController";
|
||||
import * as ssoController from "./ssoController";
|
||||
import * as usersController from "./usersController";
|
||||
import * as workspaceController from "./workspaceController";
|
||||
import * as membershipController from "./membershipController";
|
||||
import * as cloudProductsController from "./cloudProductsController";
|
||||
import * as roleController from "./roleController";
|
||||
import * as secretApprovalPolicyController from "./secretApprovalPolicyController";
|
||||
import * as secretApprovalRequestController from "./secretApprovalRequestsController";
|
||||
import * as secretRotationProviderController from "./secretRotationProviderController";
|
||||
import * as secretRotationController from "./secretRotationController";
|
||||
|
||||
export {
|
||||
identitiesController,
|
||||
secretController,
|
||||
secretSnapshotController,
|
||||
organizationsController,
|
||||
ssoController,
|
||||
usersController,
|
||||
workspaceController,
|
||||
membershipController,
|
||||
cloudProductsController,
|
||||
roleController,
|
||||
secretApprovalPolicyController,
|
||||
secretApprovalRequestController,
|
||||
secretRotationProviderController,
|
||||
secretRotationController
|
||||
};
|
86
backend-mongo/src/ee/controllers/v1/membershipController.ts
Normal file
86
backend-mongo/src/ee/controllers/v1/membershipController.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { Request, Response } from "express";
|
||||
import { IUser, Membership, Workspace } from "../../../models";
|
||||
import { EventType } from "../../../ee/models";
|
||||
import { IMembershipPermission } from "../../../models/membership";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
|
||||
import { ADMIN, MEMBER } from "../../../variables/organization";
|
||||
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../../variables";
|
||||
import _ from "lodash";
|
||||
import { EEAuditLogService } from "../../services";
|
||||
|
||||
export const denyMembershipPermissions = async (req: Request, res: Response) => {
|
||||
const { membershipId } = req.params;
|
||||
const { permissions } = req.body;
|
||||
const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => {
|
||||
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" })
|
||||
}
|
||||
|
||||
return {
|
||||
environmentSlug: permission.environmentSlug,
|
||||
ability: permission.ability
|
||||
}
|
||||
})
|
||||
|
||||
const sanitizedMembershipPermissionsUnique = _.uniqWith(sanitizedMembershipPermissions, _.isEqual)
|
||||
|
||||
const membershipToModify = await Membership.findById(membershipId)
|
||||
if (!membershipToModify) {
|
||||
throw BadRequestError({ message: "Unable to locate resource" })
|
||||
}
|
||||
|
||||
// check if the user making the request is a admin of this project
|
||||
if (![ADMIN, MEMBER].includes(membershipToModify.role)) {
|
||||
throw UnauthorizedRequestError()
|
||||
}
|
||||
|
||||
// check if the requested slugs are indeed a part of this related workspace
|
||||
const relatedWorkspace = await Workspace.findById(membershipToModify.workspace)
|
||||
if (!relatedWorkspace) {
|
||||
throw BadRequestError({ message: "Something went wrong when locating the related workspace" })
|
||||
}
|
||||
|
||||
const uniqueEnvironmentSlugs = new Set(_.uniq(_.map(relatedWorkspace.environments, "slug")));
|
||||
|
||||
sanitizedMembershipPermissionsUnique.forEach(permission => {
|
||||
if (!uniqueEnvironmentSlugs.has(permission.environmentSlug)) {
|
||||
throw BadRequestError({ message: "Unknown environment slug reference" })
|
||||
}
|
||||
})
|
||||
|
||||
// update the permissions
|
||||
const updatedMembershipWithPermissions = await Membership.findByIdAndUpdate(
|
||||
{ _id: membershipToModify._id },
|
||||
{ $set: { deniedPermissions: sanitizedMembershipPermissionsUnique } },
|
||||
{ new: true }
|
||||
).populate<{ user: IUser }>("user");
|
||||
|
||||
if (!updatedMembershipWithPermissions) {
|
||||
throw BadRequestError({ message: "The resource has been removed before it can be modified" })
|
||||
}
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS,
|
||||
metadata: {
|
||||
userId: updatedMembershipWithPermissions.user._id.toString(),
|
||||
email: updatedMembershipWithPermissions.user.email,
|
||||
deniedPermissions: updatedMembershipWithPermissions.deniedPermissions.map(({
|
||||
environmentSlug,
|
||||
ability
|
||||
}) => ({
|
||||
environmentSlug,
|
||||
ability
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: updatedMembershipWithPermissions.workspace
|
||||
}
|
||||
);
|
||||
|
||||
res.send({
|
||||
permissionsDenied: updatedMembershipWithPermissions.deniedPermissions,
|
||||
})
|
||||
}
|
550
backend-mongo/src/ee/controllers/v1/organizationsController.ts
Normal file
550
backend-mongo/src/ee/controllers/v1/organizationsController.ts
Normal file
@ -0,0 +1,550 @@
|
||||
import { Types } from "mongoose";
|
||||
import { Request, Response } from "express";
|
||||
import { getLicenseServerUrl } from "../../../config";
|
||||
import { licenseServerKeyRequest } from "../../../config/request";
|
||||
import { EELicenseService } from "../../services";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../../validation/organization";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
getAuthDataOrgPermissions,
|
||||
} from "../../services/RoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Organization } from "../../../models";
|
||||
import { OrganizationNotFoundError } from "../../../utils/errors";
|
||||
|
||||
export const getOrganizationPlansTable = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { billingCycle },
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgPlansTablev1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const { data } = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the organization current plan's feature set
|
||||
*/
|
||||
export const getOrganizationPlan = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId },
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgPlanv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const plan = await EELicenseService.getPlan(
|
||||
new Types.ObjectId(organizationId),
|
||||
new Types.ObjectId(workspaceId)
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
plan
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return checkout url for pro trial
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const startOrganizationTrial = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId },
|
||||
body: { success_url }
|
||||
} = await validateRequest(reqValidator.StartOrgTrailv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { url }
|
||||
} = await licenseServerKeyRequest.post(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/session/trial`,
|
||||
{
|
||||
success_url
|
||||
}
|
||||
);
|
||||
|
||||
EELicenseService.delPlan(new Types.ObjectId(organizationId));
|
||||
|
||||
return res.status(200).send({
|
||||
url
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the organization's current plan's billing info
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/cloud-plan/billing`
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the organization's current plan's feature table
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationPlanTable = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgPlanTablev1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/cloud-plan/table`
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
export const getOrganizationBillingDetails = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgBillingDetailsv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details`
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
export const updateOrganizationBillingDetails = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId },
|
||||
body: { name, email }
|
||||
} = await validateRequest(reqValidator.UpdateOrgBillingDetailsv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerKeyRequest.patch(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details`,
|
||||
{
|
||||
...(name ? { name } : {}),
|
||||
...(email ? { email } : {})
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the organization's payment methods on file
|
||||
*/
|
||||
export const getOrganizationPmtMethods = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgPmtMethodsv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { pmtMethods }
|
||||
} = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/payment-methods`
|
||||
);
|
||||
|
||||
return res.status(200).send(pmtMethods);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return URL to add payment method for organization
|
||||
*/
|
||||
export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId },
|
||||
body: { success_url, cancel_url }
|
||||
} = await validateRequest(reqValidator.CreateOrgPmtMethodv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { url }
|
||||
} = await licenseServerKeyRequest.post(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/payment-methods`,
|
||||
{
|
||||
success_url,
|
||||
cancel_url
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
url
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete payment method with id [pmtMethodId] for organization
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId, pmtMethodId }
|
||||
} = await validateRequest(reqValidator.DelOrgPmtMethodv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerKeyRequest.delete(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/payment-methods/${pmtMethodId}`
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the organization's tax ids on file
|
||||
*/
|
||||
export const getOrganizationTaxIds = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgTaxIdsv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { tax_ids }
|
||||
} = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/tax-ids`
|
||||
);
|
||||
|
||||
return res.status(200).send(tax_ids);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add tax id to organization
|
||||
*/
|
||||
export const addOrganizationTaxId = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId },
|
||||
body: { type, value }
|
||||
} = await validateRequest(reqValidator.CreateOrgTaxId, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerKeyRequest.post(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/tax-ids`,
|
||||
{
|
||||
type,
|
||||
value
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete tax id with id [taxId] from organization tax ids on file
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId, taxId }
|
||||
} = await validateRequest(reqValidator.DelOrgTaxIdv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerKeyRequest.delete(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/tax-ids/${taxId}`
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return organization's invoices on file
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationInvoices = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgInvoicesv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { invoices }
|
||||
} = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/invoices`
|
||||
);
|
||||
|
||||
return res.status(200).send(invoices);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return organization's licenses on file
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationLicenses = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgLicencesv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { licenses }
|
||||
} = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/licenses`
|
||||
);
|
||||
|
||||
return res.status(200).send(licenses);
|
||||
};
|
290
backend-mongo/src/ee/controllers/v1/roleController.ts
Normal file
290
backend-mongo/src/ee/controllers/v1/roleController.ts
Normal file
@ -0,0 +1,290 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Membership, User } from "../../../models";
|
||||
import {
|
||||
CreateRoleSchema,
|
||||
DeleteRoleSchema,
|
||||
GetRoleSchema,
|
||||
GetUserPermission,
|
||||
GetUserProjectPermission,
|
||||
UpdateRoleSchema
|
||||
} from "../../validation/role";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
adminProjectPermissions,
|
||||
getAuthDataProjectPermissions,
|
||||
memberProjectPermissions,
|
||||
noAccessProjectPermissions,
|
||||
viewerProjectPermission
|
||||
} from "../../services/ProjectRoleService";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
adminPermissions,
|
||||
getAuthDataOrgPermissions,
|
||||
getUserOrgPermissions,
|
||||
memberPermissions,
|
||||
noAccessPermissions
|
||||
} from "../../services/RoleService";
|
||||
import { BadRequestError } from "../../../utils/errors";
|
||||
import { Role } from "../../models";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
|
||||
export const createRole = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { workspaceId, name, description, slug, permissions, orgId }
|
||||
} = await validateRequest(CreateRoleSchema, req);
|
||||
|
||||
const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule
|
||||
if (isOrgRole) {
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(orgId)
|
||||
});
|
||||
|
||||
if (permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Role)) {
|
||||
throw BadRequestError({ message: "user doesn't have the permission." });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
if (permission.cannot(ProjectPermissionActions.Create, ProjectPermissionSub.Role)) {
|
||||
throw BadRequestError({ message: "User doesn't have the permission." });
|
||||
}
|
||||
}
|
||||
|
||||
const existingRole = await Role.findOne({ organization: orgId, workspace: workspaceId, slug });
|
||||
if (existingRole) {
|
||||
throw BadRequestError({ message: "Role already exist" });
|
||||
}
|
||||
|
||||
const role = new Role({
|
||||
organization: orgId,
|
||||
workspace: workspaceId,
|
||||
isOrgRole,
|
||||
name,
|
||||
slug,
|
||||
permissions,
|
||||
description
|
||||
});
|
||||
await role.save();
|
||||
|
||||
res.status(200).json({
|
||||
message: "Successfully created role",
|
||||
data: {
|
||||
role
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const updateRole = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id },
|
||||
body: { name, description, slug, permissions, workspaceId, orgId }
|
||||
} = await validateRequest(UpdateRoleSchema, req);
|
||||
const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule
|
||||
|
||||
if (isOrgRole) {
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(orgId)
|
||||
});
|
||||
if (permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.Role)) {
|
||||
throw BadRequestError({ message: "User doesn't have the org permission." });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Role)) {
|
||||
throw BadRequestError({ message: "User doesn't have the workspace permission." });
|
||||
}
|
||||
}
|
||||
|
||||
if (slug) {
|
||||
const existingRole = await Role.findOne({
|
||||
organization: orgId,
|
||||
slug,
|
||||
isOrgRole,
|
||||
workspace: workspaceId
|
||||
});
|
||||
if (existingRole && existingRole.id !== id) {
|
||||
throw BadRequestError({ message: "Role already exist" });
|
||||
}
|
||||
}
|
||||
|
||||
const role = await Role.findByIdAndUpdate(
|
||||
id,
|
||||
{ name, description, slug, permissions },
|
||||
{ returnDocument: "after" }
|
||||
);
|
||||
|
||||
if (!role) {
|
||||
throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
res.status(200).json({
|
||||
message: "Successfully updated role",
|
||||
data: {
|
||||
role
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteRole = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(DeleteRoleSchema, req);
|
||||
|
||||
const role = await Role.findById(id);
|
||||
if (!role) {
|
||||
throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
|
||||
const isOrgRole = !role.workspace;
|
||||
if (isOrgRole) {
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: role.organization
|
||||
});
|
||||
if (permission.cannot(OrgPermissionActions.Delete, OrgPermissionSubjects.Role)) {
|
||||
throw BadRequestError({ message: "User doesn't have the org permission." });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: role.workspace
|
||||
});
|
||||
|
||||
if (permission.cannot(ProjectPermissionActions.Delete, ProjectPermissionSub.Role)) {
|
||||
throw BadRequestError({ message: "User doesn't have the workspace permission." });
|
||||
}
|
||||
}
|
||||
|
||||
await Role.findByIdAndDelete(role.id);
|
||||
|
||||
res.status(200).json({
|
||||
message: "Successfully deleted role",
|
||||
data: {
|
||||
role
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getRoles = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId, orgId }
|
||||
} = await validateRequest(GetRoleSchema, req);
|
||||
|
||||
const isOrgRole = !workspaceId;
|
||||
if (isOrgRole) {
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(orgId)
|
||||
});
|
||||
if (permission.cannot(OrgPermissionActions.Read, OrgPermissionSubjects.Role)) {
|
||||
throw BadRequestError({ message: "User doesn't have the org permission." });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (permission.cannot(ProjectPermissionActions.Read, ProjectPermissionSub.Role)) {
|
||||
throw BadRequestError({ message: "User doesn't have the workspace permission." });
|
||||
}
|
||||
}
|
||||
|
||||
const customRoles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId });
|
||||
// as this is shared between org and workspace switch the rule set based on it
|
||||
const roles = [
|
||||
{
|
||||
_id: "admin",
|
||||
name: "Admin",
|
||||
slug: "admin",
|
||||
description: "Complete administration access over the organization",
|
||||
permissions: isOrgRole ? adminPermissions.rules : adminProjectPermissions.rules
|
||||
},
|
||||
{
|
||||
_id: "no-access",
|
||||
name: "No Access",
|
||||
slug: "no-access",
|
||||
description: "No access to any resources in the organization",
|
||||
permissions: isOrgRole ? noAccessPermissions.rules : noAccessProjectPermissions.rules
|
||||
},
|
||||
{
|
||||
_id: "member",
|
||||
name: isOrgRole ? "Member" : "Developer",
|
||||
slug: "member",
|
||||
description: "Non-administrative role in an organization",
|
||||
permissions: isOrgRole ? memberPermissions.rules : memberProjectPermissions.rules
|
||||
},
|
||||
// viewer role only for project level
|
||||
...(isOrgRole
|
||||
? []
|
||||
: [
|
||||
{
|
||||
_id: "viewer",
|
||||
name: "Viewer",
|
||||
slug: "viewer",
|
||||
description: "Non-administrative role in an organization",
|
||||
permissions: viewerProjectPermission.rules
|
||||
}
|
||||
]),
|
||||
...customRoles
|
||||
];
|
||||
|
||||
res.status(200).json({
|
||||
message: "Successfully fetched role list",
|
||||
data: {
|
||||
roles
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserPermissions = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { orgId }
|
||||
} = await validateRequest(GetUserPermission, req);
|
||||
|
||||
const { permission, membership } = await getUserOrgPermissions(req.user._id, orgId);
|
||||
|
||||
res.status(200).json({
|
||||
data: {
|
||||
permissions: packRules(permission.rules),
|
||||
membership
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserWorkspacePermissions = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(GetUserProjectPermission, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
let membership;
|
||||
if (req.authData.authPayload instanceof User) {
|
||||
membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
})
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
data: {
|
||||
permissions: packRules(permission.rules),
|
||||
membership
|
||||
}
|
||||
});
|
||||
};
|
@ -0,0 +1,143 @@
|
||||
import { Types } from "mongoose";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { Request, Response } from "express";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import { SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
|
||||
import { getSecretPolicyOfBoard } from "../../services/SecretApprovalService";
|
||||
import { BadRequestError } from "../../../utils/errors";
|
||||
import * as reqValidator from "../../validation/secretApproval";
|
||||
|
||||
const ERR_SECRET_APPROVAL_NOT_FOUND = BadRequestError({ message: "secret approval not found" });
|
||||
|
||||
export const createSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { approvals, secretPath, approvers, environment, workspaceId, name }
|
||||
} = await validateRequest(reqValidator.CreateSecretApprovalRule, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const secretApproval = new SecretApprovalPolicy({
|
||||
workspace: workspaceId,
|
||||
name: name ?? `${environment}-${nanoid(3)}`,
|
||||
secretPath,
|
||||
environment,
|
||||
approvals,
|
||||
approvers
|
||||
});
|
||||
await secretApproval.save();
|
||||
|
||||
return res.send({
|
||||
approval: secretApproval
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { approvals, approvers, secretPath, name },
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.UpdateSecretApprovalRule, req);
|
||||
|
||||
const secretApproval = await SecretApprovalPolicy.findById(id);
|
||||
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: secretApproval.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const updatedDoc = await SecretApprovalPolicy.findByIdAndUpdate(id, {
|
||||
approvals,
|
||||
approvers,
|
||||
name: (name || secretApproval?.name) ?? `${secretApproval.environment}-${nanoid(3)}`,
|
||||
...(secretPath === null ? { $unset: { secretPath: 1 } } : { secretPath })
|
||||
});
|
||||
|
||||
return res.send({
|
||||
approval: updatedDoc
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.DeleteSecretApprovalRule, req);
|
||||
|
||||
const secretApproval = await SecretApprovalPolicy.findById(id);
|
||||
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: secretApproval.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const deletedDoc = await SecretApprovalPolicy.findByIdAndDelete(id);
|
||||
|
||||
return res.send({
|
||||
approval: deletedDoc
|
||||
});
|
||||
};
|
||||
|
||||
export const getSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetSecretApprovalRuleList, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const doc = await SecretApprovalPolicy.find({ workspace: workspaceId });
|
||||
|
||||
return res.send({
|
||||
approvals: doc
|
||||
});
|
||||
};
|
||||
|
||||
export const getSecretApprovalPolicyOfBoard = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId, environment, secretPath }
|
||||
} = await validateRequest(reqValidator.GetSecretApprovalPolicyOfABoard, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { secretPath, environment })
|
||||
);
|
||||
|
||||
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
|
||||
return res.send({ policy: secretApprovalPolicy });
|
||||
};
|
@ -0,0 +1,366 @@
|
||||
import { Request, Response } from "express";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import { Folder, Membership, User } from "../../../models";
|
||||
import { ApprovalStatus, SecretApprovalRequest } from "../../models/secretApprovalRequest";
|
||||
import * as reqValidator from "../../validation/secretApprovalRequest";
|
||||
import { getFolderWithPathFromId } from "../../../services/FolderService";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
|
||||
import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
|
||||
import { performSecretApprovalRequestMerge } from "../../services/SecretApprovalService";
|
||||
import { Types } from "mongoose";
|
||||
import { EEAuditLogService } from "../../services";
|
||||
import { EventType } from "../../models";
|
||||
|
||||
export const getSecretApprovalRequestCount = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId }
|
||||
} = await validateRequest(reqValidator.getSecretApprovalRequestCount, req);
|
||||
|
||||
if (!(req.authData.authPayload instanceof User)) return;
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
const approvalRequestCount = await SecretApprovalRequest.aggregate([
|
||||
{
|
||||
$match: {
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: SecretApprovalPolicy.collection.name,
|
||||
localField: "policy",
|
||||
foreignField: "_id",
|
||||
as: "policy"
|
||||
}
|
||||
},
|
||||
{ $unwind: "$policy" },
|
||||
...(membership.role !== "admin"
|
||||
? [
|
||||
{
|
||||
$match: {
|
||||
$or: [
|
||||
{ committer: new Types.ObjectId(membership.id) },
|
||||
{ "policy.approvers": new Types.ObjectId(membership.id) }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
$group: {
|
||||
_id: "$status",
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
}
|
||||
]);
|
||||
const openRequests = approvalRequestCount.find(({ _id }) => _id === "open");
|
||||
const closedRequests = approvalRequestCount.find(({ _id }) => _id === "close");
|
||||
|
||||
return res.send({
|
||||
approvals: { open: openRequests?.count || 0, closed: closedRequests?.count || 0 }
|
||||
});
|
||||
};
|
||||
|
||||
export const getSecretApprovalRequests = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { status, committer, workspaceId, environment, limit, offset }
|
||||
} = await validateRequest(reqValidator.getSecretApprovalRequests, req);
|
||||
|
||||
if (!(req.authData.authPayload instanceof User)) return;
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
const query = {
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
committer: committer ? new Types.ObjectId(committer) : undefined,
|
||||
status
|
||||
};
|
||||
// to strip of undefined in query we use es6 spread to ignore those fields
|
||||
Object.entries(query).forEach(
|
||||
([key, value]) => value === undefined && delete query[key as keyof typeof query]
|
||||
);
|
||||
const approvalRequests = await SecretApprovalRequest.aggregate([
|
||||
{
|
||||
$match: query
|
||||
},
|
||||
{ $sort: { createdAt: -1 } },
|
||||
{
|
||||
$lookup: {
|
||||
from: SecretApprovalPolicy.collection.name,
|
||||
localField: "policy",
|
||||
foreignField: "_id",
|
||||
as: "policy"
|
||||
}
|
||||
},
|
||||
{ $unwind: "$policy" },
|
||||
...(membership.role !== "admin"
|
||||
? [
|
||||
{
|
||||
$match: {
|
||||
$or: [
|
||||
{ committer: new Types.ObjectId(membership.id) },
|
||||
{ "policy.approvers": new Types.ObjectId(membership.id) }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{ $skip: offset },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
if (!approvalRequests.length) return res.send({ approvals: [] });
|
||||
|
||||
const unqiueEnvs = environment ?? {
|
||||
$in: [...new Set(approvalRequests.map(({ environment }) => environment))]
|
||||
};
|
||||
const approvalRootFolders = await Folder.find({
|
||||
workspace: workspaceId,
|
||||
environment: unqiueEnvs
|
||||
}).lean();
|
||||
|
||||
const formatedApprovals = approvalRequests.map((el) => {
|
||||
let secretPath = "/";
|
||||
const folders = approvalRootFolders.find(({ environment }) => environment === el.environment);
|
||||
if (folders) {
|
||||
secretPath = getFolderWithPathFromId(folders?.nodes, el.folderId)?.folderPath || "/";
|
||||
}
|
||||
return { ...el, secretPath };
|
||||
});
|
||||
|
||||
return res.send({
|
||||
approvals: formatedApprovals
|
||||
});
|
||||
};
|
||||
|
||||
export const getSecretApprovalRequestDetails = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.getSecretApprovalRequestDetails, req);
|
||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id)
|
||||
.populate<{ policy: ISecretApprovalPolicy }>("policy")
|
||||
.populate({
|
||||
path: "commits.secretVersion",
|
||||
populate: {
|
||||
path: "tags"
|
||||
}
|
||||
})
|
||||
.populate("commits.secret", "version")
|
||||
.populate("commits.newVersion.tags")
|
||||
.lean();
|
||||
if (!secretApprovalRequest)
|
||||
throw BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
if (!(req.authData.authPayload instanceof User)) return;
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: secretApprovalRequest.workspace
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
// allow to fetch only if its admin or is the committer or approver
|
||||
if (
|
||||
membership.role !== "admin" &&
|
||||
!secretApprovalRequest.committer.equals(membership.id) &&
|
||||
!secretApprovalRequest.policy.approvers.find(
|
||||
(approverId) => approverId.toString() === membership._id.toString()
|
||||
)
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
let secretPath = "/";
|
||||
const approvalRootFolders = await Folder.findOne({
|
||||
workspace: secretApprovalRequest.workspace,
|
||||
environment: secretApprovalRequest.environment
|
||||
}).lean();
|
||||
if (approvalRootFolders) {
|
||||
secretPath =
|
||||
getFolderWithPathFromId(approvalRootFolders?.nodes, secretApprovalRequest.folderId)
|
||||
?.folderPath || "/";
|
||||
}
|
||||
|
||||
return res.send({
|
||||
approval: { ...secretApprovalRequest, secretPath }
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSecretApprovalReviewStatus = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { status },
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.updateSecretApprovalReviewStatus, req);
|
||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
|
||||
policy: ISecretApprovalPolicy;
|
||||
}>("policy");
|
||||
if (!secretApprovalRequest)
|
||||
throw BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
if (!(req.authData.authPayload instanceof User)) return;
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: secretApprovalRequest.workspace
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
if (
|
||||
membership.role !== "admin" &&
|
||||
secretApprovalRequest.committer !== membership.id &&
|
||||
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
const reviewerPos = secretApprovalRequest.reviewers.findIndex(
|
||||
({ member }) => member.toString() === membership._id.toString()
|
||||
);
|
||||
if (reviewerPos !== -1) {
|
||||
secretApprovalRequest.reviewers[reviewerPos].status = status;
|
||||
} else {
|
||||
secretApprovalRequest.reviewers.push({ member: membership._id, status });
|
||||
}
|
||||
await secretApprovalRequest.save();
|
||||
|
||||
return res.send({ status });
|
||||
};
|
||||
|
||||
export const mergeSecretApprovalRequest = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.mergeSecretApprovalRequest, req);
|
||||
|
||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
|
||||
policy: ISecretApprovalPolicy;
|
||||
}>("policy");
|
||||
|
||||
if (!secretApprovalRequest)
|
||||
throw BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
if (!(req.authData.authPayload instanceof User)) return;
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: secretApprovalRequest.workspace
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
if (
|
||||
membership.role !== "admin" &&
|
||||
secretApprovalRequest.committer !== membership.id &&
|
||||
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
|
||||
(prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status }),
|
||||
{}
|
||||
);
|
||||
const hasMinApproval =
|
||||
secretApprovalRequest.policy.approvals <=
|
||||
secretApprovalRequest.policy.approvers.filter(
|
||||
(approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
|
||||
).length;
|
||||
|
||||
if (!hasMinApproval) throw BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
||||
|
||||
const approval = await performSecretApprovalRequestMerge(
|
||||
id,
|
||||
req.authData,
|
||||
membership._id.toString()
|
||||
);
|
||||
return res.send({ approval });
|
||||
};
|
||||
|
||||
export const updateSecretApprovalRequestStatus = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { status },
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.updateSecretApprovalRequestStatus, req);
|
||||
|
||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
|
||||
policy: ISecretApprovalPolicy;
|
||||
}>("policy");
|
||||
|
||||
if (!secretApprovalRequest)
|
||||
throw BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
if (!(req.authData.authPayload instanceof User)) return;
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: secretApprovalRequest.workspace
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
if (
|
||||
membership.role !== "admin" &&
|
||||
secretApprovalRequest.committer !== membership.id &&
|
||||
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership._id))
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
if (secretApprovalRequest.hasMerged)
|
||||
throw BadRequestError({ message: "Approval request has been merged" });
|
||||
if (secretApprovalRequest.status === "close" && status === "close")
|
||||
throw BadRequestError({ message: "Approval request is already closed" });
|
||||
if (secretApprovalRequest.status === "open" && status === "open")
|
||||
throw BadRequestError({ message: "Approval request is already open" });
|
||||
|
||||
const updatedRequest = await SecretApprovalRequest.findByIdAndUpdate(
|
||||
id,
|
||||
{ status, statusChangeBy: membership._id },
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (status === "close") {
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.SECRET_APPROVAL_CLOSED,
|
||||
metadata: {
|
||||
closedBy: membership._id.toString(),
|
||||
secretApprovalRequestId: id,
|
||||
secretApprovalRequestSlug: secretApprovalRequest.slug
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: secretApprovalRequest.workspace
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.SECRET_APPROVAL_REOPENED,
|
||||
metadata: {
|
||||
reopenedBy: membership._id.toString(),
|
||||
secretApprovalRequestId: id,
|
||||
secretApprovalRequestSlug: secretApprovalRequest.slug
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: secretApprovalRequest.workspace
|
||||
}
|
||||
);
|
||||
}
|
||||
return res.send({ approval: updatedRequest });
|
||||
};
|
269
backend-mongo/src/ee/controllers/v1/secretController.ts
Normal file
269
backend-mongo/src/ee/controllers/v1/secretController.ts
Normal file
@ -0,0 +1,269 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { Request, Response } from "express";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import { Folder, Secret } from "../../../models";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { BadRequestError } from "../../../utils/errors";
|
||||
import * as reqValidator from "../../../validation";
|
||||
import { SecretVersion } from "../../models";
|
||||
import { EESecretService } from "../../services";
|
||||
import { getFolderWithPathFromId } from "../../../services/FolderService";
|
||||
|
||||
/**
|
||||
* Return secret versions for secret with id [secretId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getSecretVersions = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return secret versions'
|
||||
#swagger.description = 'Return secret versions'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['secretId'] = {
|
||||
"description": "ID of secret",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['offset'] = {
|
||||
"description": "Number of versions to skip",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['limit'] = {
|
||||
"description": "Maximum number of versions to return",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secretVersions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/SecretVersion"
|
||||
},
|
||||
"description": "Secret versions"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { secretId },
|
||||
query: { offset, limit }
|
||||
} = await validateRequest(reqValidator.GetSecretVersionsV1, req);
|
||||
|
||||
const secret = await Secret.findById(secretId);
|
||||
if (!secret) {
|
||||
throw BadRequestError({ message: "Failed to find secret" });
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: secret.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRollback
|
||||
);
|
||||
|
||||
const secretVersions = await SecretVersion.find({
|
||||
secret: secretId
|
||||
})
|
||||
.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
|
||||
*/
|
||||
export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Roll back secret to a version.'
|
||||
#swagger.description = 'Roll back secret to a version.'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['secretId'] = {
|
||||
"description": "ID of secret",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"description": "Version of secret to roll back to"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "object",
|
||||
$ref: "#/components/schemas/Secret",
|
||||
"description": "Secret rolled back to"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
const {
|
||||
params: { secretId },
|
||||
body: { version }
|
||||
} = await validateRequest(reqValidator.RollbackSecretVersionV1, req);
|
||||
|
||||
const toBeUpdatedSec = await Secret.findById(secretId);
|
||||
if (!toBeUpdatedSec) {
|
||||
throw BadRequestError({ message: "Failed to find secret" });
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: toBeUpdatedSec.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretRollback
|
||||
);
|
||||
|
||||
// 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;
|
||||
|
||||
let secretPath = "/";
|
||||
const folders = await Folder.findOne({ workspace, environment });
|
||||
if (folders)
|
||||
secretPath = getFolderWithPathFromId(folders.nodes, folder || "root")?.folderPath || "/";
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment, secretPath })
|
||||
);
|
||||
|
||||
// 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
|
||||
});
|
||||
};
|
110
backend-mongo/src/ee/controllers/v1/secretRotationController.ts
Normal file
110
backend-mongo/src/ee/controllers/v1/secretRotationController.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/secretRotation";
|
||||
import * as secretRotationService from "../../secretRotation/service";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
export const createSecretRotation = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: {
|
||||
provider,
|
||||
customProvider,
|
||||
interval,
|
||||
outputs,
|
||||
secretPath,
|
||||
environment,
|
||||
workspaceId,
|
||||
inputs
|
||||
}
|
||||
} = await validateRequest(reqValidator.createSecretRotationV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const secretRotation = await secretRotationService.createSecretRotation({
|
||||
workspaceId,
|
||||
inputs,
|
||||
environment,
|
||||
secretPath,
|
||||
outputs,
|
||||
interval,
|
||||
customProvider,
|
||||
provider
|
||||
});
|
||||
|
||||
return res.send({ secretRotation });
|
||||
};
|
||||
|
||||
export const restartSecretRotations = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { id }
|
||||
} = await validateRequest(reqValidator.restartSecretRotationV1, req);
|
||||
|
||||
const doc = await secretRotationService.getSecretRotationById({ id });
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: doc.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const secretRotation = await secretRotationService.restartSecretRotation({ id });
|
||||
return res.send({ secretRotation });
|
||||
};
|
||||
|
||||
export const deleteSecretRotations = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.removeSecretRotationV1, req);
|
||||
|
||||
const doc = await secretRotationService.getSecretRotationById({ id });
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: doc.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const secretRotations = await secretRotationService.deleteSecretRotation({ id });
|
||||
return res.send({ secretRotations });
|
||||
};
|
||||
|
||||
export const getSecretRotations = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId }
|
||||
} = await validateRequest(reqValidator.getSecretRotationV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const secretRotations = await secretRotationService.getSecretRotationOfWorkspace(workspaceId);
|
||||
return res.send({ secretRotations });
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/secretRotationProvider";
|
||||
import * as secretRotationProviderService from "../../secretRotation/service";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
export const getProviderTemplates = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.getSecretRotationProvidersV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const rotationProviderList = await secretRotationProviderService.getProviderTemplate({
|
||||
workspaceId
|
||||
});
|
||||
|
||||
return res.send(rotationProviderList);
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Request, Response } from "express";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import * as reqValidator from "../../../validation/secretSnapshot";
|
||||
import { ISecretVersion, SecretSnapshot, TFolderRootVersionSchema } from "../../models";
|
||||
|
||||
/**
|
||||
* Return secret snapshot with id [secretSnapshotId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getSecretSnapshot = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { secretSnapshotId }
|
||||
} = await validateRequest(reqValidator.GetSecretSnapshotV1, req);
|
||||
|
||||
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 { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: secretSnapshot.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRollback
|
||||
);
|
||||
|
||||
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
|
||||
});
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user