Compare commits
666 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b28ab3be1 | ||
|
|
c5bcc340fa | ||
|
|
bff54fbfdb | ||
|
|
867c6ba056 | ||
|
|
d1ecf03f44 | ||
|
|
fc43608eec | ||
|
|
15dd05c78d | ||
|
|
aa6f774f65 | ||
|
|
379f885354 | ||
|
|
39a5f2dbe8 | ||
|
|
0daaa9b175 | ||
|
|
0c085d21ce | ||
|
|
dcaaa430f0 | ||
|
|
2cda54ceff | ||
|
|
525e6bb65a | ||
|
|
62cebebd3d | ||
|
|
3646f14042 | ||
|
|
813e889c97 | ||
|
|
8bcd0ab0c6 | ||
|
|
5725d297b4 | ||
|
|
a428f05e77 | ||
|
|
467ecfdc99 | ||
|
|
ed8091a994 | ||
|
|
56cad93e0f | ||
|
|
3cf67e0b8d | ||
|
|
5800aceb2d | ||
|
|
729b563160 | ||
|
|
6b5618a5fc | ||
|
|
2aa72eb240 | ||
|
|
c8655c4f89 | ||
|
|
daaa03d1b3 | ||
|
|
9e5b94924f | ||
|
|
f21089900e | ||
|
|
0c0e632bc9 | ||
|
|
a13a5bd1d8 | ||
|
|
3b34b429f3 | ||
|
|
97ffd17789 | ||
|
|
10c5476d31 | ||
|
|
d3626eba2a | ||
|
|
de157b2654 | ||
|
|
337cbfaf22 | ||
|
|
f88b6d961e | ||
|
|
0426051541 | ||
|
|
4556f668de | ||
|
|
da8225a3bd | ||
|
|
f10e6b6ac2 | ||
|
|
7ec00d3850 | ||
|
|
8f8d7418ed | ||
|
|
af6d17b701 | ||
|
|
61183d001c | ||
|
|
024d12db08 | ||
|
|
dc7951efaf | ||
|
|
06e14fea55 | ||
|
|
0f656b4889 | ||
|
|
6fa1dc50be | ||
|
|
2bb41367bc | ||
|
|
20d8886bfa | ||
|
|
59ef82b740 | ||
|
|
fc543154c0 | ||
|
|
569b464157 | ||
|
|
adf83c698d | ||
|
|
8fcbc58ee2 | ||
|
|
2dcbb2be59 | ||
|
|
7026e004e1 | ||
|
|
a3084feaee | ||
|
|
e7d36de784 | ||
|
|
54cc47b14e | ||
|
|
fac44888cd | ||
|
|
9f056523c9 | ||
|
|
0af1ef387d | ||
|
|
f95f40be15 | ||
|
|
5c859e2e6c | ||
|
|
03ff5e6ece | ||
|
|
52d696aa74 | ||
|
|
a4e80712dd | ||
|
|
a947e434f0 | ||
|
|
2eb4f290a5 | ||
|
|
8ae799a771 | ||
|
|
9a5f3a5015 | ||
|
|
1ca0d6e245 | ||
|
|
7f69eebeb1 | ||
|
|
32bd9b83a3 | ||
|
|
477d60de49 | ||
|
|
1ba8275dcb | ||
|
|
a0a4994250 | ||
|
|
32dfa41970 | ||
|
|
f92efda0f0 | ||
|
|
3b0f643e9d | ||
|
|
5bcee24f88 | ||
|
|
9e3d7ea44c | ||
|
|
8cc6dac893 | ||
|
|
b7c4316c77 | ||
|
|
0c295d5e6e | ||
|
|
bc49d1f90d | ||
|
|
6f6d9dee83 | ||
|
|
cef5dd4a46 | ||
|
|
79061c0eb5 | ||
|
|
6e2c3fc1cc | ||
|
|
e301fe137f | ||
|
|
af69c83db2 | ||
|
|
53fa8da5b1 | ||
|
|
c58aac585b | ||
|
|
8c1117fcbf | ||
|
|
a6dd4f1206 | ||
|
|
5af1799991 | ||
|
|
a20a641de3 | ||
|
|
8abd38573b | ||
|
|
78abdf0e9d | ||
|
|
dc031d8d86 | ||
|
|
de6330b09d | ||
|
|
68bcc7a4b8 | ||
|
|
c04a1352cb | ||
|
|
5d1c11ceba | ||
|
|
a2aa7c9bc2 | ||
|
|
b3a351ccb2 | ||
|
|
679bc7a59b | ||
|
|
a72d0b518f | ||
|
|
6741b25907 | ||
|
|
24b5784f02 | ||
|
|
eb9b481eba | ||
|
|
64edc49392 | ||
|
|
0d1753ac74 | ||
|
|
a6558f5548 | ||
|
|
62dfeb80f2 | ||
|
|
26cd5d9643 | ||
|
|
e65fbbfc21 | ||
|
|
a2162f4d69 | ||
|
|
c9ed9aa733 | ||
|
|
9b20decdc1 | ||
|
|
adaefc8628 | ||
|
|
c6c45c4c49 | ||
|
|
95494083f2 | ||
|
|
686474f815 | ||
|
|
2c6bd8c9dc | ||
|
|
9366e31452 | ||
|
|
96ff32fb2f | ||
|
|
9342fa5744 | ||
|
|
50fc22966c | ||
|
|
4fab4c74ff | ||
|
|
e38e1a5d5f | ||
|
|
cc91ac6cc0 | ||
|
|
2d8c8e18f7 | ||
|
|
b17e2da2cf | ||
|
|
d121cce0d2 | ||
|
|
0eba7a88fa | ||
|
|
34ac16e9d7 | ||
|
|
906d9e2f1a | ||
|
|
623d84aeb5 | ||
|
|
f8122cd2ca | ||
|
|
9b7e86efc2 | ||
|
|
e7ccfbdd0e | ||
|
|
acc1474394 | ||
|
|
c90b3031a6 | ||
|
|
aaffb2e007 | ||
|
|
e0e95e95e4 | ||
|
|
fa70b440d0 | ||
|
|
42acb2ebb6 | ||
|
|
174bea8d6e | ||
|
|
f68a57950b | ||
|
|
f747bf126b | ||
|
|
1ca197fd46 | ||
|
|
63d05d929b | ||
|
|
ef5bf5d326 | ||
|
|
9d6e35d803 | ||
|
|
0cccdcab83 | ||
|
|
6607faa390 | ||
|
|
6fcf18ab51 | ||
|
|
d122c10573 | ||
|
|
ae9553ca1c | ||
|
|
ff919039c9 | ||
|
|
80eb15d46a | ||
|
|
c36b870c54 | ||
|
|
b7cbca590c | ||
|
|
606a1bbfcb | ||
|
|
3e5369c8dd | ||
|
|
dd5e4cec73 | ||
|
|
a31a040abd | ||
|
|
f0125b95c1 | ||
|
|
072f2e24c2 | ||
|
|
36b5350f9b | ||
|
|
c7489c9fdf | ||
|
|
3181e4e96e | ||
|
|
2ee0d53c5f | ||
|
|
dfa629ecc7 | ||
|
|
92dc48b882 | ||
|
|
367e1ce289 | ||
|
|
7390f34355 | ||
|
|
c47d9f6593 | ||
|
|
5399ee8208 | ||
|
|
117045e6d3 | ||
|
|
912ad64555 | ||
|
|
00855ee31d | ||
|
|
c18a273b4a | ||
|
|
ca24a4adf1 | ||
|
|
a263aaa481 | ||
|
|
0a20ba0020 | ||
|
|
6541600af6 | ||
|
|
525979d5d9 | ||
|
|
7dd1959eba | ||
|
|
e266b39254 | ||
|
|
e935989fee | ||
|
|
25c401f64d | ||
|
|
18b72da657 | ||
|
|
e8e6c89927 | ||
|
|
fd5f657334 | ||
|
|
da9605f2d2 | ||
|
|
7030de32d5 | ||
|
|
b67c5b77be | ||
|
|
d30878c4ea | ||
|
|
6be26f0a38 | ||
|
|
34a6bfaefa | ||
|
|
1c8749eb4d | ||
|
|
1198c36a2b | ||
|
|
41e6c1a383 | ||
|
|
0042c3e4a7 | ||
|
|
724190f262 | ||
|
|
6867d23ca2 | ||
|
|
de26af0c2d | ||
|
|
3f223a7514 | ||
|
|
23f5a62d61 | ||
|
|
81e2054f59 | ||
|
|
f9337effa5 | ||
|
|
2972904eb8 | ||
|
|
bdd918b4d4 | ||
|
|
88085fe17b | ||
|
|
2020a302d0 | ||
|
|
ab2dd0f300 | ||
|
|
8e6fd4b4a1 | ||
|
|
988d24927e | ||
|
|
e945d16fcf | ||
|
|
f1c0aa4f83 | ||
|
|
68362d06b3 | ||
|
|
f65c0e2ac8 | ||
|
|
0f588ced03 | ||
|
|
b0f03bb49c | ||
|
|
5063661028 | ||
|
|
7e66ab78ff | ||
|
|
665e275dc5 | ||
|
|
a6da728cca | ||
|
|
04e02d7f9f | ||
|
|
7c739dd58e | ||
|
|
05a552910c | ||
|
|
c990837066 | ||
|
|
57aec37507 | ||
|
|
0c5b4476ad | ||
|
|
17141147a8 | ||
|
|
193c2fa860 | ||
|
|
6d01aaa80f | ||
|
|
ad60eaa0f3 | ||
|
|
d878face07 | ||
|
|
8bf8388cd6 | ||
|
|
b4db853bcb | ||
|
|
5ee94c0ba9 | ||
|
|
f108349547 | ||
|
|
d25e1ab94b | ||
|
|
79fee269ee | ||
|
|
ffe362f856 | ||
|
|
04bb15a802 | ||
|
|
4d9d649db9 | ||
|
|
2897c24e83 | ||
|
|
5964dc95f0 | ||
|
|
613b2519ed | ||
|
|
996b60e43d | ||
|
|
a6d09407b9 | ||
|
|
f2e9ddef4e | ||
|
|
ca417d3257 | ||
|
|
10dadfca06 | ||
|
|
bf73a8235f | ||
|
|
67a584c1d4 | ||
|
|
8e5f03972e | ||
|
|
d8abf8f98f | ||
|
|
cb348d2e05 | ||
|
|
aceb111024 | ||
|
|
b60a4a68c7 | ||
|
|
8b6dfe48b7 | ||
|
|
6154e03c05 | ||
|
|
d0b53a6a3d | ||
|
|
317aa679cf | ||
|
|
8d1bc2e539 | ||
|
|
50c46f6e9a | ||
|
|
4f1928778a | ||
|
|
5fcba3d7f5 | ||
|
|
4db42b07c4 | ||
|
|
cd3e2d7a5a | ||
|
|
d139e22042 | ||
|
|
892296e6d5 | ||
|
|
992ef399ed | ||
|
|
5afba46743 | ||
|
|
df0aa7949e | ||
|
|
353d2e6e01 | ||
|
|
f9375bb215 | ||
|
|
8d04ff66e7 | ||
|
|
e649b11511 | ||
|
|
bda19bdddf | ||
|
|
99fd92df21 | ||
|
|
1210310063 | ||
|
|
b093384385 | ||
|
|
cec45ae9bd | ||
|
|
e6dd584dd6 | ||
|
|
7cc74dabaf | ||
|
|
2336f102f9 | ||
|
|
cebe0f6442 | ||
|
|
d9c0c23819 | ||
|
|
aa355a96f9 | ||
|
|
4a85dd2480 | ||
|
|
213909baa5 | ||
|
|
6915a60332 | ||
|
|
52a50e9ade | ||
|
|
b7c9a346c1 | ||
|
|
2d90c6ac24 | ||
|
|
7f7b5447fd | ||
|
|
142f7bb50d | ||
|
|
d209df9e10 | ||
|
|
1b56f4266b | ||
|
|
d6dc6070f3 | ||
|
|
d66323b742 | ||
|
|
7b09d74b1f | ||
|
|
c0e3c2c5e1 | ||
|
|
06189a58fe | ||
|
|
f402dd81bb | ||
|
|
c885bbc947 | ||
|
|
63fb0e5a57 | ||
|
|
37d0792a7d | ||
|
|
c8040d2f63 | ||
|
|
dbcad65b68 | ||
|
|
226da67bc0 | ||
|
|
fee2b5c3fb | ||
|
|
6bbb3d53ae | ||
|
|
610b183cef | ||
|
|
1b64b9e164 | ||
|
|
b022be9ba8 | ||
|
|
7f11363725 | ||
|
|
4aa6dd22bb | ||
|
|
8feed2916f | ||
|
|
59eaa0aa0d | ||
|
|
d5e54cb576 | ||
|
|
8837660ba7 | ||
|
|
464a489b44 | ||
|
|
7035700c8d | ||
|
|
23c2921690 | ||
|
|
7d506f3633 | ||
|
|
b186813049 | ||
|
|
bfa82225da | ||
|
|
ffa2044563 | ||
|
|
d57b69952d | ||
|
|
5a13efefd3 | ||
|
|
2f9d7060bd | ||
|
|
0aa33a2cb4 | ||
|
|
fa7dbedd5d | ||
|
|
2ea9b66943 | ||
|
|
f3beaea9e9 | ||
|
|
39ae2f1f76 | ||
|
|
366b1050ec | ||
|
|
b3aab7a6ad | ||
|
|
aa8d050d6b | ||
|
|
5200f0e98d | ||
|
|
5f4abb1b7f | ||
|
|
dfe1e30d1b | ||
|
|
e27a5be47a | ||
|
|
56786a18f1 | ||
|
|
0d2399d485 | ||
|
|
5bfc7cfde3 | ||
|
|
723f0cbc1e | ||
|
|
b141f789f6 | ||
|
|
7445ee40f8 | ||
|
|
4a9a0f7e64 | ||
|
|
63aad2e5d2 | ||
|
|
d0baa23f9a | ||
|
|
7a7673103f | ||
|
|
05d4788d1d | ||
|
|
6f0dea1b56 | ||
|
|
439ef44973 | ||
|
|
2a525b42cb | ||
|
|
aee91acfdc | ||
|
|
17388ec43e | ||
|
|
bdc1cd13a7 | ||
|
|
42db4b5c77 | ||
|
|
53da073274 | ||
|
|
b010dde661 | ||
|
|
c9ec389b24 | ||
|
|
baa2841b04 | ||
|
|
6af5c86081 | ||
|
|
f60a6929a9 | ||
|
|
2aa97fa121 | ||
|
|
b59809af46 | ||
|
|
ed24d51d3e | ||
|
|
870f0d0932 | ||
|
|
31b77bf178 | ||
|
|
b525f9aa4c | ||
|
|
8409b31d6b | ||
|
|
b878495d64 | ||
|
|
945b85da2f | ||
|
|
d4577d161e | ||
|
|
3c8e1c3ca9 | ||
|
|
88dba8c4dd | ||
|
|
21bc3bfd53 | ||
|
|
4cb5122e90 | ||
|
|
0a2a8be0ff | ||
|
|
720a046610 | ||
|
|
64ae5d4f81 | ||
|
|
ff7e22c08a | ||
|
|
0c267d073f | ||
|
|
bbc6470f65 | ||
|
|
23f1f8a576 | ||
|
|
0e6f6e612a | ||
|
|
4d1b860dad | ||
|
|
6576914e55 | ||
|
|
12075639f3 | ||
|
|
3b9bfe55d0 | ||
|
|
a0c6a7c0de | ||
|
|
a2d716aec3 | ||
|
|
c1c60e3b68 | ||
|
|
ed6e852904 | ||
|
|
a54065420c | ||
|
|
aa5a05960e | ||
|
|
f41ba2a60f | ||
|
|
2215cfefb9 | ||
|
|
4289663a16 | ||
|
|
ea19c2250e | ||
|
|
638766b346 | ||
|
|
d1ff136552 | ||
|
|
46ec11de12 | ||
|
|
4283a49e0b | ||
|
|
1e32db8c41 | ||
|
|
0f944ec7e2 | ||
|
|
736dbc9553 | ||
|
|
b4a38f1f63 | ||
|
|
646186fe38 | ||
|
|
c2725916f4 | ||
|
|
fd334e2b7d | ||
|
|
f9feca1ce4 | ||
|
|
677fd2ff32 | ||
|
|
f49eb8eb4d | ||
|
|
b0e0d68632 | ||
|
|
f3c8c16d79 | ||
|
|
2dd5086916 | ||
|
|
7532072d50 | ||
|
|
382e6107fe | ||
|
|
e6c6609e19 | ||
|
|
4cb5918950 | ||
|
|
55030f3687 | ||
|
|
ef4072e4ff | ||
|
|
c78d383ed1 | ||
|
|
5b96270874 | ||
|
|
2c0742387b | ||
|
|
1704d14f29 | ||
|
|
2d7ffbf378 | ||
|
|
dfd63f85c0 | ||
|
|
cd0c49eaf6 | ||
|
|
080e38d227 | ||
|
|
1a664fba6a | ||
|
|
c915ef815d | ||
|
|
adea4ec54d | ||
|
|
387b5eb2dd | ||
|
|
6337af59ed | ||
|
|
475c7b8f16 | ||
|
|
ac120be1c6 | ||
|
|
b70316e6d3 | ||
|
|
0a0f620d0b | ||
|
|
9132cc4a30 | ||
|
|
e50edcadfb | ||
|
|
2685099720 | ||
|
|
6fa6eb18e8 | ||
|
|
bb79396f0e | ||
|
|
da9fd6b7d0 | ||
|
|
5b8067ef77 | ||
|
|
9eabcd5cae | ||
|
|
d6e0d4cbbd | ||
|
|
e5e6db2688 | ||
|
|
186fe24484 | ||
|
|
5da96d36e6 | ||
|
|
f4b1071e23 | ||
|
|
18291b6533 | ||
|
|
8095cb68bb | ||
|
|
04cd751556 | ||
|
|
7ce2372f51 | ||
|
|
aebda93afe | ||
|
|
2b7b1141eb | ||
|
|
1ff4ff72bf | ||
|
|
d27e91a9b0 | ||
|
|
7cf063b196 | ||
|
|
642f04d493 | ||
|
|
fc6e65e4b0 | ||
|
|
db5c98ec3b | ||
|
|
73c64af27e | ||
|
|
b3f7db813f | ||
|
|
59660ff087 | ||
|
|
69a69e8e04 | ||
|
|
1094f359c3 | ||
|
|
102ee3f871 | ||
|
|
acb5ab08a8 | ||
|
|
ae59472d9a | ||
|
|
5a07b193dc | ||
|
|
fd2edb9adc | ||
|
|
1d074f7b3f | ||
|
|
81984c4bce | ||
|
|
9c891baad1 | ||
|
|
b050c60807 | ||
|
|
e47a2fd0f3 | ||
|
|
42b9cc73ac | ||
|
|
edca4248aa | ||
|
|
b1b6bc9be0 | ||
|
|
818b254cef | ||
|
|
ddfac5e34b | ||
|
|
8b5c945bad | ||
|
|
50c5eb9c50 | ||
|
|
94be67eac1 | ||
|
|
5a05139efe | ||
|
|
a62dc102fb | ||
|
|
518d74ce21 | ||
|
|
7598997deb | ||
|
|
3c876dc202 | ||
|
|
1722742ab3 | ||
|
|
d9c0eb3cfc | ||
|
|
0d990e1dc0 | ||
|
|
60ed5ff99d | ||
|
|
5b98bd66ee | ||
|
|
abd20777fe | ||
|
|
7f0d0cf8a4 | ||
|
|
6e23a573fb | ||
|
|
ce9d93003c | ||
|
|
abfa868423 | ||
|
|
331f6c08fe | ||
|
|
c0efd3d419 | ||
|
|
1385d75972 | ||
|
|
9a787dd105 | ||
|
|
0dcc435bb4 | ||
|
|
f1a67663d1 | ||
|
|
0f95bdc9bb | ||
|
|
a0eab35768 | ||
|
|
027c87dd07 | ||
|
|
f2b31352fe | ||
|
|
c9376e3126 | ||
|
|
7cbcad0e38 | ||
|
|
e167798449 | ||
|
|
fc5928772b | ||
|
|
8263bdd21d | ||
|
|
3c1d4254e7 | ||
|
|
55d7c48b1d | ||
|
|
bf623eed7f | ||
|
|
84bcac0112 | ||
|
|
31595888ea | ||
|
|
5c38b2c4eb | ||
|
|
ebe9162af9 | ||
|
|
b64cf27038 | ||
|
|
0c4e79cff6 | ||
|
|
5b9129a086 | ||
|
|
93d4a12834 | ||
|
|
bf3e2dc652 | ||
|
|
0d0e98d783 | ||
|
|
5a55cfbb9b | ||
|
|
ac93b8a6b9 | ||
|
|
93786d9ebd | ||
|
|
a6dbb580c9 | ||
|
|
e62678abdb | ||
|
|
af50eae604 | ||
|
|
cb4f6aa7f6 | ||
|
|
5e13b1a7cb | ||
|
|
60b339f450 | ||
|
|
f71c779860 | ||
|
|
221a11de9b | ||
|
|
794483c10d | ||
|
|
c9934ccdb7 | ||
|
|
54729f3c1e | ||
|
|
f1a86acb98 | ||
|
|
6b6ea3c8bf | ||
|
|
bf403fee7d | ||
|
|
5cd920cf6f | ||
|
|
45d3b479bc | ||
|
|
c7a752b01d | ||
|
|
099d359628 | ||
|
|
006a2aacbb | ||
|
|
b71d9dd53e | ||
|
|
887e320e7f | ||
|
|
d7c18fd86e | ||
|
|
7566f3db3e | ||
|
|
5d05ec58be | ||
|
|
d9a452f558 | ||
|
|
dec03b3dc0 | ||
|
|
85950bdc0b | ||
|
|
f95bd3bb04 | ||
|
|
e33b8fab34 | ||
|
|
b00fbf153e | ||
|
|
0de5919a16 | ||
|
|
699777be9e | ||
|
|
16ff49d712 | ||
|
|
54c78cf06d | ||
|
|
303eaabeea | ||
|
|
6b6f5b8d04 | ||
|
|
0c18a7e306 | ||
|
|
a23a38080b | ||
|
|
316ca66a4b | ||
|
|
2f71a01877 | ||
|
|
d5cfbfc71d | ||
|
|
12612da75e | ||
|
|
68ec5f2a18 | ||
|
|
00670450df | ||
|
|
dbd95e08e9 | ||
|
|
3713f2d134 | ||
|
|
a85a250dfd | ||
|
|
5845ed2c92 | ||
|
|
40ed505581 | ||
|
|
bf0b8d9968 | ||
|
|
d0a7437dbd | ||
|
|
21b433c5d7 | ||
|
|
7c89bc619a | ||
|
|
0d3daa9fc6 | ||
|
|
0c1f0bad17 | ||
|
|
72cf59fa54 | ||
|
|
527bc1f625 | ||
|
|
2168d09421 | ||
|
|
1c266031d7 | ||
|
|
b636d20c64 | ||
|
|
2a9ca88c2a | ||
|
|
b9c434addb | ||
|
|
451ad47327 | ||
|
|
7f61dd5fe3 | ||
|
|
3ca85028ea | ||
|
|
542a73cc6e | ||
|
|
78d07e2fda | ||
|
|
b617ffd2af | ||
|
|
3abf173d89 | ||
|
|
df8aeb10e8 | ||
|
|
26ad06df7c | ||
|
|
37fff3ef4a | ||
|
|
28c5e63bf5 | ||
|
|
a07c213b3e | ||
|
|
ed72741f48 | ||
|
|
fb0c23b71f | ||
|
|
d98f95f536 | ||
|
|
6643e83b61 | ||
|
|
7b742009a1 | ||
|
|
649e2b48f3 | ||
|
|
81f0c2b0e8 | ||
|
|
80d8aa7239 | ||
|
|
27d4b713f6 | ||
|
|
b0faaf2527 | ||
|
|
8d06d9c111 | ||
|
|
c4d565b15b | ||
|
|
06f8e69c70 | ||
|
|
7db52374cd | ||
|
|
843f205f6f | ||
|
|
2ff51ae77e | ||
|
|
2b75d81a8b | ||
|
|
cad0dcbed1 | ||
|
|
19b8388950 | ||
|
|
87e08b9e50 | ||
|
|
0b7d6bf6df | ||
|
|
89fe05b6cc | ||
|
|
d73d74e78f | ||
|
|
9a682b7a45 | ||
|
|
94201ca133 | ||
|
|
99f9e7252a | ||
|
|
42136a7097 | ||
|
|
9bb4c38bf9 | ||
|
|
5f01db69ff | ||
|
|
c59a7f4a8c | ||
|
|
8295688bed | ||
|
|
9713a3a555 | ||
|
|
d781981bbd | ||
|
|
5125fdb882 | ||
|
|
fd9693b961 | ||
|
|
f38926d666 | ||
|
|
775d07e9a0 | ||
|
|
2d5f172e77 | ||
|
|
08f0de7b46 |
@@ -3,13 +3,18 @@ target
|
|||||||
|
|
||||||
# Data folder
|
# Data folder
|
||||||
data
|
data
|
||||||
|
|
||||||
|
# Misc
|
||||||
.env
|
.env
|
||||||
.env.template
|
.env.template
|
||||||
.gitattributes
|
.gitattributes
|
||||||
|
.gitignore
|
||||||
|
rustfmt.toml
|
||||||
|
|
||||||
# IDE files
|
# IDE files
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
.editorconfig
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
@@ -19,9 +24,17 @@ data
|
|||||||
*.yml
|
*.yml
|
||||||
*.yaml
|
*.yaml
|
||||||
|
|
||||||
# Docker folders
|
# Docker
|
||||||
hooks
|
hooks
|
||||||
tools
|
tools
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker/**
|
||||||
|
!docker/healthcheck.sh
|
||||||
|
!docker/start.sh
|
||||||
|
|
||||||
# Web vault
|
# Web vault
|
||||||
web-vault
|
web-vault
|
||||||
|
|
||||||
|
# Vaultwarden Resources
|
||||||
|
resources
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
|
# shellcheck disable=SC2034,SC2148
|
||||||
## Vaultwarden Configuration File
|
## Vaultwarden Configuration File
|
||||||
## Uncomment any of the following lines to change the defaults
|
## Uncomment any of the following lines to change the defaults
|
||||||
##
|
##
|
||||||
## Be aware that most of these settings will be overridden if they were changed
|
## Be aware that most of these settings will be overridden if they were changed
|
||||||
## in the admin interface. Those overrides are stored within DATA_FOLDER/config.json .
|
## in the admin interface. Those overrides are stored within DATA_FOLDER/config.json .
|
||||||
|
##
|
||||||
|
## By default, Vaultwarden expects for this file to be named ".env" and located
|
||||||
|
## in the current working directory. If this is not the case, the environment
|
||||||
|
## variable ENV_FILE can be set to the location of this file prior to starting
|
||||||
|
## Vaultwarden.
|
||||||
|
|
||||||
## Main data folder
|
## Main data folder
|
||||||
# DATA_FOLDER=data
|
# DATA_FOLDER=data
|
||||||
@@ -24,11 +30,21 @@
|
|||||||
## Define the size of the connection pool used for connecting to the database.
|
## Define the size of the connection pool used for connecting to the database.
|
||||||
# DATABASE_MAX_CONNS=10
|
# DATABASE_MAX_CONNS=10
|
||||||
|
|
||||||
|
## Database connection initialization
|
||||||
|
## Allows SQL statements to be run whenever a new database connection is created.
|
||||||
|
## This is mainly useful for connection-scoped pragmas.
|
||||||
|
## If empty, a database-specific default is used:
|
||||||
|
## - SQLite: "PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL;"
|
||||||
|
## - MySQL: ""
|
||||||
|
## - PostgreSQL: ""
|
||||||
|
# DATABASE_CONN_INIT=""
|
||||||
|
|
||||||
## Individual folders, these override %DATA_FOLDER%
|
## Individual folders, these override %DATA_FOLDER%
|
||||||
# RSA_KEY_FILENAME=data/rsa_key
|
# RSA_KEY_FILENAME=data/rsa_key
|
||||||
# ICON_CACHE_FOLDER=data/icon_cache
|
# ICON_CACHE_FOLDER=data/icon_cache
|
||||||
# ATTACHMENTS_FOLDER=data/attachments
|
# ATTACHMENTS_FOLDER=data/attachments
|
||||||
# SENDS_FOLDER=data/sends
|
# SENDS_FOLDER=data/sends
|
||||||
|
# TMP_FOLDER=data/tmp
|
||||||
|
|
||||||
## Templates data folder, by default uses embedded templates
|
## Templates data folder, by default uses embedded templates
|
||||||
## Check source code to see the format
|
## Check source code to see the format
|
||||||
@@ -65,11 +81,34 @@
|
|||||||
## This setting applies globally to all users.
|
## This setting applies globally to all users.
|
||||||
# EMERGENCY_ACCESS_ALLOWED=true
|
# EMERGENCY_ACCESS_ALLOWED=true
|
||||||
|
|
||||||
|
## Controls whether event logging is enabled for organizations
|
||||||
|
## This setting applies to organizations.
|
||||||
|
## Disabled by default. Also check the EVENT_CLEANUP_SCHEDULE and EVENTS_DAYS_RETAIN settings.
|
||||||
|
# ORG_EVENTS_ENABLED=false
|
||||||
|
|
||||||
|
## Number of days to retain events stored in the database.
|
||||||
|
## If unset (the default), events are kept indefinitely and the scheduled job is disabled!
|
||||||
|
# EVENTS_DAYS_RETAIN=
|
||||||
|
|
||||||
|
## BETA FEATURE: Groups
|
||||||
|
## Controls whether group support is enabled for organizations
|
||||||
|
## This setting applies to organizations.
|
||||||
|
## Disabled by default because this is a beta feature, it contains known issues!
|
||||||
|
## KNOW WHAT YOU ARE DOING!
|
||||||
|
# ORG_GROUPS_ENABLED=false
|
||||||
|
|
||||||
## Job scheduler settings
|
## Job scheduler settings
|
||||||
##
|
##
|
||||||
## Job schedules use a cron-like syntax (as parsed by https://crates.io/crates/cron),
|
## Job schedules use a cron-like syntax (as parsed by https://crates.io/crates/cron),
|
||||||
## and are always in terms of UTC time (regardless of your local time zone settings).
|
## and are always in terms of UTC time (regardless of your local time zone settings).
|
||||||
##
|
##
|
||||||
|
## The schedule format is a bit different from crontab as crontab does not contains seconds.
|
||||||
|
## You can test the the format here: https://crontab.guru, but remove the first digit!
|
||||||
|
## SEC MIN HOUR DAY OF MONTH MONTH DAY OF WEEK
|
||||||
|
## "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri"
|
||||||
|
## "0 30 * * * * "
|
||||||
|
## "0 30 1 * * * "
|
||||||
|
##
|
||||||
## How often (in ms) the job scheduler thread checks for jobs that need running.
|
## How often (in ms) the job scheduler thread checks for jobs that need running.
|
||||||
## Set to 0 to globally disable scheduled jobs.
|
## Set to 0 to globally disable scheduled jobs.
|
||||||
# JOB_POLL_INTERVAL_MS=30000
|
# JOB_POLL_INTERVAL_MS=30000
|
||||||
@@ -87,12 +126,16 @@
|
|||||||
# INCOMPLETE_2FA_SCHEDULE="30 * * * * *"
|
# INCOMPLETE_2FA_SCHEDULE="30 * * * * *"
|
||||||
##
|
##
|
||||||
## Cron schedule of the job that sends expiration reminders to emergency access grantors.
|
## Cron schedule of the job that sends expiration reminders to emergency access grantors.
|
||||||
## Defaults to hourly (5 minutes after the hour). Set blank to disable this job.
|
## Defaults to hourly (3 minutes after the hour). Set blank to disable this job.
|
||||||
# EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE="0 5 * * * *"
|
# EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE="0 3 * * * *"
|
||||||
##
|
##
|
||||||
## Cron schedule of the job that grants emergency access requests that have met the required wait time.
|
## Cron schedule of the job that grants emergency access requests that have met the required wait time.
|
||||||
## Defaults to hourly (5 minutes after the hour). Set blank to disable this job.
|
## Defaults to hourly (7 minutes after the hour). Set blank to disable this job.
|
||||||
# EMERGENCY_REQUEST_TIMEOUT_SCHEDULE="0 5 * * * *"
|
# EMERGENCY_REQUEST_TIMEOUT_SCHEDULE="0 7 * * * *"
|
||||||
|
##
|
||||||
|
## Cron schedule of the job that cleans old events from the event table.
|
||||||
|
## Defaults to daily. Set blank to disable this job. Also without EVENTS_DAYS_RETAIN set, this job will not start.
|
||||||
|
# EVENT_CLEANUP_SCHEDULE="0 10 0 * * *"
|
||||||
|
|
||||||
## Enable extended logging, which shows timestamps and targets in the logs
|
## Enable extended logging, which shows timestamps and targets in the logs
|
||||||
# EXTENDED_LOGGING=true
|
# EXTENDED_LOGGING=true
|
||||||
@@ -102,12 +145,10 @@
|
|||||||
# LOG_TIMESTAMP_FORMAT="%Y-%m-%d %H:%M:%S.%3f"
|
# LOG_TIMESTAMP_FORMAT="%Y-%m-%d %H:%M:%S.%3f"
|
||||||
|
|
||||||
## Logging to file
|
## Logging to file
|
||||||
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
|
|
||||||
# LOG_FILE=/path/to/log
|
# LOG_FILE=/path/to/log
|
||||||
|
|
||||||
## Logging to Syslog
|
## Logging to Syslog
|
||||||
## This requires extended logging
|
## This requires extended logging
|
||||||
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
|
|
||||||
# USE_SYSLOG=false
|
# USE_SYSLOG=false
|
||||||
|
|
||||||
## Log level
|
## Log level
|
||||||
@@ -120,7 +161,7 @@
|
|||||||
## Enable WAL for the DB
|
## Enable WAL for the DB
|
||||||
## Set to false to avoid enabling WAL during startup.
|
## Set to false to avoid enabling WAL during startup.
|
||||||
## Note that if the DB already has WAL enabled, you will also need to disable WAL in the DB,
|
## Note that if the DB already has WAL enabled, you will also need to disable WAL in the DB,
|
||||||
## this setting only prevents vaultwarden from automatically enabling it on start.
|
## this setting only prevents Vaultwarden from automatically enabling it on start.
|
||||||
## Please read project wiki page about this setting first before changing the value as it can
|
## Please read project wiki page about this setting first before changing the value as it can
|
||||||
## cause performance degradation or might render the service unable to start.
|
## cause performance degradation or might render the service unable to start.
|
||||||
# ENABLE_DB_WAL=true
|
# ENABLE_DB_WAL=true
|
||||||
@@ -218,9 +259,13 @@
|
|||||||
## A comma-separated list means only those users can create orgs:
|
## A comma-separated list means only those users can create orgs:
|
||||||
# ORG_CREATION_USERS=admin1@example.com,admin2@example.com
|
# ORG_CREATION_USERS=admin1@example.com,admin2@example.com
|
||||||
|
|
||||||
## Token for the admin interface, preferably use a long random string
|
## Token for the admin interface, preferably an Argon2 PCH string
|
||||||
## One option is to use 'openssl rand -base64 48'
|
## Vaultwarden has a built-in generator by calling `vaultwarden hash`
|
||||||
|
## For details see: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#secure-the-admin_token
|
||||||
## If not set, the admin panel is disabled
|
## If not set, the admin panel is disabled
|
||||||
|
## New Argon2 PHC string
|
||||||
|
# ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78'
|
||||||
|
## Old plain text string (Will generate warnings in favor of Argon2)
|
||||||
# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp
|
# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp
|
||||||
|
|
||||||
## Enable this to bypass the admin panel security. This option is only
|
## Enable this to bypass the admin panel security. This option is only
|
||||||
@@ -232,6 +277,10 @@
|
|||||||
## Name shown in the invitation emails that don't come from a specific organization
|
## Name shown in the invitation emails that don't come from a specific organization
|
||||||
# INVITATION_ORG_NAME=Vaultwarden
|
# INVITATION_ORG_NAME=Vaultwarden
|
||||||
|
|
||||||
|
## The number of hours after which an organization invite token, emergency access invite token,
|
||||||
|
## email verification token and deletion request token will expire (must be at least 1)
|
||||||
|
# INVITATION_EXPIRATION_HOURS=120
|
||||||
|
|
||||||
## Per-organization attachment storage limit (KB)
|
## Per-organization attachment storage limit (KB)
|
||||||
## Max kilobytes of attachment storage allowed per organization.
|
## Max kilobytes of attachment storage allowed per organization.
|
||||||
## When this limit is reached, organization members will not be allowed to upload further attachments for ciphers owned by that organization.
|
## When this limit is reached, organization members will not be allowed to upload further attachments for ciphers owned by that organization.
|
||||||
@@ -253,9 +302,12 @@
|
|||||||
## This setting applies globally to all users.
|
## This setting applies globally to all users.
|
||||||
# INCOMPLETE_2FA_TIME_LIMIT=3
|
# INCOMPLETE_2FA_TIME_LIMIT=3
|
||||||
|
|
||||||
## Controls the PBBKDF password iterations to apply on the server
|
## Number of server-side passwords hashing iterations for the password hash.
|
||||||
## The change only applies when the password is changed
|
## The default for new users. If changed, it will be updated during login for existing users.
|
||||||
# PASSWORD_ITERATIONS=100000
|
# PASSWORD_ITERATIONS=350000
|
||||||
|
|
||||||
|
## Controls whether users can set password hints. This setting applies globally to all users.
|
||||||
|
# PASSWORD_HINTS_ALLOWED=true
|
||||||
|
|
||||||
## Controls whether a password hint should be shown directly in the web page if
|
## Controls whether a password hint should be shown directly in the web page if
|
||||||
## SMTP service is not configured. Not recommended for publicly-accessible instances
|
## SMTP service is not configured. Not recommended for publicly-accessible instances
|
||||||
@@ -267,7 +319,7 @@
|
|||||||
## It's recommended to configure this value, otherwise certain functionality might not work,
|
## It's recommended to configure this value, otherwise certain functionality might not work,
|
||||||
## like attachment downloads, email links and U2F.
|
## like attachment downloads, email links and U2F.
|
||||||
## For U2F to work, the server must use HTTPS, you can use Let's Encrypt for free certs
|
## For U2F to work, the server must use HTTPS, you can use Let's Encrypt for free certs
|
||||||
# DOMAIN=https://bw.domain.tld:8443
|
# DOMAIN=https://vw.domain.tld:8443
|
||||||
|
|
||||||
## Allowed iframe ancestors (Know the risks!)
|
## Allowed iframe ancestors (Know the risks!)
|
||||||
## https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors
|
## https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors
|
||||||
@@ -282,11 +334,14 @@
|
|||||||
## Note that this applies to both the login and the 2FA, so it's recommended to allow a burst size of at least 2.
|
## Note that this applies to both the login and the 2FA, so it's recommended to allow a burst size of at least 2.
|
||||||
# LOGIN_RATELIMIT_MAX_BURST=10
|
# LOGIN_RATELIMIT_MAX_BURST=10
|
||||||
|
|
||||||
## Number of seconds, on average, between admin requests from the same IP address before rate limiting kicks in.
|
## Number of seconds, on average, between admin login requests from the same IP address before rate limiting kicks in.
|
||||||
# ADMIN_RATELIMIT_SECONDS=300
|
# ADMIN_RATELIMIT_SECONDS=300
|
||||||
## Allow a burst of requests of up to this size, while maintaining the average indicated by `ADMIN_RATELIMIT_SECONDS`.
|
## Allow a burst of requests of up to this size, while maintaining the average indicated by `ADMIN_RATELIMIT_SECONDS`.
|
||||||
# ADMIN_RATELIMIT_MAX_BURST=3
|
# ADMIN_RATELIMIT_MAX_BURST=3
|
||||||
|
|
||||||
|
## Set the lifetime of admin sessions to this value (in minutes).
|
||||||
|
# ADMIN_SESSION_LIFETIME=20
|
||||||
|
|
||||||
## Yubico (Yubikey) Settings
|
## Yubico (Yubikey) Settings
|
||||||
## Set your Client ID and Secret Key for Yubikey OTP
|
## Set your Client ID and Secret Key for Yubikey OTP
|
||||||
## You can generate it here: https://upgrade.yubico.com/getapikey/
|
## You can generate it here: https://upgrade.yubico.com/getapikey/
|
||||||
@@ -325,19 +380,23 @@
|
|||||||
# ROCKET_WORKERS=10
|
# ROCKET_WORKERS=10
|
||||||
# ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}
|
# ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}
|
||||||
|
|
||||||
## Mail specific settings, set SMTP_HOST and SMTP_FROM to enable the mail service.
|
## Mail specific settings, set SMTP_FROM and either SMTP_HOST or USE_SENDMAIL to enable the mail service.
|
||||||
## To make sure the email links are pointing to the correct host, set the DOMAIN variable.
|
## To make sure the email links are pointing to the correct host, set the DOMAIN variable.
|
||||||
## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory
|
## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory
|
||||||
# SMTP_HOST=smtp.domain.tld
|
# SMTP_HOST=smtp.domain.tld
|
||||||
# SMTP_FROM=vaultwarden@domain.tld
|
# SMTP_FROM=vaultwarden@domain.tld
|
||||||
# SMTP_FROM_NAME=Vaultwarden
|
# SMTP_FROM_NAME=Vaultwarden
|
||||||
# SMTP_PORT=587 # Ports 587 (submission) and 25 (smtp) are standard without encryption and with encryption via STARTTLS (Explicit TLS). Port 465 is outdated and used with Implicit TLS.
|
# SMTP_SECURITY=starttls # ("starttls", "force_tls", "off") Enable a secure connection. Default is "starttls" (Explicit - ports 587 or 25), "force_tls" (Implicit - port 465) or "off", no encryption (port 25)
|
||||||
# SMTP_SSL=true # (Explicit) - This variable by default configures Explicit STARTTLS, it will upgrade an insecure connection to a secure one. Unless SMTP_EXPLICIT_TLS is set to true. Either port 587 or 25 are default.
|
# SMTP_PORT=587 # Ports 587 (submission) and 25 (smtp) are standard without encryption and with encryption via STARTTLS (Explicit TLS). Port 465 (submissions) is used for encrypted submission (Implicit TLS).
|
||||||
# SMTP_EXPLICIT_TLS=true # (Implicit) - N.B. This variable configures Implicit TLS. It's currently mislabelled (see bug #851) - SMTP_SSL Needs to be set to true for this option to work. Usually port 465 is used here.
|
|
||||||
# SMTP_USERNAME=username
|
# SMTP_USERNAME=username
|
||||||
# SMTP_PASSWORD=password
|
# SMTP_PASSWORD=password
|
||||||
# SMTP_TIMEOUT=15
|
# SMTP_TIMEOUT=15
|
||||||
|
|
||||||
|
# Whether to send mail via the `sendmail` command
|
||||||
|
# USE_SENDMAIL=false
|
||||||
|
# Which sendmail command to use. The one found in the $PATH is used if not specified.
|
||||||
|
# SENDMAIL_COMMAND="/path/to/sendmail"
|
||||||
|
|
||||||
## Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections.
|
## Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections.
|
||||||
## Possible values: ["Plain", "Login", "Xoauth2"].
|
## Possible values: ["Plain", "Login", "Xoauth2"].
|
||||||
## Multiple options need to be separated by a comma ','.
|
## Multiple options need to be separated by a comma ','.
|
||||||
@@ -348,6 +407,9 @@
|
|||||||
## but might need to be changed in case it trips some anti-spam filters
|
## but might need to be changed in case it trips some anti-spam filters
|
||||||
# HELO_NAME=
|
# HELO_NAME=
|
||||||
|
|
||||||
|
## Embed images as email attachments
|
||||||
|
# SMTP_EMBED_IMAGES=false
|
||||||
|
|
||||||
## SMTP debugging
|
## SMTP debugging
|
||||||
## When set to true this will output very detailed SMTP messages.
|
## When set to true this will output very detailed SMTP messages.
|
||||||
## WARNING: This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting!
|
## WARNING: This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting!
|
||||||
|
|||||||
1
.github/FUNDING.yml
vendored
@@ -1,2 +1,3 @@
|
|||||||
github: dani-garcia
|
github: dani-garcia
|
||||||
|
liberapay: dani-garcia
|
||||||
custom: ["https://paypal.me/DaniGG"]
|
custom: ["https://paypal.me/DaniGG"]
|
||||||
|
|||||||
219
.github/workflows/build.yml
vendored
@@ -8,8 +8,9 @@ on:
|
|||||||
- "migrations/**"
|
- "migrations/**"
|
||||||
- "Cargo.*"
|
- "Cargo.*"
|
||||||
- "build.rs"
|
- "build.rs"
|
||||||
- "diesel.toml"
|
|
||||||
- "rust-toolchain"
|
- "rust-toolchain"
|
||||||
|
- "rustfmt.toml"
|
||||||
|
- "diesel.toml"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/build.yml"
|
- ".github/workflows/build.yml"
|
||||||
@@ -17,131 +18,185 @@ on:
|
|||||||
- "migrations/**"
|
- "migrations/**"
|
||||||
- "Cargo.*"
|
- "Cargo.*"
|
||||||
- "build.rs"
|
- "build.rs"
|
||||||
- "diesel.toml"
|
|
||||||
- "rust-toolchain"
|
- "rust-toolchain"
|
||||||
|
- "rustfmt.toml"
|
||||||
|
- "diesel.toml"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
timeout-minutes: 120
|
||||||
# Make warnings errors, this is to prevent warnings slipping through.
|
# Make warnings errors, this is to prevent warnings slipping through.
|
||||||
# This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes.
|
# This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes.
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: "-D warnings"
|
RUSTFLAGS: "-D warnings"
|
||||||
|
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: git # Use the old git protocol until it is stable probably in 1.68 or 1.69. MSRV needs to be at this before removed.
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
channel:
|
channel:
|
||||||
- nightly
|
- "rust-toolchain" # The version defined in rust-toolchain
|
||||||
target-triple:
|
- "msrv" # The supported MSRV
|
||||||
- x86_64-unknown-linux-gnu
|
|
||||||
include:
|
name: Build and Test ${{ matrix.channel }}
|
||||||
- target-triple: x86_64-unknown-linux-gnu
|
|
||||||
host-triple: x86_64-unknown-linux-gnu
|
|
||||||
features: [sqlite,mysql,postgresql] # Remember to update the `cargo test` to match the amount of features
|
|
||||||
channel: nightly
|
|
||||||
os: ubuntu-20.04
|
|
||||||
ext: ""
|
|
||||||
|
|
||||||
name: Building ${{ matrix.channel }}-${{ matrix.target-triple }}
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: "Checkout"
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
|
||||||
|
|
||||||
# Install musl-tools when needed
|
|
||||||
- name: Install musl tools
|
|
||||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends musl-dev musl-tools cmake
|
|
||||||
if: matrix.target-triple == 'x86_64-unknown-linux-musl'
|
|
||||||
# End Install musl-tools when needed
|
|
||||||
|
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
- name: Install dependencies Ubuntu
|
- name: "Install dependencies Ubuntu"
|
||||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends openssl sqlite build-essential libmariadb-dev-compat libpq-dev libssl-dev pkgconf
|
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends openssl sqlite build-essential libmariadb-dev-compat libpq-dev libssl-dev pkg-config
|
||||||
if: startsWith( matrix.os, 'ubuntu' )
|
|
||||||
# End Install dependencies
|
# End Install dependencies
|
||||||
|
|
||||||
|
|
||||||
# Enable Rust Caching
|
# Determine rust-toolchain version
|
||||||
- uses: Swatinem/rust-cache@842ef286fff290e445b90b4002cc9807c3669641 # v1.3.0
|
- name: Init Variables
|
||||||
# End Enable Rust Caching
|
id: toolchain
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [[ "${{ matrix.channel }}" == 'rust-toolchain' ]]; then
|
||||||
|
RUST_TOOLCHAIN="$(cat rust-toolchain)"
|
||||||
|
elif [[ "${{ matrix.channel }}" == 'msrv' ]]; then
|
||||||
|
RUST_TOOLCHAIN="$(grep -oP 'rust-version.*"(\K.*?)(?=")' Cargo.toml)"
|
||||||
|
else
|
||||||
|
RUST_TOOLCHAIN="${{ matrix.channel }}"
|
||||||
|
fi
|
||||||
|
echo "RUST_TOOLCHAIN=${RUST_TOOLCHAIN}" | tee -a "${GITHUB_OUTPUT}"
|
||||||
|
# End Determine rust-toolchain version
|
||||||
|
|
||||||
|
|
||||||
# Uses the rust-toolchain file to determine version
|
# Only install the clippy and rustfmt components on the default rust-toolchain
|
||||||
- name: 'Install ${{ matrix.channel }}-${{ matrix.host-triple }} for target: ${{ matrix.target-triple }}'
|
- name: "Install rust-toolchain version"
|
||||||
uses: actions-rs/toolchain@b2417cde72dcf67f306c0ae8e0828a81bf0b189f # v1.0.6
|
uses: dtolnay/rust-toolchain@fc3253060d0c959bea12a59f10f8391454a0b02d # master @ 2023-03-21 - 06:36 GMT+1
|
||||||
|
if: ${{ matrix.channel == 'rust-toolchain' }}
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||||
target: ${{ matrix.target-triple }}
|
|
||||||
components: clippy, rustfmt
|
components: clippy, rustfmt
|
||||||
# End Uses the rust-toolchain file to determine version
|
# End Uses the rust-toolchain file to determine version
|
||||||
|
|
||||||
|
|
||||||
|
# Install the any other channel to be used for which we do not execute clippy and rustfmt
|
||||||
|
- name: "Install MSRV version"
|
||||||
|
uses: dtolnay/rust-toolchain@fc3253060d0c959bea12a59f10f8391454a0b02d # master @ 2023-03-21 - 06:36 GMT+1
|
||||||
|
if: ${{ matrix.channel != 'rust-toolchain' }}
|
||||||
|
with:
|
||||||
|
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||||
|
# End Install the MSRV channel to be used
|
||||||
|
|
||||||
|
|
||||||
|
# Enable Rust Caching
|
||||||
|
- uses: Swatinem/rust-cache@6fd3edff6979b79f87531400ad694fb7f2c84b1f # v2.2.1
|
||||||
|
# End Enable Rust Caching
|
||||||
|
|
||||||
|
|
||||||
|
# Show environment
|
||||||
|
- name: "Show environment"
|
||||||
|
run: |
|
||||||
|
rustc -vV
|
||||||
|
cargo -vV
|
||||||
|
# End Show environment
|
||||||
|
|
||||||
|
|
||||||
# Run cargo tests (In release mode to speed up future builds)
|
# Run cargo tests (In release mode to speed up future builds)
|
||||||
# First test all features together, afterwards test them separately.
|
# First test all features together, afterwards test them separately.
|
||||||
- name: "`cargo test --release --features ${{ join(matrix.features, ',') }} --target ${{ matrix.target-triple }}`"
|
- name: "test features: sqlite,mysql,postgresql,enable_mimalloc"
|
||||||
uses: actions-rs/cargo@ae10961054e4aa8b4aa7dffede299aaf087aa33b # v1.0.1
|
id: test_sqlite_mysql_postgresql_mimalloc
|
||||||
with:
|
if: $${{ always() }}
|
||||||
command: test
|
run: |
|
||||||
args: --release --features ${{ join(matrix.features, ',') }} --target ${{ matrix.target-triple }}
|
cargo test --release --features sqlite,mysql,postgresql,enable_mimalloc
|
||||||
# Test single features
|
|
||||||
# 0: sqlite
|
- name: "test features: sqlite,mysql,postgresql"
|
||||||
- name: "`cargo test --release --features ${{ matrix.features[0] }} --target ${{ matrix.target-triple }}`"
|
id: test_sqlite_mysql_postgresql
|
||||||
uses: actions-rs/cargo@ae10961054e4aa8b4aa7dffede299aaf087aa33b # v1.0.1
|
if: $${{ always() }}
|
||||||
with:
|
run: |
|
||||||
command: test
|
cargo test --release --features sqlite,mysql,postgresql
|
||||||
args: --release --features ${{ matrix.features[0] }} --target ${{ matrix.target-triple }}
|
|
||||||
if: ${{ matrix.features[0] != '' }}
|
- name: "test features: sqlite"
|
||||||
# 1: mysql
|
id: test_sqlite
|
||||||
- name: "`cargo test --release --features ${{ matrix.features[1] }} --target ${{ matrix.target-triple }}`"
|
if: $${{ always() }}
|
||||||
uses: actions-rs/cargo@ae10961054e4aa8b4aa7dffede299aaf087aa33b # v1.0.1
|
run: |
|
||||||
with:
|
cargo test --release --features sqlite
|
||||||
command: test
|
|
||||||
args: --release --features ${{ matrix.features[1] }} --target ${{ matrix.target-triple }}
|
- name: "test features: mysql"
|
||||||
if: ${{ matrix.features[1] != '' }}
|
id: test_mysql
|
||||||
# 2: postgresql
|
if: $${{ always() }}
|
||||||
- name: "`cargo test --release --features ${{ matrix.features[2] }} --target ${{ matrix.target-triple }}`"
|
run: |
|
||||||
uses: actions-rs/cargo@ae10961054e4aa8b4aa7dffede299aaf087aa33b # v1.0.1
|
cargo test --release --features mysql
|
||||||
with:
|
|
||||||
command: test
|
- name: "test features: postgresql"
|
||||||
args: --release --features ${{ matrix.features[2] }} --target ${{ matrix.target-triple }}
|
id: test_postgresql
|
||||||
if: ${{ matrix.features[2] != '' }}
|
if: $${{ always() }}
|
||||||
|
run: |
|
||||||
|
cargo test --release --features postgresql
|
||||||
# End Run cargo tests
|
# End Run cargo tests
|
||||||
|
|
||||||
|
|
||||||
# Run cargo clippy, and fail on warnings (In release mode to speed up future builds)
|
# Run cargo clippy, and fail on warnings (In release mode to speed up future builds)
|
||||||
- name: "`cargo clippy --release --features ${{ join(matrix.features, ',') }} --target ${{ matrix.target-triple }}`"
|
- name: "clippy features: sqlite,mysql,postgresql,enable_mimalloc"
|
||||||
uses: actions-rs/cargo@ae10961054e4aa8b4aa7dffede299aaf087aa33b # v1.0.1
|
id: clippy
|
||||||
with:
|
if: ${{ always() && matrix.channel == 'rust-toolchain' }}
|
||||||
command: clippy
|
run: |
|
||||||
args: --release --features ${{ join(matrix.features, ',') }} --target ${{ matrix.target-triple }} -- -D warnings
|
cargo clippy --release --features sqlite,mysql,postgresql,enable_mimalloc -- -D warnings
|
||||||
# End Run cargo clippy
|
# End Run cargo clippy
|
||||||
|
|
||||||
|
|
||||||
# Run cargo fmt
|
# Run cargo fmt (Only run on rust-toolchain defined version)
|
||||||
- name: '`cargo fmt`'
|
- name: "check formatting"
|
||||||
uses: actions-rs/cargo@ae10961054e4aa8b4aa7dffede299aaf087aa33b # v1.0.1
|
id: formatting
|
||||||
with:
|
if: ${{ always() && matrix.channel == 'rust-toolchain' }}
|
||||||
command: fmt
|
run: |
|
||||||
args: --all -- --check
|
cargo fmt --all -- --check
|
||||||
# End Run cargo fmt
|
# End Run cargo fmt
|
||||||
|
|
||||||
|
|
||||||
# Build the binary
|
# Check for any previous failures, if there are stop, else continue.
|
||||||
- name: "`cargo build --release --features ${{ join(matrix.features, ',') }} --target ${{ matrix.target-triple }}`"
|
# This is useful so all test/clippy/fmt actions are done, and they can all be addressed
|
||||||
uses: actions-rs/cargo@ae10961054e4aa8b4aa7dffede299aaf087aa33b # v1.0.1
|
- name: "Some checks failed"
|
||||||
with:
|
if: ${{ failure() }}
|
||||||
command: build
|
run: |
|
||||||
args: --release --features ${{ join(matrix.features, ',') }} --target ${{ matrix.target-triple }}
|
echo "### :x: Checks Failed!" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|Job|Status|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|---|------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|test (sqlite,mysql,postgresql,enable_mimalloc)|${{ steps.test_sqlite_mysql_postgresql_mimalloc.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|test (sqlite,mysql,postgresql)|${{ steps.test_sqlite_mysql_postgresql.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|test (sqlite)|${{ steps.test_sqlite.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|test (mysql)|${{ steps.test_mysql.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|test (postgresql)|${{ steps.test_postgresql.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|clippy (sqlite,mysql,postgresql,enable_mimalloc)|${{ steps.clippy.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|fmt|${{ steps.formatting.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Please check the failed jobs and fix where needed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
|
||||||
|
# Check for any previous failures, if there are stop, else continue.
|
||||||
|
# This is useful so all test/clippy/fmt actions are done, and they can all be addressed
|
||||||
|
- name: "All checks passed"
|
||||||
|
if: ${{ success() }}
|
||||||
|
run: |
|
||||||
|
echo "### :tada: Checks Passed!" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
|
||||||
|
# Build the binary to upload to the artifacts
|
||||||
|
- name: "build features: sqlite,mysql,postgresql"
|
||||||
|
if: ${{ matrix.channel == 'rust-toolchain' }}
|
||||||
|
run: |
|
||||||
|
cargo build --release --features sqlite,mysql,postgresql
|
||||||
# End Build the binary
|
# End Build the binary
|
||||||
|
|
||||||
|
|
||||||
# Upload artifact to Github Actions
|
# Upload artifact to Github Actions
|
||||||
- name: Upload artifact
|
- name: "Upload artifact"
|
||||||
uses: actions/upload-artifact@27121b0bdffd731efa15d66772be8dc71245d074 # v2.2.4
|
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||||
|
if: ${{ matrix.channel == 'rust-toolchain' }}
|
||||||
with:
|
with:
|
||||||
name: vaultwarden-${{ matrix.target-triple }}${{ matrix.ext }}
|
name: vaultwarden
|
||||||
path: target/${{ matrix.target-triple }}/release/vaultwarden${{ matrix.ext }}
|
path: target/release/vaultwarden
|
||||||
# End Upload artifact to Github Actions
|
# End Upload artifact to Github Actions
|
||||||
|
|||||||
19
.github/workflows/hadolint.yml
vendored
@@ -1,33 +1,30 @@
|
|||||||
name: Hadolint
|
name: Hadolint
|
||||||
|
|
||||||
on:
|
on: [
|
||||||
push:
|
push,
|
||||||
paths:
|
pull_request
|
||||||
- "docker/**"
|
]
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "docker/**"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
hadolint:
|
hadolint:
|
||||||
name: Validate Dockerfile syntax
|
name: Validate Dockerfile syntax
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
|
||||||
|
|
||||||
# Download hadolint
|
# Download hadolint - https://github.com/hadolint/hadolint/releases
|
||||||
- name: Download hadolint
|
- name: Download hadolint
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sudo curl -L https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \
|
sudo curl -L https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \
|
||||||
sudo chmod +x /usr/local/bin/hadolint
|
sudo chmod +x /usr/local/bin/hadolint
|
||||||
env:
|
env:
|
||||||
HADOLINT_VERSION: 2.7.0
|
HADOLINT_VERSION: 2.12.0
|
||||||
# End Download hadolint
|
# End Download hadolint
|
||||||
|
|
||||||
# Test Dockerfiles
|
# Test Dockerfiles
|
||||||
|
|||||||
168
.github/workflows/release.yml
vendored
@@ -24,21 +24,22 @@ jobs:
|
|||||||
# Some checks to determine if we need to continue with building a new docker.
|
# Some checks to determine if we need to continue with building a new docker.
|
||||||
# We will skip this check if we are creating a tag, because that has the same hash as a previous run already.
|
# We will skip this check if we are creating a tag, because that has the same hash as a previous run already.
|
||||||
skip_check:
|
skip_check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
||||||
outputs:
|
outputs:
|
||||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||||
steps:
|
steps:
|
||||||
- name: Skip Duplicates Actions
|
- name: Skip Duplicates Actions
|
||||||
id: skip_check
|
id: skip_check
|
||||||
uses: fkirc/skip-duplicate-actions@f75dd6564bb646f95277dc8c3b80612e46a4a1ea # v3.4.1
|
uses: fkirc/skip-duplicate-actions@12aca0a884f6137d619d6a8a09fcc3406ced5281 # v5.3.0
|
||||||
with:
|
with:
|
||||||
cancel_others: 'true'
|
cancel_others: 'true'
|
||||||
# Only run this when not creating a tag
|
# Only run this when not creating a tag
|
||||||
if: ${{ startsWith(github.ref, 'refs/heads/') }}
|
if: ${{ startsWith(github.ref, 'refs/heads/') }}
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
|
timeout-minutes: 120
|
||||||
needs: skip_check
|
needs: skip_check
|
||||||
# Start a local docker registry to be used to generate multi-arch images.
|
# Start a local docker registry to be used to generate multi-arch images.
|
||||||
services:
|
services:
|
||||||
@@ -47,11 +48,23 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 5000:5000
|
- 5000:5000
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILDKIT: 1 # Disabled for now, but we should look at this because it will speedup building!
|
# Use BuildKit (https://docs.docker.com/build/buildkit/) for better
|
||||||
# DOCKER_REPO/secrets.DOCKERHUB_REPO needs to be 'index.docker.io/<user>/<repo>'
|
# build performance and the ability to copy extended file attributes
|
||||||
DOCKER_REPO: ${{ secrets.DOCKERHUB_REPO }}
|
# (e.g., for executable capabilities) across build phases.
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
SOURCE_COMMIT: ${{ github.sha }}
|
SOURCE_COMMIT: ${{ github.sha }}
|
||||||
SOURCE_REPOSITORY_URL: "https://github.com/${{ github.repository }}"
|
SOURCE_REPOSITORY_URL: "https://github.com/${{ github.repository }}"
|
||||||
|
# The *_REPO variables need to be configured as repository variables
|
||||||
|
# Append `/settings/variables/actions` to your repo url
|
||||||
|
# DOCKERHUB_REPO needs to be 'index.docker.io/<user>/<repo>'
|
||||||
|
# Check for Docker hub credentials in secrets
|
||||||
|
HAVE_DOCKERHUB_LOGIN: ${{ vars.DOCKERHUB_REPO != '' && secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }}
|
||||||
|
# GHCR_REPO needs to be 'ghcr.io/<user>/<repo>'
|
||||||
|
# Check for Github credentials in secrets
|
||||||
|
HAVE_GHCR_LOGIN: ${{ vars.GHCR_REPO != '' && github.repository_owner != '' && secrets.GITHUB_TOKEN != '' }}
|
||||||
|
# QUAY_REPO needs to be 'quay.io/<user>/<repo>'
|
||||||
|
# Check for Quay.io credentials in secrets
|
||||||
|
HAVE_QUAY_LOGIN: ${{ vars.QUAY_REPO != '' && secrets.QUAY_USERNAME != '' && secrets.QUAY_TOKEN != '' }}
|
||||||
if: ${{ needs.skip_check.outputs.should_skip != 'true' && github.repository == 'dani-garcia/vaultwarden' }}
|
if: ${{ needs.skip_check.outputs.should_skip != 'true' && github.repository == 'dani-garcia/vaultwarden' }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -60,17 +73,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# Login to Docker Hub
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 # v1.10.0
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
# Determine Docker Tag
|
# Determine Docker Tag
|
||||||
- name: Init Variables
|
- name: Init Variables
|
||||||
id: vars
|
id: vars
|
||||||
@@ -78,42 +84,152 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
# Check which main tag we are going to build determined by github.ref
|
# Check which main tag we are going to build determined by github.ref
|
||||||
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||||
echo "set-output name=DOCKER_TAG::${GITHUB_REF#refs/*/}"
|
echo "DOCKER_TAG=${GITHUB_REF#refs/*/}" | tee -a "${GITHUB_OUTPUT}"
|
||||||
echo "::set-output name=DOCKER_TAG::${GITHUB_REF#refs/*/}"
|
|
||||||
elif [[ "${{ github.ref }}" == refs/heads/* ]]; then
|
elif [[ "${{ github.ref }}" == refs/heads/* ]]; then
|
||||||
echo "set-output name=DOCKER_TAG::testing"
|
echo "DOCKER_TAG=testing" | tee -a "${GITHUB_OUTPUT}"
|
||||||
echo "::set-output name=DOCKER_TAG::testing"
|
|
||||||
fi
|
fi
|
||||||
# End Determine Docker Tag
|
# End Determine Docker Tag
|
||||||
|
|
||||||
- name: Build Debian based images
|
# Login to Docker Hub
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
||||||
|
|
||||||
|
# Login to GitHub Container Registry
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}
|
||||||
|
|
||||||
|
# Login to Quay.io
|
||||||
|
- name: Login to Quay.io
|
||||||
|
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0
|
||||||
|
with:
|
||||||
|
registry: quay.io
|
||||||
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
|
password: ${{ secrets.QUAY_TOKEN }}
|
||||||
|
if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}
|
||||||
|
|
||||||
|
# Debian
|
||||||
|
|
||||||
|
# Docker Hub
|
||||||
|
- name: Build Debian based images (docker.io)
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
|
DOCKER_REPO: "${{ vars.DOCKERHUB_REPO }}"
|
||||||
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}"
|
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}"
|
||||||
run: |
|
run: |
|
||||||
./hooks/build
|
./hooks/build
|
||||||
if: ${{ matrix.base_image == 'debian' }}
|
if: ${{ matrix.base_image == 'debian' && env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
||||||
|
|
||||||
- name: Push Debian based images
|
- name: Push Debian based images (docker.io)
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
|
DOCKER_REPO: "${{ vars.DOCKERHUB_REPO }}"
|
||||||
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}"
|
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}"
|
||||||
run: |
|
run: |
|
||||||
./hooks/push
|
./hooks/push
|
||||||
if: ${{ matrix.base_image == 'debian' }}
|
if: ${{ matrix.base_image == 'debian' && env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
||||||
|
|
||||||
- name: Build Alpine based images
|
# GitHub Container Registry
|
||||||
|
- name: Build Debian based images (ghcr.io)
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
|
DOCKER_REPO: "${{ vars.GHCR_REPO }}"
|
||||||
|
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}"
|
||||||
|
run: |
|
||||||
|
./hooks/build
|
||||||
|
if: ${{ matrix.base_image == 'debian' && env.HAVE_GHCR_LOGIN == 'true' }}
|
||||||
|
|
||||||
|
- name: Push Debian based images (ghcr.io)
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DOCKER_REPO: "${{ vars.GHCR_REPO }}"
|
||||||
|
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}"
|
||||||
|
run: |
|
||||||
|
./hooks/push
|
||||||
|
if: ${{ matrix.base_image == 'debian' && env.HAVE_GHCR_LOGIN == 'true' }}
|
||||||
|
|
||||||
|
# Quay.io
|
||||||
|
- name: Build Debian based images (quay.io)
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DOCKER_REPO: "${{ vars.QUAY_REPO }}"
|
||||||
|
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}"
|
||||||
|
run: |
|
||||||
|
./hooks/build
|
||||||
|
if: ${{ matrix.base_image == 'debian' && env.HAVE_QUAY_LOGIN == 'true' }}
|
||||||
|
|
||||||
|
- name: Push Debian based images (quay.io)
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DOCKER_REPO: "${{ vars.QUAY_REPO }}"
|
||||||
|
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}"
|
||||||
|
run: |
|
||||||
|
./hooks/push
|
||||||
|
if: ${{ matrix.base_image == 'debian' && env.HAVE_QUAY_LOGIN == 'true' }}
|
||||||
|
|
||||||
|
# Alpine
|
||||||
|
|
||||||
|
# Docker Hub
|
||||||
|
- name: Build Alpine based images (docker.io)
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DOCKER_REPO: "${{ vars.DOCKERHUB_REPO }}"
|
||||||
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}-alpine"
|
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}-alpine"
|
||||||
run: |
|
run: |
|
||||||
./hooks/build
|
./hooks/build
|
||||||
if: ${{ matrix.base_image == 'alpine' }}
|
if: ${{ matrix.base_image == 'alpine' && env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
||||||
|
|
||||||
- name: Push Alpine based images
|
- name: Push Alpine based images (docker.io)
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
|
DOCKER_REPO: "${{ vars.DOCKERHUB_REPO }}"
|
||||||
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}-alpine"
|
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}-alpine"
|
||||||
run: |
|
run: |
|
||||||
./hooks/push
|
./hooks/push
|
||||||
if: ${{ matrix.base_image == 'alpine' }}
|
if: ${{ matrix.base_image == 'alpine' && env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
||||||
|
|
||||||
|
# GitHub Container Registry
|
||||||
|
- name: Build Alpine based images (ghcr.io)
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DOCKER_REPO: "${{ vars.GHCR_REPO }}"
|
||||||
|
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}-alpine"
|
||||||
|
run: |
|
||||||
|
./hooks/build
|
||||||
|
if: ${{ matrix.base_image == 'alpine' && env.HAVE_GHCR_LOGIN == 'true' }}
|
||||||
|
|
||||||
|
- name: Push Alpine based images (ghcr.io)
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DOCKER_REPO: "${{ vars.GHCR_REPO }}"
|
||||||
|
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}-alpine"
|
||||||
|
run: |
|
||||||
|
./hooks/push
|
||||||
|
if: ${{ matrix.base_image == 'alpine' && env.HAVE_GHCR_LOGIN == 'true' }}
|
||||||
|
|
||||||
|
# Quay.io
|
||||||
|
- name: Build Alpine based images (quay.io)
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DOCKER_REPO: "${{ vars.QUAY_REPO }}"
|
||||||
|
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}-alpine"
|
||||||
|
run: |
|
||||||
|
./hooks/build
|
||||||
|
if: ${{ matrix.base_image == 'alpine' && env.HAVE_QUAY_LOGIN == 'true' }}
|
||||||
|
|
||||||
|
- name: Push Alpine based images (quay.io)
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DOCKER_REPO: "${{ vars.QUAY_REPO }}"
|
||||||
|
DOCKER_TAG: "${{steps.vars.outputs.DOCKER_TAG}}-alpine"
|
||||||
|
run: |
|
||||||
|
./hooks/push
|
||||||
|
if: ${{ matrix.base_image == 'alpine' && env.HAVE_QUAY_LOGIN == 'true' }}
|
||||||
|
|||||||
@@ -3,5 +3,9 @@ ignored:
|
|||||||
- DL3008
|
- DL3008
|
||||||
# disable explicit version for apk install
|
# disable explicit version for apk install
|
||||||
- DL3018
|
- DL3018
|
||||||
|
# disable check for consecutive `RUN` instructions
|
||||||
|
- DL3059
|
||||||
trustedRegistries:
|
trustedRegistries:
|
||||||
- docker.io
|
- docker.io
|
||||||
|
- ghcr.io
|
||||||
|
- quay.io
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
---
|
---
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.0.1
|
rev: v4.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-json
|
- id: check-json
|
||||||
- id: check-toml
|
- id: check-toml
|
||||||
|
- id: mixed-line-ending
|
||||||
|
args: ["--fix=no"]
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
exclude: "(.*js$|.*css$)"
|
exclude: "(.*js$|.*css$)"
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
|
- id: check-symlinks
|
||||||
|
- id: forbid-submodules
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: fmt
|
- id: fmt
|
||||||
@@ -25,14 +29,16 @@ repos:
|
|||||||
description: Test the package for errors.
|
description: Test the package for errors.
|
||||||
entry: cargo test
|
entry: cargo test
|
||||||
language: system
|
language: system
|
||||||
args: ["--features", "sqlite,mysql,postgresql", "--"]
|
args: ["--features", "sqlite,mysql,postgresql,enable_mimalloc", "--"]
|
||||||
types: [rust]
|
types_or: [rust, file]
|
||||||
|
files: (Cargo.toml|Cargo.lock|rust-toolchain|.*\.rs$)
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
- id: cargo-clippy
|
- id: cargo-clippy
|
||||||
name: cargo clippy
|
name: cargo clippy
|
||||||
description: Lint Rust sources
|
description: Lint Rust sources
|
||||||
entry: cargo clippy
|
entry: cargo clippy
|
||||||
language: system
|
language: system
|
||||||
args: ["--features", "sqlite,mysql,postgresql", "--", "-D", "warnings"]
|
args: ["--features", "sqlite,mysql,postgresql,enable_mimalloc", "--", "-D", "warnings"]
|
||||||
types: [rust]
|
types_or: [rust, file]
|
||||||
|
files: (Cargo.toml|Cargo.lock|rust-toolchain|clippy.toml|.*\.rs$)
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|||||||
3512
Cargo.lock
generated
196
Cargo.toml
@@ -3,16 +3,17 @@ name = "vaultwarden"
|
|||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.60"
|
rust-version = "1.66.1"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
repository = "https://github.com/dani-garcia/vaultwarden"
|
repository = "https://github.com/dani-garcia/vaultwarden"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "GPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
publish = false
|
publish = false
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
# default = ["sqlite"]
|
||||||
# Empty to keep compatibility, prefer to set USE_SYSLOG=true
|
# Empty to keep compatibility, prefer to set USE_SYSLOG=true
|
||||||
enable_syslog = []
|
enable_syslog = []
|
||||||
mysql = ["diesel/mysql", "diesel_migrations/mysql"]
|
mysql = ["diesel/mysql", "diesel_migrations/mysql"]
|
||||||
@@ -20,135 +21,158 @@ postgresql = ["diesel/postgres", "diesel_migrations/postgres"]
|
|||||||
sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "libsqlite3-sys"]
|
sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "libsqlite3-sys"]
|
||||||
# Enable to use a vendored and statically linked openssl
|
# Enable to use a vendored and statically linked openssl
|
||||||
vendored_openssl = ["openssl/vendored"]
|
vendored_openssl = ["openssl/vendored"]
|
||||||
|
# Enable MiMalloc memory allocator to replace the default malloc
|
||||||
|
# This can improve performance for Alpine builds
|
||||||
|
enable_mimalloc = ["mimalloc"]
|
||||||
|
# This is a development dependency, and should only be used during development!
|
||||||
|
# It enables the usage of the diesel_logger crate, which is able to output the generated queries.
|
||||||
|
# You also need to set an env variable `QUERY_LOGGER=1` to fully activate this so you do not have to re-compile
|
||||||
|
# if you want to turn off the logging for a specific run.
|
||||||
|
query_logger = ["diesel_logger"]
|
||||||
|
|
||||||
# Enable unstable features, requires nightly
|
# Enable unstable features, requires nightly
|
||||||
# Currently only used to enable rusts official ip support
|
# Currently only used to enable rusts official ip support
|
||||||
unstable = []
|
unstable = []
|
||||||
|
|
||||||
[target."cfg(not(windows))".dependencies]
|
[target."cfg(not(windows))".dependencies]
|
||||||
syslog = "4.0.1"
|
# Logging
|
||||||
|
syslog = "6.0.1" # Needs to be v4 until fern is updated
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
|
# Logging
|
||||||
rocket = { version = "=0.5.0-dev", features = ["tls"], default-features = false }
|
log = "0.4.17"
|
||||||
rocket_contrib = "=0.5.0-dev"
|
fern = { version = "0.6.2", features = ["syslog-6"] }
|
||||||
|
tracing = { version = "0.1.37", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work
|
||||||
|
|
||||||
# HTTP client
|
# A `dotenv` implementation for Rust
|
||||||
reqwest = { version = "0.11.8", features = ["blocking", "json", "gzip", "brotli", "socks", "cookies", "trust-dns"] }
|
dotenvy = { version = "0.15.7", default-features = false }
|
||||||
|
|
||||||
# Used for custom short lived cookie jar
|
# Lazy initialization
|
||||||
cookie = "0.15.1"
|
once_cell = "1.17.1"
|
||||||
cookie_store = "0.15.1"
|
|
||||||
bytes = "1.1.0"
|
|
||||||
url = "2.2.2"
|
|
||||||
|
|
||||||
# multipart/form-data support
|
# Numerical libraries
|
||||||
multipart = { version = "0.18.0", features = ["server"], default-features = false }
|
num-traits = "0.2.15"
|
||||||
|
num-derive = "0.3.3"
|
||||||
|
|
||||||
# WebSockets library
|
# Web framework
|
||||||
ws = { version = "0.11.1", package = "parity-ws" }
|
rocket = { version = "0.5.0-rc.3", features = ["tls", "json"], default-features = false }
|
||||||
|
|
||||||
# MessagePack library
|
# WebSockets libraries
|
||||||
rmpv = "1.0.0"
|
tokio-tungstenite = "0.18.0"
|
||||||
|
rmpv = "1.0.0" # MessagePack library
|
||||||
|
|
||||||
# Concurrent hashmap implementation
|
# Concurrent HashMap used for WebSocket messaging and favicons
|
||||||
chashmap = "2.2.2"
|
dashmap = "5.4.0"
|
||||||
|
|
||||||
|
# Async futures
|
||||||
|
futures = "0.3.28"
|
||||||
|
tokio = { version = "1.27.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal"] }
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = { version = "1.0.132", features = ["derive"] }
|
serde = { version = "1.0.159", features = ["derive"] }
|
||||||
serde_json = "1.0.73"
|
serde_json = "1.0.95"
|
||||||
|
|
||||||
# Logging
|
|
||||||
log = "0.4.14"
|
|
||||||
fern = { version = "0.6.0", features = ["syslog-4"] }
|
|
||||||
|
|
||||||
# A safe, extensible ORM and Query builder
|
# A safe, extensible ORM and Query builder
|
||||||
diesel = { version = "1.4.8", features = [ "chrono", "r2d2"] }
|
diesel = { version = "2.0.3", features = ["chrono", "r2d2"] }
|
||||||
diesel_migrations = "1.4.0"
|
diesel_migrations = "2.0.0"
|
||||||
|
diesel_logger = { version = "0.2.0", optional = true }
|
||||||
|
|
||||||
# Bundled SQLite
|
# Bundled/Static SQLite
|
||||||
libsqlite3-sys = { version = "0.22.2", features = ["bundled"], optional = true }
|
libsqlite3-sys = { version = "0.25.2", features = ["bundled"], optional = true }
|
||||||
|
|
||||||
# Crypto-related libraries
|
# Crypto-related libraries
|
||||||
rand = "0.8.4"
|
rand = { version = "0.8.5", features = ["small_rng"] }
|
||||||
ring = "0.16.20"
|
ring = "0.16.20"
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "0.8.2", features = ["v4"] }
|
uuid = { version = "1.3.0", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time libraries
|
# Date and time libraries
|
||||||
chrono = { version = "0.4.19", features = ["serde"] }
|
chrono = { version = "0.4.24", features = ["clock", "serde"], default-features = false }
|
||||||
chrono-tz = "0.6.1"
|
chrono-tz = "0.8.1"
|
||||||
time = "0.2.27"
|
time = "0.3.20"
|
||||||
|
|
||||||
# Job scheduler
|
# Job scheduler
|
||||||
job_scheduler = "1.2.1"
|
job_scheduler_ng = "2.0.4"
|
||||||
|
|
||||||
# TOTP library
|
# Data encoding library Hex/Base32/Base64
|
||||||
totp-lite = "1.0.3"
|
data-encoding = "2.3.3"
|
||||||
|
|
||||||
# Data encoding library
|
|
||||||
data-encoding = "2.3.2"
|
|
||||||
|
|
||||||
# JWT library
|
# JWT library
|
||||||
jsonwebtoken = "7.2.0"
|
jsonwebtoken = "8.3.0"
|
||||||
|
|
||||||
# U2F library
|
# TOTP library
|
||||||
u2f = "0.2.0"
|
totp-lite = "2.0.0"
|
||||||
webauthn-rs = "0.3.1"
|
|
||||||
|
|
||||||
# Yubico Library
|
# Yubico Library
|
||||||
yubico = { version = "0.10.0", features = ["online-tokio"], default-features = false }
|
yubico = { version = "0.11.0", features = ["online-tokio"], default-features = false }
|
||||||
|
|
||||||
# A `dotenv` implementation for Rust
|
# WebAuthn libraries
|
||||||
dotenv = { version = "0.15.0", default-features = false }
|
webauthn-rs = "0.3.2"
|
||||||
|
|
||||||
# Lazy initialization
|
# Handling of URL's for WebAuthn and favicons
|
||||||
once_cell = "1.9.0"
|
url = "2.3.1"
|
||||||
|
|
||||||
# Numerical libraries
|
|
||||||
num-traits = "0.2.14"
|
|
||||||
num-derive = "0.3.3"
|
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
tracing = { version = "0.1.29", features = ["log"] } # Needed to have lettre trace logging used when SMTP_DEBUG is enabled.
|
lettre = { version = "0.10.3", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
||||||
lettre = { version = "0.10.0-rc.4", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false }
|
percent-encoding = "2.2.0" # URL encoding library used for URL's in the emails
|
||||||
|
email_address = "0.2.4"
|
||||||
|
|
||||||
# Template library
|
# HTML Template library
|
||||||
handlebars = { version = "4.1.6", features = ["dir_source"] }
|
handlebars = { version = "4.3.6", features = ["dir_source"] }
|
||||||
|
|
||||||
# For favicon extraction from main website
|
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
||||||
html5ever = "0.25.1"
|
reqwest = { version = "0.11.16", features = ["stream", "json", "gzip", "brotli", "socks", "cookies", "trust-dns"] }
|
||||||
markup5ever_rcdom = "0.1.0"
|
|
||||||
regex = { version = "1.5.4", features = ["std", "perf", "unicode-perl"], default-features = false }
|
|
||||||
data-url = "0.1.1"
|
|
||||||
|
|
||||||
# Used by U2F, JWT and Postgres
|
# Favicon extraction libraries
|
||||||
openssl = "0.10.38"
|
html5gum = "0.5.2"
|
||||||
|
regex = { version = "1.7.3", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||||
|
data-url = "0.2.0"
|
||||||
|
bytes = "1.4.0"
|
||||||
|
|
||||||
# URL encoding library
|
# Cache function results (Used for version check and favicon fetching)
|
||||||
percent-encoding = "2.1.0"
|
cached = "0.42.0"
|
||||||
# Punycode conversion
|
|
||||||
idna = "0.2.3"
|
# Used for custom short lived cookie jar during favicon extraction
|
||||||
|
cookie = "0.16.2"
|
||||||
|
cookie_store = "0.19.0"
|
||||||
|
|
||||||
|
# Used by U2F, JWT and PostgreSQL
|
||||||
|
openssl = "0.10.48"
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
pico-args = "0.4.2"
|
pico-args = "0.5.0"
|
||||||
|
|
||||||
# Logging panics to logfile instead stderr only
|
|
||||||
backtrace = "0.3.63"
|
|
||||||
|
|
||||||
# Macro ident concatenation
|
# Macro ident concatenation
|
||||||
paste = "1.0.6"
|
paste = "1.0.12"
|
||||||
governor = "0.3.2"
|
governor = "0.5.1"
|
||||||
|
|
||||||
[patch.crates-io]
|
# Check client versions for specific features.
|
||||||
# Use newest ring
|
semver = "1.0.17"
|
||||||
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
|
|
||||||
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
|
|
||||||
|
|
||||||
# The maintainer of the `job_scheduler` crate doesn't seem to have responded
|
# Allow overriding the default memory allocator
|
||||||
# to any issues or PRs for almost a year (as of April 2021). This hopefully
|
# Mainly used for the musl builds, since the default musl malloc is very slow
|
||||||
# temporary fork updates Cargo.toml to use more up-to-date dependencies.
|
mimalloc = { version = "=0.1.34", features = ["secure"], default-features = false, optional = true }
|
||||||
# In particular, `cron` has since implemented parsing of some common syntax
|
libmimalloc-sys = "=0.1.30"
|
||||||
# that wasn't previously supported (https://github.com/zslayton/cron/pull/64).
|
which = "4.4.0"
|
||||||
job_scheduler = { git = 'https://github.com/jjlin/job_scheduler', rev = 'ee023418dbba2bfe1e30a5fd7d937f9e33739806' }
|
|
||||||
|
# Argon2 library with support for the PHC format
|
||||||
|
argon2 = "0.5.0"
|
||||||
|
|
||||||
|
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
|
||||||
|
rpassword = "7.2.0"
|
||||||
|
|
||||||
|
# Strip debuginfo from the release builds
|
||||||
|
# Also enable thin LTO for some optimizations
|
||||||
|
[profile.release]
|
||||||
|
strip = "debuginfo"
|
||||||
|
lto = "thin"
|
||||||
|
|
||||||
|
# Always build argon2 using opt-level 3
|
||||||
|
# This is a huge speed improvement during testing
|
||||||
|
[profile.dev.package.argon2]
|
||||||
|
opt-level = 3
|
||||||
|
|
||||||
|
# A little bit of a speedup
|
||||||
|
[profile.dev]
|
||||||
|
split-debuginfo = "unpacked"
|
||||||
|
|||||||
143
LICENSE.txt
@@ -1,5 +1,5 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
@@ -7,17 +7,15 @@
|
|||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
software and other kinds of works.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
software for all its users.
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
|||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
Developers that use our General Public Licenses protect your rights
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
you this License which gives you legal permission to copy, distribute
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
and/or modify the software.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
A secondary benefit of defending all users' freedom is that
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
improvements made in alternate versions of the program, if they
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
receive widespread use, become available for other developers to
|
||||||
or can get the source code. And you must show them these terms so they
|
incorporate. Many developers of free software are heartened and
|
||||||
know their rights.
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
The GNU Affero General Public License is designed specifically to
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
ensure that, in such cases, the modified source code becomes available
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
An older license, called the Affero General Public License and
|
||||||
that there is no warranty for this free software. For both users' and
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
changed, so that their problems will not be attributed erroneously to
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
authors of previous versions.
|
this license.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@@ -72,7 +60,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
|||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the work with which it is combined will remain governed by version
|
||||||
section 13, concerning interaction through a network will apply to the
|
3 of the GNU General Public License.
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU General Public License from time to time. Such new versions will
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU General
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU General Public License, you may choose any version ever published
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU Affero General Public License as published
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If your software can interact with users remotely through a computer
|
||||||
notice like this when it starts in an interactive mode:
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
<program> Copyright (C) <year> <name of author>
|
interface could display a "Source" link that leads users to an archive
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
of the code. There are many ways you could offer source, and different
|
||||||
This is free software, and you are welcome to redistribute it
|
solutions will be better for different programs; see section 13 for the
|
||||||
under certain conditions; type `show c' for details.
|
specific requirements.
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
|
|||||||
41
README.md
@@ -3,16 +3,18 @@
|
|||||||
📢 Note: This project was known as Bitwarden_RS and has been renamed to separate itself from the official Bitwarden server in the hopes of avoiding confusion and trademark/branding issues. Please see [#1642](https://github.com/dani-garcia/vaultwarden/discussions/1642) for more explanation.
|
📢 Note: This project was known as Bitwarden_RS and has been renamed to separate itself from the official Bitwarden server in the hopes of avoiding confusion and trademark/branding issues. Please see [#1642](https://github.com/dani-garcia/vaultwarden/discussions/1642) for more explanation.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
[](https://github.com/dani-garcia/vaultwarden/actions/workflows/build.yml)
|
||||||
|
[](https://github.com/dani-garcia/vaultwarden/pkgs/container/vaultwarden)
|
||||||
[](https://hub.docker.com/r/vaultwarden/server)
|
[](https://hub.docker.com/r/vaultwarden/server)
|
||||||
|
[](https://quay.io/repository/vaultwarden/server)
|
||||||
[](https://deps.rs/repo/github/dani-garcia/vaultwarden)
|
[](https://deps.rs/repo/github/dani-garcia/vaultwarden)
|
||||||
[](https://github.com/dani-garcia/vaultwarden/releases/latest)
|
[](https://github.com/dani-garcia/vaultwarden/releases/latest)
|
||||||
[](https://github.com/dani-garcia/vaultwarden/blob/master/LICENSE.txt)
|
[](https://github.com/dani-garcia/vaultwarden/blob/main/LICENSE.txt)
|
||||||
[](https://matrix.to/#/#vaultwarden:matrix.org)
|
[](https://matrix.to/#/#vaultwarden:matrix.org)
|
||||||
|
|
||||||
Image is based on [Rust implementation of Bitwarden API](https://github.com/dani-garcia/vaultwarden).
|
Image is based on [Rust implementation of Bitwarden API](https://github.com/dani-garcia/vaultwarden).
|
||||||
|
|
||||||
**This project is not associated with the [Bitwarden](https://bitwarden.com/) project nor 8bit Solutions LLC.**
|
**This project is not associated with the [Bitwarden](https://bitwarden.com/) project nor Bitwarden, Inc.**
|
||||||
|
|
||||||
#### ⚠️**IMPORTANT**⚠️: When using this server, please report any bugs or suggestions to us directly (look at the bottom of this page for ways to get in touch), regardless of whatever clients you are using (mobile, desktop, browser...). DO NOT use the official support channels.
|
#### ⚠️**IMPORTANT**⚠️: When using this server, please report any bugs or suggestions to us directly (look at the bottom of this page for ways to get in touch), regardless of whatever clients you are using (mobile, desktop, browser...). DO NOT use the official support channels.
|
||||||
|
|
||||||
@@ -23,12 +25,13 @@ Image is based on [Rust implementation of Bitwarden API](https://github.com/dani
|
|||||||
Basically full implementation of Bitwarden API is provided including:
|
Basically full implementation of Bitwarden API is provided including:
|
||||||
|
|
||||||
* Organizations support
|
* Organizations support
|
||||||
* Attachments
|
* Attachments and Send
|
||||||
* Vault API support
|
* Vault API support
|
||||||
* Serving the static files for Vault interface
|
* Serving the static files for Vault interface
|
||||||
* Website icons API
|
* Website icons API
|
||||||
* Authenticator and U2F support
|
* Authenticator and U2F support
|
||||||
* YubiKey and Duo support
|
* YubiKey and Duo support
|
||||||
|
* Emergency Access
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
Pull the docker image and mount a volume from the host for persistent storage:
|
Pull the docker image and mount a volume from the host for persistent storage:
|
||||||
@@ -39,7 +42,7 @@ docker run -d --name vaultwarden -v /vw-data/:/data/ -p 80:80 vaultwarden/server
|
|||||||
```
|
```
|
||||||
This will preserve any persistent data under /vw-data/, you can adapt the path to whatever suits you.
|
This will preserve any persistent data under /vw-data/, you can adapt the path to whatever suits you.
|
||||||
|
|
||||||
**IMPORTANT**: Some web browsers, like Chrome, disallow the use of Web Crypto APIs in insecure contexts. In this case, you might get an error like `Cannot read property 'importKey'`. To solve this problem, you need to access the web vault from HTTPS.
|
**IMPORTANT**: Most modern web browsers, disallow the use of Web Crypto APIs in insecure contexts. In this case, you might get an error like `Cannot read property 'importKey'`. To solve this problem, you need to access the web vault via HTTPS or localhost.
|
||||||
|
|
||||||
This can be configured in [vaultwarden directly](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS) or using a third-party reverse proxy ([some examples](https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples)).
|
This can be configured in [vaultwarden directly](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS) or using a third-party reverse proxy ([some examples](https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples)).
|
||||||
|
|
||||||
@@ -49,42 +52,44 @@ If you have an available domain name, you can get HTTPS certificates with [Let's
|
|||||||
See the [vaultwarden wiki](https://github.com/dani-garcia/vaultwarden/wiki) for more information on how to configure and run the vaultwarden server.
|
See the [vaultwarden wiki](https://github.com/dani-garcia/vaultwarden/wiki) for more information on how to configure and run the vaultwarden server.
|
||||||
|
|
||||||
## Get in touch
|
## Get in touch
|
||||||
To ask a question, offer suggestions or new features or to get help configuring or installing the software, please [use the forum](https://vaultwarden.discourse.group/).
|
To ask a question, offer suggestions or new features or to get help configuring or installing the software, please use [GitHub Discussions](https://github.com/dani-garcia/vaultwarden/discussions) or [the forum](https://vaultwarden.discourse.group/).
|
||||||
|
|
||||||
If you spot any bugs or crashes with vaultwarden itself, please [create an issue](https://github.com/dani-garcia/vaultwarden/issues/). Make sure there aren't any similar issues open, though!
|
If you spot any bugs or crashes with vaultwarden itself, please [create an issue](https://github.com/dani-garcia/vaultwarden/issues/). Make sure you are on the latest version and there aren't any similar issues open, though!
|
||||||
|
|
||||||
If you prefer to chat, we're usually hanging around at [#vaultwarden:matrix.org](https://matrix.to/#/#vaultwarden:matrix.org) room on Matrix. Feel free to join us!
|
If you prefer to chat, we're usually hanging around at [#vaultwarden:matrix.org](https://matrix.to/#/#vaultwarden:matrix.org) room on Matrix. Feel free to join us!
|
||||||
|
|
||||||
### Sponsors
|
### Sponsors
|
||||||
Thanks for your contribution to the project!
|
Thanks for your contribution to the project!
|
||||||
|
|
||||||
|
<!--
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/netdadaltd">
|
<a href="https://github.com/username">
|
||||||
<img src="https://avatars.githubusercontent.com/u/77323954?s=75&v=4" width="75px;" alt="netdadaltd"/>
|
<img src="https://avatars.githubusercontent.com/u/725423?s=75&v=4" width="75px;" alt="username"/>
|
||||||
<br />
|
<br />
|
||||||
<sub><b>netDada Ltd.</b></sub>
|
<sub><b>username</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
-->
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/Gyarbij" style="width: 75px">
|
<a href="https://github.com/themightychris" style="width: 75px">
|
||||||
<sub><b>Chono N</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center">
|
|
||||||
<a href="https://github.com/themightychris">
|
|
||||||
<sub><b>Chris Alfano</b></sub>
|
<sub><b>Chris Alfano</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/numberly" style="width: 75px">
|
||||||
|
<sub><b>Numberly</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
[global.limits]
|
|
||||||
json = 10485760 # 10 MiB
|
|
||||||
21
build.rs
@@ -9,20 +9,25 @@ fn main() {
|
|||||||
println!("cargo:rustc-cfg=mysql");
|
println!("cargo:rustc-cfg=mysql");
|
||||||
#[cfg(feature = "postgresql")]
|
#[cfg(feature = "postgresql")]
|
||||||
println!("cargo:rustc-cfg=postgresql");
|
println!("cargo:rustc-cfg=postgresql");
|
||||||
|
#[cfg(feature = "query_logger")]
|
||||||
|
println!("cargo:rustc-cfg=query_logger");
|
||||||
|
|
||||||
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
|
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
|
||||||
compile_error!(
|
compile_error!(
|
||||||
"You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite"
|
"You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[cfg(all(not(debug_assertions), feature = "query_logger"))]
|
||||||
|
compile_error!("Query Logging is only allowed during development, it is not intented for production usage!");
|
||||||
|
|
||||||
// Support $BWRS_VERSION for legacy compatibility, but default to $VW_VERSION.
|
// Support $BWRS_VERSION for legacy compatibility, but default to $VW_VERSION.
|
||||||
// If neither exist, read from git.
|
// If neither exist, read from git.
|
||||||
let maybe_vaultwarden_version =
|
let maybe_vaultwarden_version =
|
||||||
env::var("VW_VERSION").or_else(|_| env::var("BWRS_VERSION")).or_else(|_| version_from_git_info());
|
env::var("VW_VERSION").or_else(|_| env::var("BWRS_VERSION")).or_else(|_| version_from_git_info());
|
||||||
|
|
||||||
if let Ok(version) = maybe_vaultwarden_version {
|
if let Ok(version) = maybe_vaultwarden_version {
|
||||||
println!("cargo:rustc-env=VW_VERSION={}", version);
|
println!("cargo:rustc-env=VW_VERSION={version}");
|
||||||
println!("cargo:rustc-env=CARGO_PKG_VERSION={}", version);
|
println!("cargo:rustc-env=CARGO_PKG_VERSION={version}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,29 +52,29 @@ fn version_from_git_info() -> Result<String, std::io::Error> {
|
|||||||
// the current commit doesn't have an associated tag
|
// the current commit doesn't have an associated tag
|
||||||
let exact_tag = run(&["git", "describe", "--abbrev=0", "--tags", "--exact-match"]).ok();
|
let exact_tag = run(&["git", "describe", "--abbrev=0", "--tags", "--exact-match"]).ok();
|
||||||
if let Some(ref exact) = exact_tag {
|
if let Some(ref exact) = exact_tag {
|
||||||
println!("cargo:rustc-env=GIT_EXACT_TAG={}", exact);
|
println!("cargo:rustc-env=GIT_EXACT_TAG={exact}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// The last available tag, equal to exact_tag when
|
// The last available tag, equal to exact_tag when
|
||||||
// the current commit is tagged
|
// the current commit is tagged
|
||||||
let last_tag = run(&["git", "describe", "--abbrev=0", "--tags"])?;
|
let last_tag = run(&["git", "describe", "--abbrev=0", "--tags"])?;
|
||||||
println!("cargo:rustc-env=GIT_LAST_TAG={}", last_tag);
|
println!("cargo:rustc-env=GIT_LAST_TAG={last_tag}");
|
||||||
|
|
||||||
// The current branch name
|
// The current branch name
|
||||||
let branch = run(&["git", "rev-parse", "--abbrev-ref", "HEAD"])?;
|
let branch = run(&["git", "rev-parse", "--abbrev-ref", "HEAD"])?;
|
||||||
println!("cargo:rustc-env=GIT_BRANCH={}", branch);
|
println!("cargo:rustc-env=GIT_BRANCH={branch}");
|
||||||
|
|
||||||
// The current git commit hash
|
// The current git commit hash
|
||||||
let rev = run(&["git", "rev-parse", "HEAD"])?;
|
let rev = run(&["git", "rev-parse", "HEAD"])?;
|
||||||
let rev_short = rev.get(..8).unwrap_or_default();
|
let rev_short = rev.get(..8).unwrap_or_default();
|
||||||
println!("cargo:rustc-env=GIT_REV={}", rev_short);
|
println!("cargo:rustc-env=GIT_REV={rev_short}");
|
||||||
|
|
||||||
// Combined version
|
// Combined version
|
||||||
if let Some(exact) = exact_tag {
|
if let Some(exact) = exact_tag {
|
||||||
Ok(exact)
|
Ok(exact)
|
||||||
} else if &branch != "main" && &branch != "master" {
|
} else if &branch != "main" && &branch != "master" {
|
||||||
Ok(format!("{}-{} ({})", last_tag, rev_short, branch))
|
Ok(format!("{last_tag}-{rev_short} ({branch})"))
|
||||||
} else {
|
} else {
|
||||||
Ok(format!("{}-{}", last_tag, rev_short))
|
Ok(format!("{last_tag}-{rev_short}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
# The cross-built images have the build arch (`amd64`) embedded in the image
|
# The cross-built images have the build arch (`amd64`) embedded in the image
|
||||||
# manifest, rather than the target arch. For example:
|
# manifest, rather than the target arch. For example:
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -2,40 +2,42 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
{% set rust_version = "1.68.2" %}
|
||||||
{% set build_stage_base_image = "rust:1.58-buster" %}
|
{% set debian_version = "bullseye" %}
|
||||||
|
{% set alpine_version = "3.17" %}
|
||||||
|
{% set build_stage_base_image = "rust:%s-%s" % (rust_version, debian_version) %}
|
||||||
{% if "alpine" in target_file %}
|
{% if "alpine" in target_file %}
|
||||||
{% if "amd64" in target_file %}
|
{% if "amd64" in target_file %}
|
||||||
{% set build_stage_base_image = "blackdex/rust-musl:x86_64-musl-nightly-2022-01-23" %}
|
{% set build_stage_base_image = "blackdex/rust-musl:x86_64-musl-stable-%s" % rust_version %}
|
||||||
{% set runtime_stage_base_image = "alpine:3.15" %}
|
{% set runtime_stage_base_image = "alpine:%s" % alpine_version %}
|
||||||
{% set package_arch_target = "x86_64-unknown-linux-musl" %}
|
{% set package_arch_target = "x86_64-unknown-linux-musl" %}
|
||||||
{% elif "armv7" in target_file %}
|
{% elif "armv7" in target_file %}
|
||||||
{% set build_stage_base_image = "blackdex/rust-musl:armv7-musleabihf-nightly-2022-01-23" %}
|
{% set build_stage_base_image = "blackdex/rust-musl:armv7-musleabihf-stable-%s" % rust_version %}
|
||||||
{% set runtime_stage_base_image = "balenalib/armv7hf-alpine:3.15" %}
|
{% set runtime_stage_base_image = "balenalib/armv7hf-alpine:%s" % alpine_version %}
|
||||||
{% set package_arch_target = "armv7-unknown-linux-musleabihf" %}
|
{% set package_arch_target = "armv7-unknown-linux-musleabihf" %}
|
||||||
{% elif "armv6" in target_file %}
|
{% elif "armv6" in target_file %}
|
||||||
{% set build_stage_base_image = "blackdex/rust-musl:arm-musleabi-nightly-2022-01-23" %}
|
{% set build_stage_base_image = "blackdex/rust-musl:arm-musleabi-stable-%s" % rust_version %}
|
||||||
{% set runtime_stage_base_image = "balenalib/rpi-alpine:3.15" %}
|
{% set runtime_stage_base_image = "balenalib/rpi-alpine:%s" % alpine_version %}
|
||||||
{% set package_arch_target = "arm-unknown-linux-musleabi" %}
|
{% set package_arch_target = "arm-unknown-linux-musleabi" %}
|
||||||
{% elif "arm64" in target_file %}
|
{% elif "arm64" in target_file %}
|
||||||
{% set build_stage_base_image = "blackdex/rust-musl:aarch64-musl-nightly-2022-01-23" %}
|
{% set build_stage_base_image = "blackdex/rust-musl:aarch64-musl-stable-%s" % rust_version %}
|
||||||
{% set runtime_stage_base_image = "balenalib/aarch64-alpine:3.15" %}
|
{% set runtime_stage_base_image = "balenalib/aarch64-alpine:%s" % alpine_version %}
|
||||||
{% set package_arch_target = "aarch64-unknown-linux-musl" %}
|
{% set package_arch_target = "aarch64-unknown-linux-musl" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif "amd64" in target_file %}
|
{% elif "amd64" in target_file %}
|
||||||
{% set runtime_stage_base_image = "debian:buster-slim" %}
|
{% set runtime_stage_base_image = "debian:%s-slim" % debian_version %}
|
||||||
{% elif "arm64" in target_file %}
|
{% elif "arm64" in target_file %}
|
||||||
{% set runtime_stage_base_image = "balenalib/aarch64-debian:buster" %}
|
{% set runtime_stage_base_image = "balenalib/aarch64-debian:%s" % debian_version %}
|
||||||
{% set package_arch_name = "arm64" %}
|
{% set package_arch_name = "arm64" %}
|
||||||
{% set package_arch_target = "aarch64-unknown-linux-gnu" %}
|
{% set package_arch_target = "aarch64-unknown-linux-gnu" %}
|
||||||
{% set package_cross_compiler = "aarch64-linux-gnu" %}
|
{% set package_cross_compiler = "aarch64-linux-gnu" %}
|
||||||
{% elif "armv6" in target_file %}
|
{% elif "armv6" in target_file %}
|
||||||
{% set runtime_stage_base_image = "balenalib/rpi-debian:buster" %}
|
{% set runtime_stage_base_image = "balenalib/rpi-debian:%s" % debian_version %}
|
||||||
{% set package_arch_name = "armel" %}
|
{% set package_arch_name = "armel" %}
|
||||||
{% set package_arch_target = "arm-unknown-linux-gnueabi" %}
|
{% set package_arch_target = "arm-unknown-linux-gnueabi" %}
|
||||||
{% set package_cross_compiler = "arm-linux-gnueabi" %}
|
{% set package_cross_compiler = "arm-linux-gnueabi" %}
|
||||||
{% elif "armv7" in target_file %}
|
{% elif "armv7" in target_file %}
|
||||||
{% set runtime_stage_base_image = "balenalib/armv7hf-debian:buster" %}
|
{% set runtime_stage_base_image = "balenalib/armv7hf-debian:%s" % debian_version %}
|
||||||
{% set package_arch_name = "armhf" %}
|
{% set package_arch_name = "armhf" %}
|
||||||
{% set package_arch_target = "armv7-unknown-linux-gnueabihf" %}
|
{% set package_arch_target = "armv7-unknown-linux-gnueabihf" %}
|
||||||
{% set package_cross_compiler = "arm-linux-gnueabihf" %}
|
{% set package_cross_compiler = "arm-linux-gnueabihf" %}
|
||||||
@@ -50,7 +52,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% set package_arch_target_param = "" %}
|
{% set package_arch_target_param = "" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if "buildx" in target_file %}
|
{% if "buildkit" in target_file %}
|
||||||
{% set mount_rust_cache = "--mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry " %}
|
{% set mount_rust_cache = "--mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry " %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set mount_rust_cache = "" %}
|
{% set mount_rust_cache = "" %}
|
||||||
@@ -59,8 +61,8 @@
|
|||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
{% set vault_version = "2.25.1b" %}
|
{% set vault_version = "v2023.3.0b" %}
|
||||||
{% set vault_image_digest = "sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba" %}
|
{% set vault_image_digest = "sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee" %}
|
||||||
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
||||||
# Using the digest instead of the tag name provides better security,
|
# Using the digest instead of the tag name provides better security,
|
||||||
# as the digest of an image is immutable, whereas a tag name can later
|
# as the digest of an image is immutable, whereas a tag name can later
|
||||||
@@ -70,21 +72,19 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v{{ vault_version }}
|
# $ docker pull vaultwarden/web-vault:{{ vault_version }}
|
||||||
# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" vaultwarden/web-vault:v{{ vault_version }}
|
# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" vaultwarden/web-vault:{{ vault_version }}
|
||||||
# [vaultwarden/web-vault@{{ vault_image_digest }}]
|
# [vaultwarden/web-vault@{{ vault_image_digest }}]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" vaultwarden/web-vault@{{ vault_image_digest }}
|
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" vaultwarden/web-vault@{{ vault_image_digest }}
|
||||||
# [vaultwarden/web-vault:v{{ vault_version }}]
|
# [vaultwarden/web-vault:{{ vault_version }}]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@{{ vault_image_digest }} as vault
|
FROM vaultwarden/web-vault@{{ vault_image_digest }} as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM {{ build_stage_base_image }} as build
|
FROM {{ build_stage_base_image }} as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
LANG=C.UTF-8 \
|
LANG=C.UTF-8 \
|
||||||
@@ -93,39 +93,29 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
{# {% if "alpine" not in target_file and "buildx" in target_file %}
|
|
||||||
# Debian based Buildx builds can use some special apt caching to speedup building.
|
|
||||||
# By default Debian based images have some rules to keep docker builds clean, we need to remove this.
|
|
||||||
# See: https://hub.docker.com/r/docker/dockerfile
|
|
||||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
|
||||||
{% endif %} #}
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN {{ mount_rust_cache -}} mkdir -pv "${CARGO_HOME}" \
|
RUN {{ mount_rust_cache -}} mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
{% if "alpine" in target_file %}
|
{% if "alpine" in target_file %}
|
||||||
ENV RUSTFLAGS='-C link-arg=-s'
|
{% if "armv6" in target_file %}
|
||||||
{% if "armv7" in target_file %}
|
# To be able to build the armv6 image with mimalloc we need to specifically specify the libatomic.a file location
|
||||||
{#- https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html -#}
|
ENV RUSTFLAGS='-Clink-arg=/usr/local/musl/{{ package_arch_target }}/lib/libatomic.a'
|
||||||
ENV CFLAGS_armv7_unknown_linux_musleabihf="-mfpu=vfpv3-d16"
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif "arm" in target_file %}
|
{% elif "arm" in target_file %}
|
||||||
#
|
# Install build dependencies for the {{ package_arch_name }} architecture
|
||||||
# Install required build libs for {{ package_arch_name }} architecture.
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN dpkg --add-architecture {{ package_arch_name }} \
|
RUN dpkg --add-architecture {{ package_arch_name }} \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libssl-dev{{ package_arch_prefix }} \
|
gcc-{{ package_cross_compiler }} \
|
||||||
libc6-dev{{ package_arch_prefix }} \
|
libc6-dev{{ package_arch_prefix }} \
|
||||||
libpq5{{ package_arch_prefix }} \
|
|
||||||
libpq-dev{{ package_arch_prefix }} \
|
|
||||||
libmariadb3{{ package_arch_prefix }} \
|
|
||||||
libmariadb-dev{{ package_arch_prefix }} \
|
libmariadb-dev{{ package_arch_prefix }} \
|
||||||
libmariadb-dev-compat{{ package_arch_prefix }} \
|
libmariadb-dev-compat{{ package_arch_prefix }} \
|
||||||
gcc-{{ package_cross_compiler }} \
|
libmariadb3{{ package_arch_prefix }} \
|
||||||
|
libpq-dev{{ package_arch_prefix }} \
|
||||||
|
libpq5{{ package_arch_prefix }} \
|
||||||
|
libssl-dev{{ package_arch_prefix }} \
|
||||||
#
|
#
|
||||||
# Make sure cargo has the right target config
|
# Make sure cargo has the right target config
|
||||||
&& echo '[target.{{ package_arch_target }}]' >> "${CARGO_HOME}/config" \
|
&& echo '[target.{{ package_arch_target }}]' >> "${CARGO_HOME}/config" \
|
||||||
@@ -137,16 +127,13 @@ ENV CC_{{ package_arch_target | replace("-", "_") }}="/usr/bin/{{ package_cross_
|
|||||||
CROSS_COMPILE="1" \
|
CROSS_COMPILE="1" \
|
||||||
OPENSSL_INCLUDE_DIR="/usr/include/{{ package_cross_compiler }}" \
|
OPENSSL_INCLUDE_DIR="/usr/include/{{ package_cross_compiler }}" \
|
||||||
OPENSSL_LIB_DIR="/usr/lib/{{ package_cross_compiler }}"
|
OPENSSL_LIB_DIR="/usr/lib/{{ package_cross_compiler }}"
|
||||||
|
|
||||||
{% elif "amd64" in target_file %}
|
{% elif "amd64" in target_file %}
|
||||||
# Install DB packages
|
# Install build dependencies
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libmariadb-dev{{ package_arch_prefix }} \
|
libmariadb-dev \
|
||||||
libpq-dev{{ package_arch_prefix }} \
|
libpq-dev
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
@@ -163,7 +150,12 @@ RUN {{ mount_rust_cache -}} rustup target add {{ package_arch_target }}
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
||||||
|
{% if "alpine" in target_file %}
|
||||||
|
# Enable MiMalloc to improve performance on Alpine builds
|
||||||
|
ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
||||||
|
{% else %}
|
||||||
ARG DB=sqlite,mysql,postgresql
|
ARG DB=sqlite,mysql,postgresql
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
# Builds your dependencies and removes the
|
||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
@@ -180,30 +172,22 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN {{ mount_rust_cache -}} cargo build --features ${DB} --release{{ package_arch_target_param }}
|
RUN {{ mount_rust_cache -}} cargo build --features ${DB} --release{{ package_arch_target_param }}
|
||||||
{% if "alpine" in target_file %}
|
|
||||||
{% if "armv7" in target_file %}
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN musl-strip target/{{ package_arch_target }}/release/vaultwarden
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM {{ runtime_stage_base_image }}
|
FROM {{ runtime_stage_base_image }}
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_WORKERS=10
|
ROCKET_PORT=80
|
||||||
{%- if "alpine" in runtime_stage_base_image %} \
|
{%- if "alpine" in runtime_stage_base_image %} \
|
||||||
SSL_CERT_DIR=/etc/ssl/certs
|
SSL_CERT_DIR=/etc/ssl/certs
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% if "amd64" not in target_file %}
|
{% if "amd64" not in target_file %}
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-start" ]
|
RUN [ "cross-build-start" ]
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -211,26 +195,30 @@ RUN [ "cross-build-start" ]
|
|||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
{% if "alpine" in runtime_stage_base_image %}
|
{% if "alpine" in runtime_stage_base_image %}
|
||||||
&& apk add --no-cache \
|
&& apk add --no-cache \
|
||||||
openssl \
|
ca-certificates \
|
||||||
tzdata \
|
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
openssl \
|
||||||
ca-certificates
|
tzdata
|
||||||
{% else %}
|
{% else %}
|
||||||
&& apt-get update && apt-get install -y \
|
&& apt-get update && apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
|
openssl \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if "armv6" in target_file and "alpine" not in target_file %}
|
||||||
|
# In the Balena Bullseye images for armv6/rpi-debian there is a missing symlink.
|
||||||
|
# This symlink was there in the buster images, and for some reason this is needed.
|
||||||
|
RUN ln -v -s /lib/ld-linux-armhf.so.3 /lib/ld-linux.so.3
|
||||||
|
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
{% if "amd64" not in target_file %}
|
{% if "amd64" not in target_file %}
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -241,7 +229,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
{% if package_arch_target is defined %}
|
{% if package_arch_target is defined %}
|
||||||
COPY --from=build /app/target/{{ package_arch_target }}/release/vaultwarden .
|
COPY --from=build /app/target/{{ package_arch_target }}/release/vaultwarden .
|
||||||
@@ -254,6 +241,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ all: $(OBJECTS)
|
|||||||
%/Dockerfile.alpine: Dockerfile.j2 render_template
|
%/Dockerfile.alpine: Dockerfile.j2 render_template
|
||||||
./render_template "$<" "{\"target_file\":\"$@\"}" > "$@"
|
./render_template "$<" "{\"target_file\":\"$@\"}" > "$@"
|
||||||
|
|
||||||
%/Dockerfile.buildx: Dockerfile.j2 render_template
|
%/Dockerfile.buildkit: Dockerfile.j2 render_template
|
||||||
./render_template "$<" "{\"target_file\":\"$@\"}" > "$@"
|
./render_template "$<" "{\"target_file\":\"$@\"}" > "$@"
|
||||||
|
|
||||||
%/Dockerfile.buildx.alpine: Dockerfile.j2 render_template
|
%/Dockerfile.buildkit.alpine: Dockerfile.j2 render_template
|
||||||
./render_template "$<" "{\"target_file\":\"$@\"}" > "$@"
|
./render_template "$<" "{\"target_file\":\"$@\"}" > "$@"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.58-buster as build
|
FROM rust:1.68.2-bullseye as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,19 +36,16 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN mkdir -pv "${CARGO_HOME}" \
|
RUN mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
# Install DB packages
|
# Install build dependencies
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libmariadb-dev \
|
libmariadb-dev \
|
||||||
libpq-dev \
|
libpq-dev
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -81,29 +75,27 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN cargo build --features ${DB} --release
|
RUN cargo build --features ${DB} --release
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM debian:buster-slim
|
FROM debian:bullseye-slim
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_WORKERS=10
|
ROCKET_PORT=80
|
||||||
|
|
||||||
|
|
||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apt-get update && apt-get install -y \
|
&& apt-get update && apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
|
openssl \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
@@ -115,7 +107,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/release/vaultwarden .
|
COPY --from=build /app/target/release/vaultwarden .
|
||||||
|
|
||||||
@@ -124,6 +115,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:x86_64-musl-nightly-2022-01-23 as build
|
FROM blackdex/rust-musl:x86_64-musl-stable-1.68.2 as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,12 +36,10 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN mkdir -pv "${CARGO_HOME}" \
|
RUN mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
ENV RUSTFLAGS='-C link-arg=-s'
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -58,7 +53,8 @@ COPY ./build.rs ./build.rs
|
|||||||
RUN rustup target add x86_64-unknown-linux-musl
|
RUN rustup target add x86_64-unknown-linux-musl
|
||||||
|
|
||||||
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
||||||
ARG DB=sqlite,mysql,postgresql
|
# Enable MiMalloc to improve performance on Alpine builds
|
||||||
|
ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
# Builds your dependencies and removes the
|
||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
@@ -75,17 +71,16 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
|
RUN cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM alpine:3.15
|
FROM alpine:3.17
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_PORT=80 \
|
||||||
ROCKET_WORKERS=10 \
|
|
||||||
SSL_CERT_DIR=/etc/ssl/certs
|
SSL_CERT_DIR=/etc/ssl/certs
|
||||||
|
|
||||||
|
|
||||||
@@ -93,11 +88,10 @@ ENV ROCKET_ENV="staging" \
|
|||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apk add --no-cache \
|
&& apk add --no-cache \
|
||||||
openssl \
|
ca-certificates \
|
||||||
tzdata \
|
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
openssl \
|
||||||
ca-certificates
|
tzdata
|
||||||
|
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
@@ -107,7 +101,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/vaultwarden .
|
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/vaultwarden .
|
||||||
|
|
||||||
@@ -116,6 +109,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.58-buster as build
|
FROM rust:1.68.2-bullseye as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,19 +36,16 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
# Install DB packages
|
# Install build dependencies
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libmariadb-dev \
|
libmariadb-dev \
|
||||||
libpq-dev \
|
libpq-dev
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -81,29 +75,27 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM debian:buster-slim
|
FROM debian:bullseye-slim
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_WORKERS=10
|
ROCKET_PORT=80
|
||||||
|
|
||||||
|
|
||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apt-get update && apt-get install -y \
|
&& apt-get update && apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
|
openssl \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
@@ -115,7 +107,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/release/vaultwarden .
|
COPY --from=build /app/target/release/vaultwarden .
|
||||||
|
|
||||||
@@ -124,6 +115,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:x86_64-musl-nightly-2022-01-23 as build
|
FROM blackdex/rust-musl:x86_64-musl-stable-1.68.2 as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,12 +36,10 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
ENV RUSTFLAGS='-C link-arg=-s'
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -58,7 +53,8 @@ COPY ./build.rs ./build.rs
|
|||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry rustup target add x86_64-unknown-linux-musl
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry rustup target add x86_64-unknown-linux-musl
|
||||||
|
|
||||||
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
||||||
ARG DB=sqlite,mysql,postgresql
|
# Enable MiMalloc to improve performance on Alpine builds
|
||||||
|
ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
# Builds your dependencies and removes the
|
||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
@@ -75,17 +71,16 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM alpine:3.15
|
FROM alpine:3.17
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_PORT=80 \
|
||||||
ROCKET_WORKERS=10 \
|
|
||||||
SSL_CERT_DIR=/etc/ssl/certs
|
SSL_CERT_DIR=/etc/ssl/certs
|
||||||
|
|
||||||
|
|
||||||
@@ -93,11 +88,10 @@ ENV ROCKET_ENV="staging" \
|
|||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apk add --no-cache \
|
&& apk add --no-cache \
|
||||||
openssl \
|
ca-certificates \
|
||||||
tzdata \
|
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
openssl \
|
||||||
ca-certificates
|
tzdata
|
||||||
|
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
@@ -107,7 +101,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/vaultwarden .
|
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/vaultwarden .
|
||||||
|
|
||||||
@@ -116,6 +109,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.58-buster as build
|
FROM rust:1.68.2-bullseye as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,26 +36,23 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN mkdir -pv "${CARGO_HOME}" \
|
RUN mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
#
|
# Install build dependencies for the arm64 architecture
|
||||||
# Install required build libs for arm64 architecture.
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN dpkg --add-architecture arm64 \
|
RUN dpkg --add-architecture arm64 \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libssl-dev:arm64 \
|
gcc-aarch64-linux-gnu \
|
||||||
libc6-dev:arm64 \
|
libc6-dev:arm64 \
|
||||||
libpq5:arm64 \
|
|
||||||
libpq-dev:arm64 \
|
|
||||||
libmariadb3:arm64 \
|
|
||||||
libmariadb-dev:arm64 \
|
libmariadb-dev:arm64 \
|
||||||
libmariadb-dev-compat:arm64 \
|
libmariadb-dev-compat:arm64 \
|
||||||
gcc-aarch64-linux-gnu \
|
libmariadb3:arm64 \
|
||||||
|
libpq-dev:arm64 \
|
||||||
|
libpq5:arm64 \
|
||||||
|
libssl-dev:arm64 \
|
||||||
#
|
#
|
||||||
# Make sure cargo has the right target config
|
# Make sure cargo has the right target config
|
||||||
&& echo '[target.aarch64-unknown-linux-gnu]' >> "${CARGO_HOME}/config" \
|
&& echo '[target.aarch64-unknown-linux-gnu]' >> "${CARGO_HOME}/config" \
|
||||||
@@ -71,7 +65,6 @@ ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc" \
|
|||||||
OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu" \
|
OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu" \
|
||||||
OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu"
|
OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu"
|
||||||
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -101,35 +94,31 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/aarch64-debian:buster
|
FROM balenalib/aarch64-debian:bullseye
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_WORKERS=10
|
ROCKET_PORT=80
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-start" ]
|
RUN [ "cross-build-start" ]
|
||||||
|
|
||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apt-get update && apt-get install -y \
|
&& apt-get update && apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
|
openssl \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
@@ -139,7 +128,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/vaultwarden .
|
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/vaultwarden .
|
||||||
|
|
||||||
@@ -148,6 +136,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:aarch64-musl-nightly-2022-01-23 as build
|
FROM blackdex/rust-musl:aarch64-musl-stable-1.68.2 as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,12 +36,10 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN mkdir -pv "${CARGO_HOME}" \
|
RUN mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
ENV RUSTFLAGS='-C link-arg=-s'
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -58,7 +53,8 @@ COPY ./build.rs ./build.rs
|
|||||||
RUN rustup target add aarch64-unknown-linux-musl
|
RUN rustup target add aarch64-unknown-linux-musl
|
||||||
|
|
||||||
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
||||||
ARG DB=sqlite,mysql,postgresql
|
# Enable MiMalloc to improve performance on Alpine builds
|
||||||
|
ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
# Builds your dependencies and removes the
|
||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
@@ -75,33 +71,29 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-musl
|
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-musl
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/aarch64-alpine:3.15
|
FROM balenalib/aarch64-alpine:3.17
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_PORT=80 \
|
||||||
ROCKET_WORKERS=10 \
|
|
||||||
SSL_CERT_DIR=/etc/ssl/certs
|
SSL_CERT_DIR=/etc/ssl/certs
|
||||||
|
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-start" ]
|
RUN [ "cross-build-start" ]
|
||||||
|
|
||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apk add --no-cache \
|
&& apk add --no-cache \
|
||||||
openssl \
|
ca-certificates \
|
||||||
tzdata \
|
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
openssl \
|
||||||
ca-certificates
|
tzdata
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
@@ -111,7 +103,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/aarch64-unknown-linux-musl/release/vaultwarden .
|
COPY --from=build /app/target/aarch64-unknown-linux-musl/release/vaultwarden .
|
||||||
|
|
||||||
@@ -120,6 +111,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.58-buster as build
|
FROM rust:1.68.2-bullseye as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,26 +36,23 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
#
|
# Install build dependencies for the arm64 architecture
|
||||||
# Install required build libs for arm64 architecture.
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN dpkg --add-architecture arm64 \
|
RUN dpkg --add-architecture arm64 \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libssl-dev:arm64 \
|
gcc-aarch64-linux-gnu \
|
||||||
libc6-dev:arm64 \
|
libc6-dev:arm64 \
|
||||||
libpq5:arm64 \
|
|
||||||
libpq-dev:arm64 \
|
|
||||||
libmariadb3:arm64 \
|
|
||||||
libmariadb-dev:arm64 \
|
libmariadb-dev:arm64 \
|
||||||
libmariadb-dev-compat:arm64 \
|
libmariadb-dev-compat:arm64 \
|
||||||
gcc-aarch64-linux-gnu \
|
libmariadb3:arm64 \
|
||||||
|
libpq-dev:arm64 \
|
||||||
|
libpq5:arm64 \
|
||||||
|
libssl-dev:arm64 \
|
||||||
#
|
#
|
||||||
# Make sure cargo has the right target config
|
# Make sure cargo has the right target config
|
||||||
&& echo '[target.aarch64-unknown-linux-gnu]' >> "${CARGO_HOME}/config" \
|
&& echo '[target.aarch64-unknown-linux-gnu]' >> "${CARGO_HOME}/config" \
|
||||||
@@ -71,7 +65,6 @@ ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc" \
|
|||||||
OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu" \
|
OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu" \
|
||||||
OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu"
|
OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu"
|
||||||
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -101,35 +94,31 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/aarch64-debian:buster
|
FROM balenalib/aarch64-debian:bullseye
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_WORKERS=10
|
ROCKET_PORT=80
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-start" ]
|
RUN [ "cross-build-start" ]
|
||||||
|
|
||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apt-get update && apt-get install -y \
|
&& apt-get update && apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
|
openssl \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
@@ -139,7 +128,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/vaultwarden .
|
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/vaultwarden .
|
||||||
|
|
||||||
@@ -148,6 +136,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:aarch64-musl-nightly-2022-01-23 as build
|
FROM blackdex/rust-musl:aarch64-musl-stable-1.68.2 as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,12 +36,10 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
ENV RUSTFLAGS='-C link-arg=-s'
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -58,7 +53,8 @@ COPY ./build.rs ./build.rs
|
|||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry rustup target add aarch64-unknown-linux-musl
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry rustup target add aarch64-unknown-linux-musl
|
||||||
|
|
||||||
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
||||||
ARG DB=sqlite,mysql,postgresql
|
# Enable MiMalloc to improve performance on Alpine builds
|
||||||
|
ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
# Builds your dependencies and removes the
|
||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
@@ -75,33 +71,29 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=aarch64-unknown-linux-musl
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=aarch64-unknown-linux-musl
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/aarch64-alpine:3.15
|
FROM balenalib/aarch64-alpine:3.17
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_PORT=80 \
|
||||||
ROCKET_WORKERS=10 \
|
|
||||||
SSL_CERT_DIR=/etc/ssl/certs
|
SSL_CERT_DIR=/etc/ssl/certs
|
||||||
|
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-start" ]
|
RUN [ "cross-build-start" ]
|
||||||
|
|
||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apk add --no-cache \
|
&& apk add --no-cache \
|
||||||
openssl \
|
ca-certificates \
|
||||||
tzdata \
|
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
openssl \
|
||||||
ca-certificates
|
tzdata
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
@@ -111,7 +103,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/aarch64-unknown-linux-musl/release/vaultwarden .
|
COPY --from=build /app/target/aarch64-unknown-linux-musl/release/vaultwarden .
|
||||||
|
|
||||||
@@ -120,6 +111,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.58-buster as build
|
FROM rust:1.68.2-bullseye as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,26 +36,23 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN mkdir -pv "${CARGO_HOME}" \
|
RUN mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
#
|
# Install build dependencies for the armel architecture
|
||||||
# Install required build libs for armel architecture.
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN dpkg --add-architecture armel \
|
RUN dpkg --add-architecture armel \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libssl-dev:armel \
|
gcc-arm-linux-gnueabi \
|
||||||
libc6-dev:armel \
|
libc6-dev:armel \
|
||||||
libpq5:armel \
|
|
||||||
libpq-dev:armel \
|
|
||||||
libmariadb3:armel \
|
|
||||||
libmariadb-dev:armel \
|
libmariadb-dev:armel \
|
||||||
libmariadb-dev-compat:armel \
|
libmariadb-dev-compat:armel \
|
||||||
gcc-arm-linux-gnueabi \
|
libmariadb3:armel \
|
||||||
|
libpq-dev:armel \
|
||||||
|
libpq5:armel \
|
||||||
|
libssl-dev:armel \
|
||||||
#
|
#
|
||||||
# Make sure cargo has the right target config
|
# Make sure cargo has the right target config
|
||||||
&& echo '[target.arm-unknown-linux-gnueabi]' >> "${CARGO_HOME}/config" \
|
&& echo '[target.arm-unknown-linux-gnueabi]' >> "${CARGO_HOME}/config" \
|
||||||
@@ -71,7 +65,6 @@ ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc" \
|
|||||||
OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi" \
|
OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi" \
|
||||||
OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi"
|
OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi"
|
||||||
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -101,35 +94,35 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/rpi-debian:buster
|
FROM balenalib/rpi-debian:bullseye
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_WORKERS=10
|
ROCKET_PORT=80
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-start" ]
|
RUN [ "cross-build-start" ]
|
||||||
|
|
||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apt-get update && apt-get install -y \
|
&& apt-get update && apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
|
openssl \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
# In the Balena Bullseye images for armv6/rpi-debian there is a missing symlink.
|
||||||
|
# This symlink was there in the buster images, and for some reason this is needed.
|
||||||
|
RUN ln -v -s /lib/ld-linux-armhf.so.3 /lib/ld-linux.so.3
|
||||||
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
@@ -139,7 +132,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/vaultwarden .
|
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/vaultwarden .
|
||||||
|
|
||||||
@@ -148,6 +140,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:arm-musleabi-nightly-2022-01-23 as build
|
FROM blackdex/rust-musl:arm-musleabi-stable-1.68.2 as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,12 +36,12 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN mkdir -pv "${CARGO_HOME}" \
|
RUN mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
ENV RUSTFLAGS='-C link-arg=-s'
|
# To be able to build the armv6 image with mimalloc we need to specifically specify the libatomic.a file location
|
||||||
|
ENV RUSTFLAGS='-Clink-arg=/usr/local/musl/arm-unknown-linux-musleabi/lib/libatomic.a'
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -58,7 +55,8 @@ COPY ./build.rs ./build.rs
|
|||||||
RUN rustup target add arm-unknown-linux-musleabi
|
RUN rustup target add arm-unknown-linux-musleabi
|
||||||
|
|
||||||
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
||||||
ARG DB=sqlite,mysql,postgresql
|
# Enable MiMalloc to improve performance on Alpine builds
|
||||||
|
ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
# Builds your dependencies and removes the
|
||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
@@ -75,33 +73,29 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-musleabi
|
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-musleabi
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/rpi-alpine:3.15
|
FROM balenalib/rpi-alpine:3.17
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_PORT=80 \
|
||||||
ROCKET_WORKERS=10 \
|
|
||||||
SSL_CERT_DIR=/etc/ssl/certs
|
SSL_CERT_DIR=/etc/ssl/certs
|
||||||
|
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-start" ]
|
RUN [ "cross-build-start" ]
|
||||||
|
|
||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apk add --no-cache \
|
&& apk add --no-cache \
|
||||||
openssl \
|
ca-certificates \
|
||||||
tzdata \
|
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
openssl \
|
||||||
ca-certificates
|
tzdata
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
@@ -111,7 +105,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/arm-unknown-linux-musleabi/release/vaultwarden .
|
COPY --from=build /app/target/arm-unknown-linux-musleabi/release/vaultwarden .
|
||||||
|
|
||||||
@@ -120,6 +113,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.58-buster as build
|
FROM rust:1.68.2-bullseye as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,26 +36,23 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
#
|
# Install build dependencies for the armel architecture
|
||||||
# Install required build libs for armel architecture.
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN dpkg --add-architecture armel \
|
RUN dpkg --add-architecture armel \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libssl-dev:armel \
|
gcc-arm-linux-gnueabi \
|
||||||
libc6-dev:armel \
|
libc6-dev:armel \
|
||||||
libpq5:armel \
|
|
||||||
libpq-dev:armel \
|
|
||||||
libmariadb3:armel \
|
|
||||||
libmariadb-dev:armel \
|
libmariadb-dev:armel \
|
||||||
libmariadb-dev-compat:armel \
|
libmariadb-dev-compat:armel \
|
||||||
gcc-arm-linux-gnueabi \
|
libmariadb3:armel \
|
||||||
|
libpq-dev:armel \
|
||||||
|
libpq5:armel \
|
||||||
|
libssl-dev:armel \
|
||||||
#
|
#
|
||||||
# Make sure cargo has the right target config
|
# Make sure cargo has the right target config
|
||||||
&& echo '[target.arm-unknown-linux-gnueabi]' >> "${CARGO_HOME}/config" \
|
&& echo '[target.arm-unknown-linux-gnueabi]' >> "${CARGO_HOME}/config" \
|
||||||
@@ -71,7 +65,6 @@ ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc" \
|
|||||||
OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi" \
|
OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi" \
|
||||||
OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi"
|
OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi"
|
||||||
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -101,35 +94,35 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/rpi-debian:buster
|
FROM balenalib/rpi-debian:bullseye
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_WORKERS=10
|
ROCKET_PORT=80
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-start" ]
|
RUN [ "cross-build-start" ]
|
||||||
|
|
||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apt-get update && apt-get install -y \
|
&& apt-get update && apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
|
openssl \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
# In the Balena Bullseye images for armv6/rpi-debian there is a missing symlink.
|
||||||
|
# This symlink was there in the buster images, and for some reason this is needed.
|
||||||
|
RUN ln -v -s /lib/ld-linux-armhf.so.3 /lib/ld-linux.so.3
|
||||||
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
@@ -139,7 +132,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/vaultwarden .
|
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/vaultwarden .
|
||||||
|
|
||||||
@@ -148,6 +140,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:arm-musleabi-nightly-2022-01-23 as build
|
FROM blackdex/rust-musl:arm-musleabi-stable-1.68.2 as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,12 +36,12 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
ENV RUSTFLAGS='-C link-arg=-s'
|
# To be able to build the armv6 image with mimalloc we need to specifically specify the libatomic.a file location
|
||||||
|
ENV RUSTFLAGS='-Clink-arg=/usr/local/musl/arm-unknown-linux-musleabi/lib/libatomic.a'
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -58,7 +55,8 @@ COPY ./build.rs ./build.rs
|
|||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry rustup target add arm-unknown-linux-musleabi
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry rustup target add arm-unknown-linux-musleabi
|
||||||
|
|
||||||
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
||||||
ARG DB=sqlite,mysql,postgresql
|
# Enable MiMalloc to improve performance on Alpine builds
|
||||||
|
ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
# Builds your dependencies and removes the
|
||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
@@ -75,33 +73,29 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=arm-unknown-linux-musleabi
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=arm-unknown-linux-musleabi
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/rpi-alpine:3.15
|
FROM balenalib/rpi-alpine:3.17
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_PORT=80 \
|
||||||
ROCKET_WORKERS=10 \
|
|
||||||
SSL_CERT_DIR=/etc/ssl/certs
|
SSL_CERT_DIR=/etc/ssl/certs
|
||||||
|
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-start" ]
|
RUN [ "cross-build-start" ]
|
||||||
|
|
||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apk add --no-cache \
|
&& apk add --no-cache \
|
||||||
openssl \
|
ca-certificates \
|
||||||
tzdata \
|
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
openssl \
|
||||||
ca-certificates
|
tzdata
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
@@ -111,7 +105,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/arm-unknown-linux-musleabi/release/vaultwarden .
|
COPY --from=build /app/target/arm-unknown-linux-musleabi/release/vaultwarden .
|
||||||
|
|
||||||
@@ -120,6 +113,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.58-buster as build
|
FROM rust:1.68.2-bullseye as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,26 +36,23 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN mkdir -pv "${CARGO_HOME}" \
|
RUN mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
#
|
# Install build dependencies for the armhf architecture
|
||||||
# Install required build libs for armhf architecture.
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN dpkg --add-architecture armhf \
|
RUN dpkg --add-architecture armhf \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libssl-dev:armhf \
|
gcc-arm-linux-gnueabihf \
|
||||||
libc6-dev:armhf \
|
libc6-dev:armhf \
|
||||||
libpq5:armhf \
|
|
||||||
libpq-dev:armhf \
|
|
||||||
libmariadb3:armhf \
|
|
||||||
libmariadb-dev:armhf \
|
libmariadb-dev:armhf \
|
||||||
libmariadb-dev-compat:armhf \
|
libmariadb-dev-compat:armhf \
|
||||||
gcc-arm-linux-gnueabihf \
|
libmariadb3:armhf \
|
||||||
|
libpq-dev:armhf \
|
||||||
|
libpq5:armhf \
|
||||||
|
libssl-dev:armhf \
|
||||||
#
|
#
|
||||||
# Make sure cargo has the right target config
|
# Make sure cargo has the right target config
|
||||||
&& echo '[target.armv7-unknown-linux-gnueabihf]' >> "${CARGO_HOME}/config" \
|
&& echo '[target.armv7-unknown-linux-gnueabihf]' >> "${CARGO_HOME}/config" \
|
||||||
@@ -71,7 +65,6 @@ ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc" \
|
|||||||
OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf" \
|
OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf" \
|
||||||
OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
|
OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
|
||||||
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -101,35 +94,31 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/armv7hf-debian:buster
|
FROM balenalib/armv7hf-debian:bullseye
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_WORKERS=10
|
ROCKET_PORT=80
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-start" ]
|
RUN [ "cross-build-start" ]
|
||||||
|
|
||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apt-get update && apt-get install -y \
|
&& apt-get update && apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
|
openssl \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
@@ -139,7 +128,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/vaultwarden .
|
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/vaultwarden .
|
||||||
|
|
||||||
@@ -148,6 +136,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:armv7-musleabihf-nightly-2022-01-23 as build
|
FROM blackdex/rust-musl:armv7-musleabihf-stable-1.68.2 as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,13 +36,10 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN mkdir -pv "${CARGO_HOME}" \
|
RUN mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
ENV RUSTFLAGS='-C link-arg=-s'
|
|
||||||
ENV CFLAGS_armv7_unknown_linux_musleabihf="-mfpu=vfpv3-d16"
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -59,7 +53,8 @@ COPY ./build.rs ./build.rs
|
|||||||
RUN rustup target add armv7-unknown-linux-musleabihf
|
RUN rustup target add armv7-unknown-linux-musleabihf
|
||||||
|
|
||||||
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
||||||
ARG DB=sqlite,mysql,postgresql
|
# Enable MiMalloc to improve performance on Alpine builds
|
||||||
|
ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
# Builds your dependencies and removes the
|
||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
@@ -76,35 +71,29 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabihf
|
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabihf
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN musl-strip target/armv7-unknown-linux-musleabihf/release/vaultwarden
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/armv7hf-alpine:3.15
|
FROM balenalib/armv7hf-alpine:3.17
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_PORT=80 \
|
||||||
ROCKET_WORKERS=10 \
|
|
||||||
SSL_CERT_DIR=/etc/ssl/certs
|
SSL_CERT_DIR=/etc/ssl/certs
|
||||||
|
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-start" ]
|
RUN [ "cross-build-start" ]
|
||||||
|
|
||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apk add --no-cache \
|
&& apk add --no-cache \
|
||||||
openssl \
|
ca-certificates \
|
||||||
tzdata \
|
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
openssl \
|
||||||
ca-certificates
|
tzdata
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
@@ -114,7 +103,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/armv7-unknown-linux-musleabihf/release/vaultwarden .
|
COPY --from=build /app/target/armv7-unknown-linux-musleabihf/release/vaultwarden .
|
||||||
|
|
||||||
@@ -123,6 +111,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.58-buster as build
|
FROM rust:1.68.2-bullseye as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,26 +36,23 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
#
|
# Install build dependencies for the armhf architecture
|
||||||
# Install required build libs for armhf architecture.
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN dpkg --add-architecture armhf \
|
RUN dpkg --add-architecture armhf \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libssl-dev:armhf \
|
gcc-arm-linux-gnueabihf \
|
||||||
libc6-dev:armhf \
|
libc6-dev:armhf \
|
||||||
libpq5:armhf \
|
|
||||||
libpq-dev:armhf \
|
|
||||||
libmariadb3:armhf \
|
|
||||||
libmariadb-dev:armhf \
|
libmariadb-dev:armhf \
|
||||||
libmariadb-dev-compat:armhf \
|
libmariadb-dev-compat:armhf \
|
||||||
gcc-arm-linux-gnueabihf \
|
libmariadb3:armhf \
|
||||||
|
libpq-dev:armhf \
|
||||||
|
libpq5:armhf \
|
||||||
|
libssl-dev:armhf \
|
||||||
#
|
#
|
||||||
# Make sure cargo has the right target config
|
# Make sure cargo has the right target config
|
||||||
&& echo '[target.armv7-unknown-linux-gnueabihf]' >> "${CARGO_HOME}/config" \
|
&& echo '[target.armv7-unknown-linux-gnueabihf]' >> "${CARGO_HOME}/config" \
|
||||||
@@ -71,7 +65,6 @@ ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc" \
|
|||||||
OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf" \
|
OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf" \
|
||||||
OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
|
OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
|
||||||
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -101,35 +94,31 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/armv7hf-debian:buster
|
FROM balenalib/armv7hf-debian:bullseye
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_WORKERS=10
|
ROCKET_PORT=80
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-start" ]
|
RUN [ "cross-build-start" ]
|
||||||
|
|
||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apt-get update && apt-get install -y \
|
&& apt-get update && apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
|
openssl \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
@@ -139,7 +128,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/vaultwarden .
|
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/vaultwarden .
|
||||||
|
|
||||||
@@ -148,6 +136,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,20 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2.25.1b
|
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||||
# [vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba]
|
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||||
# [vaultwarden/web-vault:v2.25.1b]
|
# [vaultwarden/web-vault:v2023.3.0b]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:9b82318d553d72f091e8755f5aff80eed495f90bbe5b0703522953480f5c2fba as vault
|
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:armv7-musleabihf-nightly-2022-01-23 as build
|
FROM blackdex/rust-musl:armv7-musleabihf-stable-1.68.2 as build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -39,13 +36,10 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
ENV RUSTFLAGS='-C link-arg=-s'
|
|
||||||
ENV CFLAGS_armv7_unknown_linux_musleabihf="-mfpu=vfpv3-d16"
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -59,7 +53,8 @@ COPY ./build.rs ./build.rs
|
|||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry rustup target add armv7-unknown-linux-musleabihf
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry rustup target add armv7-unknown-linux-musleabihf
|
||||||
|
|
||||||
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
||||||
ARG DB=sqlite,mysql,postgresql
|
# Enable MiMalloc to improve performance on Alpine builds
|
||||||
|
ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
# Builds your dependencies and removes the
|
||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
@@ -76,35 +71,29 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabihf
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabihf
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN musl-strip target/armv7-unknown-linux-musleabihf/release/vaultwarden
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/armv7hf-alpine:3.15
|
FROM balenalib/armv7hf-alpine:3.17
|
||||||
|
|
||||||
ENV ROCKET_ENV="staging" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
ROCKET_PORT=80 \
|
ROCKET_PORT=80 \
|
||||||
ROCKET_WORKERS=10 \
|
|
||||||
SSL_CERT_DIR=/etc/ssl/certs
|
SSL_CERT_DIR=/etc/ssl/certs
|
||||||
|
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-start" ]
|
RUN [ "cross-build-start" ]
|
||||||
|
|
||||||
# Create data folder and Install needed libraries
|
# Create data folder and Install needed libraries
|
||||||
RUN mkdir /data \
|
RUN mkdir /data \
|
||||||
&& apk add --no-cache \
|
&& apk add --no-cache \
|
||||||
openssl \
|
ca-certificates \
|
||||||
tzdata \
|
|
||||||
curl \
|
curl \
|
||||||
dumb-init \
|
openssl \
|
||||||
ca-certificates
|
tzdata
|
||||||
|
|
||||||
# hadolint ignore=DL3059
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
@@ -114,7 +103,6 @@ EXPOSE 3012
|
|||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/armv7-unknown-linux-musleabihf/release/vaultwarden .
|
COPY --from=build /app/target/armv7-unknown-linux-musleabihf/release/vaultwarden .
|
||||||
|
|
||||||
@@ -123,6 +111,4 @@ COPY docker/start.sh /start.sh
|
|||||||
|
|
||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
# Use the value of the corresponding env var (if present),
|
# Use the value of the corresponding env var (if present),
|
||||||
# or a default value otherwise.
|
# or a default value otherwise.
|
||||||
: ${DATA_FOLDER:="data"}
|
: "${DATA_FOLDER:="data"}"
|
||||||
: ${ROCKET_PORT:="80"}
|
: "${ROCKET_PORT:="80"}"
|
||||||
|
|
||||||
CONFIG_FILE="${DATA_FOLDER}"/config.json
|
CONFIG_FILE="${DATA_FOLDER}"/config.json
|
||||||
|
|
||||||
@@ -45,9 +45,13 @@ if [ -r "${CONFIG_FILE}" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
addr="${ROCKET_ADDRESS}"
|
||||||
|
if [ -z "${addr}" ] || [ "${addr}" = '0.0.0.0' ] || [ "${addr}" = '::' ]; then
|
||||||
|
addr='localhost'
|
||||||
|
fi
|
||||||
base_path="$(get_base_path "${DOMAIN}")"
|
base_path="$(get_base_path "${DOMAIN}")"
|
||||||
if [ -n "${ROCKET_TLS}" ]; then
|
if [ -n "${ROCKET_TLS}" ]; then
|
||||||
s='s'
|
s='s'
|
||||||
fi
|
fi
|
||||||
curl --insecure --fail --silent --show-error \
|
curl --insecure --fail --silent --show-error \
|
||||||
"http${s}://localhost:${ROCKET_PORT}${base_path}/alive" || exit 1
|
"http${s}://${addr}:${ROCKET_PORT}${base_path}/alive" || exit 1
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ fi
|
|||||||
|
|
||||||
if [ -d /etc/vaultwarden.d ]; then
|
if [ -d /etc/vaultwarden.d ]; then
|
||||||
for f in /etc/vaultwarden.d/*.sh; do
|
for f in /etc/vaultwarden.d/*.sh; do
|
||||||
if [ -r $f ]; then
|
if [ -r "${f}" ]; then
|
||||||
. $f
|
. "${f}"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
elif [ -d /etc/bitwarden_rs.d ]; then
|
elif [ -d /etc/bitwarden_rs.d ]; then
|
||||||
echo "### You are using the old /etc/bitwarden_rs.d script directory, please migrate to /etc/vaultwarden.d ###"
|
echo "### You are using the old /etc/bitwarden_rs.d script directory, please migrate to /etc/vaultwarden.d ###"
|
||||||
for f in /etc/bitwarden_rs.d/*.sh; do
|
for f in /etc/bitwarden_rs.d/*.sh; do
|
||||||
if [ -r $f ]; then
|
if [ -r "${f}" ]; then
|
||||||
. $f
|
. "${f}"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# The default Debian-based images support these arches for all database backends.
|
# The default Debian-based images support these arches for all database backends.
|
||||||
arches=(
|
arches=(
|
||||||
amd64
|
amd64
|
||||||
@@ -5,7 +7,9 @@ arches=(
|
|||||||
armv7
|
armv7
|
||||||
arm64
|
arm64
|
||||||
)
|
)
|
||||||
|
export arches
|
||||||
|
|
||||||
if [[ "${DOCKER_TAG}" == *alpine ]]; then
|
if [[ "${DOCKER_TAG}" == *alpine ]]; then
|
||||||
distro_suffix=.alpine
|
distro_suffix=.alpine
|
||||||
fi
|
fi
|
||||||
|
export distro_suffix
|
||||||
|
|||||||
13
hooks/build
@@ -1,7 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
echo ">>> Building images..."
|
echo ">>> Building images..."
|
||||||
|
|
||||||
|
# shellcheck source=arches.sh
|
||||||
source ./hooks/arches.sh
|
source ./hooks/arches.sh
|
||||||
|
|
||||||
if [[ -z "${SOURCE_COMMIT}" ]]; then
|
if [[ -z "${SOURCE_COMMIT}" ]]; then
|
||||||
@@ -23,10 +24,10 @@ LABELS=(
|
|||||||
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
|
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
|
||||||
org.opencontainers.image.created="$(date --utc --iso-8601=seconds)"
|
org.opencontainers.image.created="$(date --utc --iso-8601=seconds)"
|
||||||
org.opencontainers.image.documentation="https://github.com/dani-garcia/vaultwarden/wiki"
|
org.opencontainers.image.documentation="https://github.com/dani-garcia/vaultwarden/wiki"
|
||||||
org.opencontainers.image.licenses="GPL-3.0-only"
|
org.opencontainers.image.licenses="AGPL-3.0-only"
|
||||||
org.opencontainers.image.revision="${SOURCE_COMMIT}"
|
org.opencontainers.image.revision="${SOURCE_COMMIT}"
|
||||||
org.opencontainers.image.source="${SOURCE_REPOSITORY_URL}"
|
org.opencontainers.image.source="${SOURCE_REPOSITORY_URL}"
|
||||||
org.opencontainers.image.url="https://hub.docker.com/r/${DOCKER_REPO#*/}"
|
org.opencontainers.image.url="https://github.com/dani-garcia/vaultwarden"
|
||||||
org.opencontainers.image.version="${SOURCE_VERSION}"
|
org.opencontainers.image.version="${SOURCE_VERSION}"
|
||||||
)
|
)
|
||||||
LABEL_ARGS=()
|
LABEL_ARGS=()
|
||||||
@@ -34,9 +35,9 @@ for label in "${LABELS[@]}"; do
|
|||||||
LABEL_ARGS+=(--label "${label}")
|
LABEL_ARGS+=(--label "${label}")
|
||||||
done
|
done
|
||||||
|
|
||||||
# Check if DOCKER_BUILDKIT is set, if so, use the Dockerfile.buildx as template
|
# Check if DOCKER_BUILDKIT is set, if so, use the Dockerfile.buildkit as template
|
||||||
if [[ -n "${DOCKER_BUILDKIT}" ]]; then
|
if [[ -n "${DOCKER_BUILDKIT}" ]]; then
|
||||||
buildx_suffix=.buildx
|
buildkit_suffix=.buildkit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
@@ -45,6 +46,6 @@ for arch in "${arches[@]}"; do
|
|||||||
docker build \
|
docker build \
|
||||||
"${LABEL_ARGS[@]}" \
|
"${LABEL_ARGS[@]}" \
|
||||||
-t "${DOCKER_REPO}:${DOCKER_TAG}-${arch}" \
|
-t "${DOCKER_REPO}:${DOCKER_TAG}-${arch}" \
|
||||||
-f docker/${arch}/Dockerfile${buildx_suffix}${distro_suffix} \
|
-f "docker/${arch}/Dockerfile${buildkit_suffix}${distro_suffix}" \
|
||||||
.
|
.
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
|
|||||||
54
hooks/push
@@ -1,5 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# shellcheck source=arches.sh
|
||||||
source ./hooks/arches.sh
|
source ./hooks/arches.sh
|
||||||
|
|
||||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||||
@@ -41,7 +42,7 @@ LOCAL_REPO="${LOCAL_REGISTRY}/${REPO}"
|
|||||||
|
|
||||||
echo ">>> Pushing images to local registry..."
|
echo ">>> Pushing images to local registry..."
|
||||||
|
|
||||||
for arch in ${arches[@]}; do
|
for arch in "${arches[@]}"; do
|
||||||
docker_image="${DOCKER_REPO}:${DOCKER_TAG}-${arch}"
|
docker_image="${DOCKER_REPO}:${DOCKER_TAG}-${arch}"
|
||||||
local_image="${LOCAL_REPO}:${DOCKER_TAG}-${arch}"
|
local_image="${LOCAL_REPO}:${DOCKER_TAG}-${arch}"
|
||||||
docker tag "${docker_image}" "${local_image}"
|
docker tag "${docker_image}" "${local_image}"
|
||||||
@@ -71,9 +72,9 @@ tags=("${DOCKER_REPO}:${DOCKER_TAG}")
|
|||||||
# to make it easier for users to track the latest release.
|
# to make it easier for users to track the latest release.
|
||||||
if [[ "${DOCKER_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
if [[ "${DOCKER_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||||
if [[ "${DOCKER_TAG}" == *alpine ]]; then
|
if [[ "${DOCKER_TAG}" == *alpine ]]; then
|
||||||
tags+=(${DOCKER_REPO}:alpine)
|
tags+=("${DOCKER_REPO}:alpine")
|
||||||
else
|
else
|
||||||
tags+=(${DOCKER_REPO}:latest)
|
tags+=("${DOCKER_REPO}:latest")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -91,10 +92,10 @@ declare -A arch_to_platform=(
|
|||||||
[arm64]="linux/arm64"
|
[arm64]="linux/arm64"
|
||||||
)
|
)
|
||||||
platforms=()
|
platforms=()
|
||||||
for arch in ${arches[@]}; do
|
for arch in "${arches[@]}"; do
|
||||||
platforms+=("${arch_to_platform[$arch]}")
|
platforms+=("${arch_to_platform[$arch]}")
|
||||||
done
|
done
|
||||||
platforms="$(join "," "${platforms[@]}")"
|
platform="$(join "," "${platforms[@]}")"
|
||||||
|
|
||||||
# Run the build, pushing the resulting images and multi-arch manifest list to
|
# Run the build, pushing the resulting images and multi-arch manifest list to
|
||||||
# Docker Hub. The Dockerfile is read from stdin to avoid sending any build
|
# Docker Hub. The Dockerfile is read from stdin to avoid sending any build
|
||||||
@@ -104,46 +105,7 @@ docker buildx build \
|
|||||||
--network host \
|
--network host \
|
||||||
--build-arg LOCAL_REPO="${LOCAL_REPO}" \
|
--build-arg LOCAL_REPO="${LOCAL_REPO}" \
|
||||||
--build-arg DOCKER_TAG="${DOCKER_TAG}" \
|
--build-arg DOCKER_TAG="${DOCKER_TAG}" \
|
||||||
--platform "${platforms}" \
|
--platform "${platform}" \
|
||||||
"${tag_args[@]}" \
|
"${tag_args[@]}" \
|
||||||
--push \
|
--push \
|
||||||
- < ./docker/Dockerfile.buildx
|
- < ./docker/Dockerfile.buildx
|
||||||
|
|
||||||
# Add an extra arch-specific tag for `arm32v6`; Docker can't seem to properly
|
|
||||||
# auto-select that image on ARMv6 platforms like Raspberry Pi 1 and Zero
|
|
||||||
# (https://github.com/moby/moby/issues/41017).
|
|
||||||
#
|
|
||||||
# Note that we use `arm32v6` instead of `armv6` to be consistent with the
|
|
||||||
# existing vaultwarden tags, which adhere to the naming conventions of the
|
|
||||||
# Docker per-architecture repos (e.g., https://hub.docker.com/u/arm32v6).
|
|
||||||
# Unfortunately, these per-arch repo names aren't always consistent with the
|
|
||||||
# corresponding platform (OS/arch/variant) IDs, particularly in the case of
|
|
||||||
# 32-bit ARM arches (e.g., `linux/arm/v6` is used, not `linux/arm32/v6`).
|
|
||||||
#
|
|
||||||
# TODO: It looks like this issue should be fixed starting in Docker 20.10.0,
|
|
||||||
# so this step can be removed once fixed versions are in wider distribution.
|
|
||||||
#
|
|
||||||
# Tags:
|
|
||||||
#
|
|
||||||
# testing => testing-arm32v6
|
|
||||||
# testing-alpine => <ignored>
|
|
||||||
# x.y.z => x.y.z-arm32v6, latest-arm32v6
|
|
||||||
# x.y.z-alpine => <ignored>
|
|
||||||
#
|
|
||||||
if [[ "${DOCKER_TAG}" != *alpine ]]; then
|
|
||||||
image="${DOCKER_REPO}":"${DOCKER_TAG}"
|
|
||||||
|
|
||||||
# Fetch the multi-arch manifest list and find the digest of the armv6 image.
|
|
||||||
filter='.manifests|.[]|select(.platform.architecture=="arm" and .platform.variant=="v6")|.digest'
|
|
||||||
digest="$(docker manifest inspect "${image}" | jq -r "${filter}")"
|
|
||||||
|
|
||||||
# Pull the armv6 image by digest, retag it, and repush it.
|
|
||||||
docker pull "${DOCKER_REPO}"@"${digest}"
|
|
||||||
docker tag "${DOCKER_REPO}"@"${digest}" "${image}"-arm32v6
|
|
||||||
docker push "${image}"-arm32v6
|
|
||||||
|
|
||||||
if [[ "${DOCKER_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
|
||||||
docker tag "${image}"-arm32v6 "${DOCKER_REPO}:latest"-arm32v6
|
|
||||||
docker push "${DOCKER_REPO}:latest"-arm32v6
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- First remove the previous primary key
|
||||||
|
ALTER TABLE devices DROP PRIMARY KEY;
|
||||||
|
-- Add a new combined one
|
||||||
|
ALTER TABLE devices ADD PRIMARY KEY (uuid, user_uuid);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
DROP TABLE `groups`;
|
||||||
|
DROP TABLE groups_users;
|
||||||
|
DROP TABLE collections_groups;
|
||||||
23
migrations/mysql/2022-07-27-110000_add_group_support/up.sql
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
CREATE TABLE `groups` (
|
||||||
|
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
organizations_uuid VARCHAR(40) NOT NULL REFERENCES organizations (uuid),
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
access_all BOOLEAN NOT NULL,
|
||||||
|
external_id VARCHAR(300) NULL,
|
||||||
|
creation_date DATETIME NOT NULL,
|
||||||
|
revision_date DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE groups_users (
|
||||||
|
groups_uuid CHAR(36) NOT NULL REFERENCES `groups` (uuid),
|
||||||
|
users_organizations_uuid VARCHAR(36) NOT NULL REFERENCES users_organizations (uuid),
|
||||||
|
UNIQUE (groups_uuid, users_organizations_uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE collections_groups (
|
||||||
|
collections_uuid VARCHAR(40) NOT NULL REFERENCES collections (uuid),
|
||||||
|
groups_uuid CHAR(36) NOT NULL REFERENCES `groups` (uuid),
|
||||||
|
read_only BOOLEAN NOT NULL,
|
||||||
|
hide_passwords BOOLEAN NOT NULL,
|
||||||
|
UNIQUE (collections_uuid, groups_uuid)
|
||||||
|
);
|
||||||
1
migrations/mysql/2022-10-18-170602_add_events/down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE event;
|
||||||
19
migrations/mysql/2022-10-18-170602_add_events/up.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
CREATE TABLE event (
|
||||||
|
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
event_type INTEGER NOT NULL,
|
||||||
|
user_uuid CHAR(36),
|
||||||
|
org_uuid CHAR(36),
|
||||||
|
cipher_uuid CHAR(36),
|
||||||
|
collection_uuid CHAR(36),
|
||||||
|
group_uuid CHAR(36),
|
||||||
|
org_user_uuid CHAR(36),
|
||||||
|
act_user_uuid CHAR(36),
|
||||||
|
device_type INTEGER,
|
||||||
|
ip_address TEXT,
|
||||||
|
event_date DATETIME NOT NULL,
|
||||||
|
policy_uuid CHAR(36),
|
||||||
|
provider_uuid CHAR(36),
|
||||||
|
provider_user_uuid CHAR(36),
|
||||||
|
provider_org_uuid CHAR(36),
|
||||||
|
UNIQUE (uuid)
|
||||||
|
);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users_organizations
|
||||||
|
ADD COLUMN reset_password_key TEXT;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN avatar_color VARCHAR(7);
|
||||||
7
migrations/mysql/2023-01-31-222222_add_argon2/up.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN
|
||||||
|
client_kdf_memory INTEGER DEFAULT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN
|
||||||
|
client_kdf_parallelism INTEGER DEFAULT NULL;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- First remove the previous primary key
|
||||||
|
ALTER TABLE devices DROP CONSTRAINT devices_pkey;
|
||||||
|
-- Add a new combined one
|
||||||
|
ALTER TABLE devices ADD PRIMARY KEY (uuid, user_uuid);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
DROP TABLE groups;
|
||||||
|
DROP TABLE groups_users;
|
||||||
|
DROP TABLE collections_groups;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
CREATE TABLE groups (
|
||||||
|
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
organizations_uuid VARCHAR(40) NOT NULL REFERENCES organizations (uuid),
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
access_all BOOLEAN NOT NULL,
|
||||||
|
external_id VARCHAR(300) NULL,
|
||||||
|
creation_date TIMESTAMP NOT NULL,
|
||||||
|
revision_date TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE groups_users (
|
||||||
|
groups_uuid CHAR(36) NOT NULL REFERENCES groups (uuid),
|
||||||
|
users_organizations_uuid VARCHAR(36) NOT NULL REFERENCES users_organizations (uuid),
|
||||||
|
PRIMARY KEY (groups_uuid, users_organizations_uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE collections_groups (
|
||||||
|
collections_uuid VARCHAR(40) NOT NULL REFERENCES collections (uuid),
|
||||||
|
groups_uuid CHAR(36) NOT NULL REFERENCES groups (uuid),
|
||||||
|
read_only BOOLEAN NOT NULL,
|
||||||
|
hide_passwords BOOLEAN NOT NULL,
|
||||||
|
PRIMARY KEY (collections_uuid, groups_uuid)
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE event;
|
||||||
19
migrations/postgresql/2022-10-18-170602_add_events/up.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
CREATE TABLE event (
|
||||||
|
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
event_type INTEGER NOT NULL,
|
||||||
|
user_uuid CHAR(36),
|
||||||
|
org_uuid CHAR(36),
|
||||||
|
cipher_uuid CHAR(36),
|
||||||
|
collection_uuid CHAR(36),
|
||||||
|
group_uuid CHAR(36),
|
||||||
|
org_user_uuid CHAR(36),
|
||||||
|
act_user_uuid CHAR(36),
|
||||||
|
device_type INTEGER,
|
||||||
|
ip_address TEXT,
|
||||||
|
event_date TIMESTAMP NOT NULL,
|
||||||
|
policy_uuid CHAR(36),
|
||||||
|
provider_uuid CHAR(36),
|
||||||
|
provider_user_uuid CHAR(36),
|
||||||
|
provider_org_uuid CHAR(36),
|
||||||
|
UNIQUE (uuid)
|
||||||
|
);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users_organizations
|
||||||
|
ADD COLUMN reset_password_key TEXT;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN avatar_color TEXT;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN
|
||||||
|
client_kdf_memory INTEGER DEFAULT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN
|
||||||
|
client_kdf_parallelism INTEGER DEFAULT NULL;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- Create new devices table with primary keys on both uuid and user_uuid
|
||||||
|
CREATE TABLE devices_new (
|
||||||
|
uuid TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME NOT NULL,
|
||||||
|
user_uuid TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
atype INTEGER NOT NULL,
|
||||||
|
push_token TEXT,
|
||||||
|
refresh_token TEXT NOT NULL,
|
||||||
|
twofactor_remember TEXT,
|
||||||
|
PRIMARY KEY(uuid, user_uuid),
|
||||||
|
FOREIGN KEY(user_uuid) REFERENCES users(uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Transfer current data to new table
|
||||||
|
INSERT INTO devices_new SELECT * FROM devices;
|
||||||
|
|
||||||
|
-- Drop the old table
|
||||||
|
DROP TABLE devices;
|
||||||
|
|
||||||
|
-- Rename the new table to the original name
|
||||||
|
ALTER TABLE devices_new RENAME TO devices;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
DROP TABLE groups;
|
||||||
|
DROP TABLE groups_users;
|
||||||
|
DROP TABLE collections_groups;
|
||||||
23
migrations/sqlite/2022-07-27-110000_add_group_support/up.sql
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
CREATE TABLE groups (
|
||||||
|
uuid TEXT NOT NULL PRIMARY KEY,
|
||||||
|
organizations_uuid TEXT NOT NULL REFERENCES organizations (uuid),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
access_all BOOLEAN NOT NULL,
|
||||||
|
external_id TEXT NULL,
|
||||||
|
creation_date TIMESTAMP NOT NULL,
|
||||||
|
revision_date TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE groups_users (
|
||||||
|
groups_uuid TEXT NOT NULL REFERENCES groups (uuid),
|
||||||
|
users_organizations_uuid TEXT NOT NULL REFERENCES users_organizations (uuid),
|
||||||
|
UNIQUE (groups_uuid, users_organizations_uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE collections_groups (
|
||||||
|
collections_uuid TEXT NOT NULL REFERENCES collections (uuid),
|
||||||
|
groups_uuid TEXT NOT NULL REFERENCES groups (uuid),
|
||||||
|
read_only BOOLEAN NOT NULL,
|
||||||
|
hide_passwords BOOLEAN NOT NULL,
|
||||||
|
UNIQUE (collections_uuid, groups_uuid)
|
||||||
|
);
|
||||||
1
migrations/sqlite/2022-10-18-170602_add_events/down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE event;
|
||||||
19
migrations/sqlite/2022-10-18-170602_add_events/up.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
CREATE TABLE event (
|
||||||
|
uuid TEXT NOT NULL PRIMARY KEY,
|
||||||
|
event_type INTEGER NOT NULL,
|
||||||
|
user_uuid TEXT,
|
||||||
|
org_uuid TEXT,
|
||||||
|
cipher_uuid TEXT,
|
||||||
|
collection_uuid TEXT,
|
||||||
|
group_uuid TEXT,
|
||||||
|
org_user_uuid TEXT,
|
||||||
|
act_user_uuid TEXT,
|
||||||
|
device_type INTEGER,
|
||||||
|
ip_address TEXT,
|
||||||
|
event_date DATETIME NOT NULL,
|
||||||
|
policy_uuid TEXT,
|
||||||
|
provider_uuid TEXT,
|
||||||
|
provider_user_uuid TEXT,
|
||||||
|
provider_org_uuid TEXT,
|
||||||
|
UNIQUE (uuid)
|
||||||
|
);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users_organizations
|
||||||
|
ADD COLUMN reset_password_key TEXT;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN avatar_color TEXT;
|
||||||
7
migrations/sqlite/2023-01-31-222222_add_argon2/up.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN
|
||||||
|
client_kdf_memory INTEGER DEFAULT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN
|
||||||
|
client_kdf_parallelism INTEGER DEFAULT NULL;
|
||||||
93
resources/404.svg
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="500"
|
||||||
|
height="222"
|
||||||
|
viewBox="0 0 500 222"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
|
||||||
|
sodipodi:docname="404.svg"
|
||||||
|
inkscape:export-filename="404.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.3791767"
|
||||||
|
inkscape:cx="284.59007"
|
||||||
|
inkscape:cy="214.25826"
|
||||||
|
inkscape:window-width="1916"
|
||||||
|
inkscape:window-height="1038"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="18"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showguides="false" /><defs
|
||||||
|
id="defs2"><mask
|
||||||
|
id="holes"><rect
|
||||||
|
x="-60"
|
||||||
|
y="-60"
|
||||||
|
width="120"
|
||||||
|
height="120"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect3296" /><circle
|
||||||
|
id="hole"
|
||||||
|
cy="-40"
|
||||||
|
r="3"
|
||||||
|
cx="0" /><use
|
||||||
|
transform="rotate(72)"
|
||||||
|
xlink:href="#hole"
|
||||||
|
id="use3299" /><use
|
||||||
|
transform="rotate(144)"
|
||||||
|
xlink:href="#hole"
|
||||||
|
id="use3301" /><use
|
||||||
|
transform="rotate(-144)"
|
||||||
|
xlink:href="#hole"
|
||||||
|
id="use3303" /><use
|
||||||
|
transform="rotate(-72)"
|
||||||
|
xlink:href="#hole"
|
||||||
|
id="use3305" /></mask></defs><g
|
||||||
|
inkscape:label="Ebene 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"><rect
|
||||||
|
style="fill:none;fill-opacity:0.5;stroke:none;stroke-width:0.74;stroke-opacity:1"
|
||||||
|
id="rect681"
|
||||||
|
width="666"
|
||||||
|
height="222"
|
||||||
|
x="0"
|
||||||
|
y="0" /><text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:128px;line-height:1.25;font-family:'Open Sans';-inkscape-font-specification:'Open Sans';text-align:center;text-anchor:middle;fill:#000000;fill-opacity:0.7;stroke-width:1"
|
||||||
|
x="249.9375"
|
||||||
|
y="134.8125"
|
||||||
|
id="text3425"><tspan
|
||||||
|
id="tspan3423"
|
||||||
|
x="249.9375"
|
||||||
|
y="134.8125"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:128px;font-family:'Open Sans';-inkscape-font-specification:'Open Sans';text-align:center;text-anchor:middle;fill:#000000;fill-opacity:0.7;stroke-width:1"
|
||||||
|
sodipodi:role="line">404</tspan></text><text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:26.6667px;line-height:1.25;font-family:'Open Sans';-inkscape-font-specification:'Open Sans';text-align:center;text-anchor:middle"
|
||||||
|
x="249.04297"
|
||||||
|
y="194.68582"
|
||||||
|
id="text4067"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan4065"
|
||||||
|
x="249.04295"
|
||||||
|
y="194.68582"
|
||||||
|
style="font-size:26.6667px;text-align:center;text-anchor:middle;fill:#000000;fill-opacity:0.7">Return to the web vault?</tspan></text></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 6.5 KiB |
@@ -1 +1 @@
|
|||||||
nightly-2022-01-23
|
1.68.2
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
version = "Two"
|
edition = "2021"
|
||||||
edition = "2018"
|
|
||||||
max_width = 120
|
max_width = 120
|
||||||
newline_style = "Unix"
|
newline_style = "Unix"
|
||||||
use_small_heuristics = "Off"
|
use_small_heuristics = "Off"
|
||||||
struct_lit_single_line = false
|
|
||||||
overflow_delimited_expr = true
|
|
||||||
|
|||||||
600
src/api/admin.rs
@@ -3,16 +3,17 @@ use serde::de::DeserializeOwned;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
|
use rocket::serde::json::Json;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{Cookie, Cookies, SameSite, Status},
|
form::Form,
|
||||||
request::{self, FlashMessage, Form, FromRequest, Outcome, Request},
|
http::{Cookie, CookieJar, MediaType, SameSite, Status},
|
||||||
response::{content::Html, Flash, Redirect},
|
request::{FromRequest, Outcome, Request},
|
||||||
Route,
|
response::{content::RawHtml as Html, Redirect},
|
||||||
|
Catcher, Route,
|
||||||
};
|
};
|
||||||
use rocket_contrib::json::Json;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{ApiResult, EmptyResult, JsonResult, NumberOrString},
|
api::{core::log_event, ApiResult, EmptyResult, JsonResult, Notify, NumberOrString},
|
||||||
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
|
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
|
||||||
config::ConfigBuilder,
|
config::ConfigBuilder,
|
||||||
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
|
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
|
||||||
@@ -30,9 +31,9 @@ pub fn routes() -> Vec<Route> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
routes![
|
routes![
|
||||||
admin_login,
|
|
||||||
get_users_json,
|
get_users_json,
|
||||||
get_user_json,
|
get_user_json,
|
||||||
|
get_user_by_mail_json,
|
||||||
post_admin_login,
|
post_admin_login,
|
||||||
admin_page,
|
admin_page,
|
||||||
invite_user,
|
invite_user,
|
||||||
@@ -52,10 +53,19 @@ pub fn routes() -> Vec<Route> {
|
|||||||
organizations_overview,
|
organizations_overview,
|
||||||
delete_organization,
|
delete_organization,
|
||||||
diagnostics,
|
diagnostics,
|
||||||
get_diagnostics_config
|
get_diagnostics_config,
|
||||||
|
resend_user_invite,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn catchers() -> Vec<Catcher> {
|
||||||
|
if !CONFIG.disable_admin_token() && !CONFIG.is_admin_token_set() {
|
||||||
|
catchers![]
|
||||||
|
} else {
|
||||||
|
catchers![admin_login]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static DB_TYPE: Lazy<&str> = Lazy::new(|| {
|
static DB_TYPE: Lazy<&str> = Lazy::new(|| {
|
||||||
DbConnType::from_url(&CONFIG.database_url())
|
DbConnType::from_url(&CONFIG.database_url())
|
||||||
.map(|t| match t {
|
.map(|t| match t {
|
||||||
@@ -76,30 +86,24 @@ fn admin_disabled() -> &'static str {
|
|||||||
|
|
||||||
const COOKIE_NAME: &str = "VW_ADMIN";
|
const COOKIE_NAME: &str = "VW_ADMIN";
|
||||||
const ADMIN_PATH: &str = "/admin";
|
const ADMIN_PATH: &str = "/admin";
|
||||||
|
const DT_FMT: &str = "%Y-%m-%d %H:%M:%S %Z";
|
||||||
|
|
||||||
const BASE_TEMPLATE: &str = "admin/base";
|
const BASE_TEMPLATE: &str = "admin/base";
|
||||||
|
|
||||||
|
const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000";
|
||||||
|
|
||||||
fn admin_path() -> String {
|
fn admin_path() -> String {
|
||||||
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
|
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Referer(Option<String>);
|
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for Referer {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
|
||||||
Outcome::Success(Referer(request.headers().get_one("Referer").map(str::to_string)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct IpHeader(Option<String>);
|
struct IpHeader(Option<String>);
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for IpHeader {
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for IpHeader {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
fn from_request(req: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
if req.headers().get_one(&CONFIG.ip_header()).is_some() {
|
if req.headers().get_one(&CONFIG.ip_header()).is_some() {
|
||||||
Outcome::Success(IpHeader(Some(CONFIG.ip_header())))
|
Outcome::Success(IpHeader(Some(CONFIG.ip_header())))
|
||||||
} else if req.headers().get_one("X-Client-IP").is_some() {
|
} else if req.headers().get_one("X-Client-IP").is_some() {
|
||||||
@@ -114,35 +118,36 @@ impl<'a, 'r> FromRequest<'a, 'r> for IpHeader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used for `Location` response headers, which must specify an absolute URI
|
fn admin_url() -> String {
|
||||||
/// (see https://tools.ietf.org/html/rfc2616#section-14.30).
|
format!("{}{}", CONFIG.domain_origin(), admin_path())
|
||||||
fn admin_url(referer: Referer) -> String {
|
|
||||||
// If we get a referer use that to make it work when, DOMAIN is not set
|
|
||||||
if let Some(mut referer) = referer.0 {
|
|
||||||
if let Some(start_index) = referer.find(ADMIN_PATH) {
|
|
||||||
referer.truncate(start_index + ADMIN_PATH.len());
|
|
||||||
return referer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.domain_set() {
|
#[derive(Responder)]
|
||||||
// Don't use CONFIG.domain() directly, since the user may want to keep a
|
enum AdminResponse {
|
||||||
// trailing slash there, particularly when running under a subpath.
|
#[response(status = 200)]
|
||||||
format!("{}{}{}", CONFIG.domain_origin(), CONFIG.domain_path(), ADMIN_PATH)
|
Ok(ApiResult<Html<String>>),
|
||||||
} else {
|
#[response(status = 401)]
|
||||||
// Last case, when no referer or domain set, technically invalid but better than nothing
|
Unauthorized(ApiResult<Html<String>>),
|
||||||
ADMIN_PATH.to_string()
|
#[response(status = 429)]
|
||||||
}
|
TooManyRequests(ApiResult<Html<String>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/", rank = 2)]
|
#[catch(401)]
|
||||||
fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> {
|
fn admin_login(request: &Request<'_>) -> ApiResult<Html<String>> {
|
||||||
|
if request.format() == Some(&MediaType::JSON) {
|
||||||
|
err_code!("Authorization failed.", Status::Unauthorized.code);
|
||||||
|
}
|
||||||
|
let redirect = request.segments::<std::path::PathBuf>(0..).unwrap_or_default().display().to_string();
|
||||||
|
render_admin_login(None, Some(redirect))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_admin_login(msg: Option<&str>, redirect: Option<String>) -> ApiResult<Html<String>> {
|
||||||
// If there is an error, show it
|
// If there is an error, show it
|
||||||
let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg()));
|
let msg = msg.map(|msg| format!("Error: {msg}"));
|
||||||
let json = json!({
|
let json = json!({
|
||||||
"page_content": "admin/login",
|
"page_content": "admin/login",
|
||||||
"version": VERSION,
|
|
||||||
"error": msg,
|
"error": msg,
|
||||||
|
"redirect": redirect,
|
||||||
"urlpath": CONFIG.domain_path()
|
"urlpath": CONFIG.domain_path()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,25 +159,25 @@ fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> {
|
|||||||
#[derive(FromForm)]
|
#[derive(FromForm)]
|
||||||
struct LoginForm {
|
struct LoginForm {
|
||||||
token: String,
|
token: String,
|
||||||
|
redirect: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/", data = "<data>")]
|
#[post("/", data = "<data>")]
|
||||||
fn post_admin_login(
|
fn post_admin_login(data: Form<LoginForm>, cookies: &CookieJar<'_>, ip: ClientIp) -> Result<Redirect, AdminResponse> {
|
||||||
data: Form<LoginForm>,
|
|
||||||
mut cookies: Cookies,
|
|
||||||
ip: ClientIp,
|
|
||||||
referer: Referer,
|
|
||||||
) -> Result<Redirect, Flash<Redirect>> {
|
|
||||||
let data = data.into_inner();
|
let data = data.into_inner();
|
||||||
|
let redirect = data.redirect;
|
||||||
|
|
||||||
if crate::ratelimit::check_limit_admin(&ip.ip).is_err() {
|
if crate::ratelimit::check_limit_admin(&ip.ip).is_err() {
|
||||||
return Err(Flash::error(Redirect::to(admin_url(referer)), "Too many requests, try again later."));
|
return Err(AdminResponse::TooManyRequests(render_admin_login(
|
||||||
|
Some("Too many requests, try again later."),
|
||||||
|
redirect,
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the token is invalid, redirect to login page
|
// If the token is invalid, redirect to login page
|
||||||
if !_validate_token(&data.token) {
|
if !_validate_token(&data.token) {
|
||||||
error!("Invalid admin token. IP: {}", ip.ip);
|
error!("Invalid admin token. IP: {}", ip.ip);
|
||||||
Err(Flash::error(Redirect::to(admin_url(referer)), "Invalid admin token, please try again."))
|
Err(AdminResponse::Unauthorized(render_admin_login(Some("Invalid admin token, please try again."), redirect)))
|
||||||
} else {
|
} else {
|
||||||
// If the token received is valid, generate JWT and save it as a cookie
|
// If the token received is valid, generate JWT and save it as a cookie
|
||||||
let claims = generate_admin_claims();
|
let claims = generate_admin_claims();
|
||||||
@@ -180,19 +185,36 @@ fn post_admin_login(
|
|||||||
|
|
||||||
let cookie = Cookie::build(COOKIE_NAME, jwt)
|
let cookie = Cookie::build(COOKIE_NAME, jwt)
|
||||||
.path(admin_path())
|
.path(admin_path())
|
||||||
.max_age(time::Duration::minutes(20))
|
.max_age(rocket::time::Duration::minutes(CONFIG.admin_session_lifetime()))
|
||||||
.same_site(SameSite::Strict)
|
.same_site(SameSite::Strict)
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
cookies.add(cookie);
|
cookies.add(cookie);
|
||||||
Ok(Redirect::to(admin_url(referer)))
|
if let Some(redirect) = redirect {
|
||||||
|
Ok(Redirect::to(format!("{}{}", admin_path(), redirect)))
|
||||||
|
} else {
|
||||||
|
Err(AdminResponse::Ok(render_admin_page()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _validate_token(token: &str) -> bool {
|
fn _validate_token(token: &str) -> bool {
|
||||||
match CONFIG.admin_token().as_ref() {
|
match CONFIG.admin_token().as_ref() {
|
||||||
None => false,
|
None => false,
|
||||||
|
Some(t) if t.starts_with("$argon2") => {
|
||||||
|
use argon2::password_hash::PasswordVerifier;
|
||||||
|
match argon2::password_hash::PasswordHash::new(t) {
|
||||||
|
Ok(h) => {
|
||||||
|
// NOTE: hash params from `ADMIN_TOKEN` are used instead of what is configured in the `Argon2` instance.
|
||||||
|
argon2::Argon2::default().verify_password(token.trim().as_ref(), &h).is_ok()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("The configured Argon2 PHC in `ADMIN_TOKEN` is invalid: {e}");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(t) => crate::crypto::ct_eq(t.trim(), token.trim()),
|
Some(t) => crate::crypto::ct_eq(t.trim(), token.trim()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,34 +222,16 @@ fn _validate_token(token: &str) -> bool {
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct AdminTemplateData {
|
struct AdminTemplateData {
|
||||||
page_content: String,
|
page_content: String,
|
||||||
version: Option<&'static str>,
|
|
||||||
page_data: Option<Value>,
|
page_data: Option<Value>,
|
||||||
config: Value,
|
|
||||||
can_backup: bool,
|
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
urlpath: String,
|
urlpath: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AdminTemplateData {
|
impl AdminTemplateData {
|
||||||
fn new() -> Self {
|
fn new(page_content: &str, page_data: Value) -> Self {
|
||||||
Self {
|
|
||||||
page_content: String::from("admin/settings"),
|
|
||||||
version: VERSION,
|
|
||||||
config: CONFIG.prepare_json(),
|
|
||||||
can_backup: *CAN_BACKUP,
|
|
||||||
logged_in: true,
|
|
||||||
urlpath: CONFIG.domain_path(),
|
|
||||||
page_data: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_data(page_content: &str, page_data: Value) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
page_content: String::from(page_content),
|
page_content: String::from(page_content),
|
||||||
version: VERSION,
|
|
||||||
page_data: Some(page_data),
|
page_data: Some(page_data),
|
||||||
config: CONFIG.prepare_json(),
|
|
||||||
can_backup: *CAN_BACKUP,
|
|
||||||
logged_in: true,
|
logged_in: true,
|
||||||
urlpath: CONFIG.domain_path(),
|
urlpath: CONFIG.domain_path(),
|
||||||
}
|
}
|
||||||
@@ -238,20 +242,28 @@ impl AdminTemplateData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/", rank = 1)]
|
fn render_admin_page() -> ApiResult<Html<String>> {
|
||||||
fn admin_page(_token: AdminToken) -> ApiResult<Html<String>> {
|
let settings_json = json!({
|
||||||
let text = AdminTemplateData::new().render()?;
|
"config": CONFIG.prepare_json(),
|
||||||
|
"can_backup": *CAN_BACKUP,
|
||||||
|
});
|
||||||
|
let text = AdminTemplateData::new("admin/settings", settings_json).render()?;
|
||||||
Ok(Html(text))
|
Ok(Html(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
fn admin_page(_token: AdminToken) -> ApiResult<Html<String>> {
|
||||||
|
render_admin_page()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct InviteData {
|
struct InviteData {
|
||||||
email: String,
|
email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_user_or_404(uuid: &str, conn: &DbConn) -> ApiResult<User> {
|
async fn get_user_or_404(uuid: &str, conn: &mut DbConn) -> ApiResult<User> {
|
||||||
if let Some(user) = User::find_by_uuid(uuid, conn) {
|
if let Some(user) = User::find_by_uuid(uuid, conn).await {
|
||||||
Ok(user)
|
Ok(user)
|
||||||
} else {
|
} else {
|
||||||
err_code!("User doesn't exist", Status::NotFound.code);
|
err_code!("User doesn't exist", Status::NotFound.code);
|
||||||
@@ -259,128 +271,187 @@ fn get_user_or_404(uuid: &str, conn: &DbConn) -> ApiResult<User> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/invite", data = "<data>")]
|
#[post("/invite", data = "<data>")]
|
||||||
fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> JsonResult {
|
async fn invite_user(data: Json<InviteData>, _token: AdminToken, mut conn: DbConn) -> JsonResult {
|
||||||
let data: InviteData = data.into_inner();
|
let data: InviteData = data.into_inner();
|
||||||
let email = data.email.clone();
|
let email = data.email.clone();
|
||||||
if User::find_by_mail(&data.email, &conn).is_some() {
|
if User::find_by_mail(&data.email, &mut conn).await.is_some() {
|
||||||
err_code!("User already exists", Status::Conflict.code)
|
err_code!("User already exists", Status::Conflict.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut user = User::new(email);
|
let mut user = User::new(email);
|
||||||
|
|
||||||
// TODO: After try_blocks is stabilized, this can be made more readable
|
async fn _generate_invite(user: &User, conn: &mut DbConn) -> EmptyResult {
|
||||||
// See: https://github.com/rust-lang/rust/issues/31436
|
|
||||||
(|| {
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_invite(&user.email, &user.uuid, None, None, &CONFIG.invitation_org_name(), None)?;
|
mail::send_invite(&user.email, &user.uuid, None, None, &CONFIG.invitation_org_name(), None).await
|
||||||
} else {
|
} else {
|
||||||
let invitation = Invitation::new(user.email.clone());
|
let invitation = Invitation::new(&user.email);
|
||||||
invitation.save(&conn)?;
|
invitation.save(conn).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user.save(&conn)
|
_generate_invite(&user, &mut conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;
|
||||||
})()
|
user.save(&mut conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;
|
||||||
.map_err(|e| e.with_code(Status::InternalServerError.code))?;
|
|
||||||
|
|
||||||
Ok(Json(user.to_json(&conn)))
|
Ok(Json(user.to_json(&mut conn).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/test/smtp", data = "<data>")]
|
#[post("/test/smtp", data = "<data>")]
|
||||||
fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
|
async fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
|
||||||
let data: InviteData = data.into_inner();
|
let data: InviteData = data.into_inner();
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_test(&data.email)
|
mail::send_test(&data.email).await
|
||||||
} else {
|
} else {
|
||||||
err!("Mail is not enabled")
|
err!("Mail is not enabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/logout")]
|
#[get("/logout")]
|
||||||
fn logout(mut cookies: Cookies, referer: Referer) -> Redirect {
|
fn logout(cookies: &CookieJar<'_>) -> Redirect {
|
||||||
cookies.remove(Cookie::named(COOKIE_NAME));
|
cookies.remove(Cookie::build(COOKIE_NAME, "").path(admin_path()).finish());
|
||||||
Redirect::to(admin_url(referer))
|
Redirect::to(admin_path())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users")]
|
#[get("/users")]
|
||||||
fn get_users_json(_token: AdminToken, conn: DbConn) -> Json<Value> {
|
async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json<Value> {
|
||||||
let users = User::get_all(&conn);
|
let users = User::get_all(&mut conn).await;
|
||||||
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
|
let mut users_json = Vec::with_capacity(users.len());
|
||||||
|
for u in users {
|
||||||
|
let mut usr = u.to_json(&mut conn).await;
|
||||||
|
usr["UserEnabled"] = json!(u.enabled);
|
||||||
|
usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
|
||||||
|
users_json.push(usr);
|
||||||
|
}
|
||||||
|
|
||||||
Json(Value::Array(users_json))
|
Json(Value::Array(users_json))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users/overview")]
|
#[get("/users/overview")]
|
||||||
fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
let users = User::get_all(&conn);
|
let users = User::get_all(&mut conn).await;
|
||||||
let dt_fmt = "%Y-%m-%d %H:%M:%S %Z";
|
let mut users_json = Vec::with_capacity(users.len());
|
||||||
let users_json: Vec<Value> = users
|
for u in users {
|
||||||
.iter()
|
let mut usr = u.to_json(&mut conn).await;
|
||||||
.map(|u| {
|
usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &mut conn).await);
|
||||||
let mut usr = u.to_json(&conn);
|
usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, &mut conn).await);
|
||||||
usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &conn));
|
usr["attachment_size"] = json!(get_display_size(Attachment::size_by_user(&u.uuid, &mut conn).await as i32));
|
||||||
usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, &conn));
|
|
||||||
usr["attachment_size"] = json!(get_display_size(Attachment::size_by_user(&u.uuid, &conn) as i32));
|
|
||||||
usr["user_enabled"] = json!(u.enabled);
|
usr["user_enabled"] = json!(u.enabled);
|
||||||
usr["created_at"] = json!(format_naive_datetime_local(&u.created_at, dt_fmt));
|
usr["created_at"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
|
||||||
usr["last_active"] = match u.last_active(&conn) {
|
usr["last_active"] = match u.last_active(&mut conn).await {
|
||||||
Some(dt) => json!(format_naive_datetime_local(&dt, dt_fmt)),
|
Some(dt) => json!(format_naive_datetime_local(&dt, DT_FMT)),
|
||||||
None => json!("Never"),
|
None => json!("Never"),
|
||||||
};
|
};
|
||||||
usr
|
users_json.push(usr);
|
||||||
})
|
}
|
||||||
.collect();
|
|
||||||
|
|
||||||
let text = AdminTemplateData::with_data("admin/users", json!(users_json)).render()?;
|
let text = AdminTemplateData::new("admin/users", json!(users_json)).render()?;
|
||||||
Ok(Html(text))
|
Ok(Html(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users/<uuid>")]
|
#[get("/users/by-mail/<mail>")]
|
||||||
fn get_user_json(uuid: String, _token: AdminToken, conn: DbConn) -> JsonResult {
|
async fn get_user_by_mail_json(mail: String, _token: AdminToken, mut conn: DbConn) -> JsonResult {
|
||||||
let user = get_user_or_404(&uuid, &conn)?;
|
if let Some(u) = User::find_by_mail(&mail, &mut conn).await {
|
||||||
|
let mut usr = u.to_json(&mut conn).await;
|
||||||
|
usr["UserEnabled"] = json!(u.enabled);
|
||||||
|
usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
|
||||||
|
Ok(Json(usr))
|
||||||
|
} else {
|
||||||
|
err_code!("User doesn't exist", Status::NotFound.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Json(user.to_json(&conn)))
|
#[get("/users/<uuid>")]
|
||||||
|
async fn get_user_json(uuid: String, _token: AdminToken, mut conn: DbConn) -> JsonResult {
|
||||||
|
let u = get_user_or_404(&uuid, &mut conn).await?;
|
||||||
|
let mut usr = u.to_json(&mut conn).await;
|
||||||
|
usr["UserEnabled"] = json!(u.enabled);
|
||||||
|
usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
|
||||||
|
Ok(Json(usr))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/<uuid>/delete")]
|
#[post("/users/<uuid>/delete")]
|
||||||
fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
async fn delete_user(uuid: String, token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||||
let user = get_user_or_404(&uuid, &conn)?;
|
let user = get_user_or_404(&uuid, &mut conn).await?;
|
||||||
user.delete(&conn)
|
|
||||||
|
// Get the user_org records before deleting the actual user
|
||||||
|
let user_orgs = UserOrganization::find_any_state_by_user(&uuid, &mut conn).await;
|
||||||
|
let res = user.delete(&mut conn).await;
|
||||||
|
|
||||||
|
for user_org in user_orgs {
|
||||||
|
log_event(
|
||||||
|
EventType::OrganizationUserRemoved as i32,
|
||||||
|
&user_org.uuid,
|
||||||
|
user_org.org_uuid,
|
||||||
|
String::from(ACTING_ADMIN_USER),
|
||||||
|
14, // Use UnknownBrowser type
|
||||||
|
&token.ip.ip,
|
||||||
|
&mut conn,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/<uuid>/deauth")]
|
#[post("/users/<uuid>/deauth")]
|
||||||
fn deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
async fn deauth_user(uuid: String, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||||
let mut user = get_user_or_404(&uuid, &conn)?;
|
let mut user = get_user_or_404(&uuid, &mut conn).await?;
|
||||||
Device::delete_all_by_user(&user.uuid, &conn)?;
|
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
|
||||||
user.reset_security_stamp();
|
user.reset_security_stamp();
|
||||||
|
|
||||||
user.save(&conn)
|
let save_result = user.save(&mut conn).await;
|
||||||
|
|
||||||
|
nt.send_logout(&user, None).await;
|
||||||
|
|
||||||
|
save_result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/<uuid>/disable")]
|
#[post("/users/<uuid>/disable")]
|
||||||
fn disable_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
async fn disable_user(uuid: String, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||||
let mut user = get_user_or_404(&uuid, &conn)?;
|
let mut user = get_user_or_404(&uuid, &mut conn).await?;
|
||||||
Device::delete_all_by_user(&user.uuid, &conn)?;
|
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
|
||||||
user.reset_security_stamp();
|
user.reset_security_stamp();
|
||||||
user.enabled = false;
|
user.enabled = false;
|
||||||
|
|
||||||
user.save(&conn)
|
let save_result = user.save(&mut conn).await;
|
||||||
|
|
||||||
|
nt.send_logout(&user, None).await;
|
||||||
|
|
||||||
|
save_result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/<uuid>/enable")]
|
#[post("/users/<uuid>/enable")]
|
||||||
fn enable_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
async fn enable_user(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||||
let mut user = get_user_or_404(&uuid, &conn)?;
|
let mut user = get_user_or_404(&uuid, &mut conn).await?;
|
||||||
user.enabled = true;
|
user.enabled = true;
|
||||||
|
|
||||||
user.save(&conn)
|
user.save(&mut conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/<uuid>/remove-2fa")]
|
#[post("/users/<uuid>/remove-2fa")]
|
||||||
fn remove_2fa(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
async fn remove_2fa(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||||
let mut user = get_user_or_404(&uuid, &conn)?;
|
let mut user = get_user_or_404(&uuid, &mut conn).await?;
|
||||||
TwoFactor::delete_all_by_user(&user.uuid, &conn)?;
|
TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?;
|
||||||
user.totp_recover = None;
|
user.totp_recover = None;
|
||||||
user.save(&conn)
|
user.save(&mut conn).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/users/<uuid>/invite/resend")]
|
||||||
|
async fn resend_user_invite(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||||
|
if let Some(user) = User::find_by_uuid(&uuid, &mut conn).await {
|
||||||
|
//TODO: replace this with user.status check when it will be available (PR#3397)
|
||||||
|
if !user.password_hash.is_empty() {
|
||||||
|
err_code!("User already accepted invitation", Status::BadRequest.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
if CONFIG.mail_enabled() {
|
||||||
|
mail::send_invite(&user.email, &user.uuid, None, None, &CONFIG.invitation_org_name(), None).await
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err_code!("User doesn't exist", Status::NotFound.code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@@ -391,10 +462,11 @@ struct UserOrgTypeData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/org_type", data = "<data>")]
|
#[post("/users/org_type", data = "<data>")]
|
||||||
fn update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
async fn update_user_org_type(data: Json<UserOrgTypeData>, token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||||
let data: UserOrgTypeData = data.into_inner();
|
let data: UserOrgTypeData = data.into_inner();
|
||||||
|
|
||||||
let mut user_to_edit = match UserOrganization::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &conn) {
|
let mut user_to_edit =
|
||||||
|
match UserOrganization::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("The specified user isn't member of the organization"),
|
None => err!("The specified user isn't member of the organization"),
|
||||||
};
|
};
|
||||||
@@ -405,46 +477,70 @@ fn update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, conn: D
|
|||||||
};
|
};
|
||||||
|
|
||||||
if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner {
|
if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner {
|
||||||
// Removing owner permmission, check that there are at least another owner
|
// Removing owner permission, check that there is at least one other confirmed owner
|
||||||
let num_owners = UserOrganization::find_by_org_and_type(&data.org_uuid, UserOrgType::Owner as i32, &conn).len();
|
if UserOrganization::count_confirmed_by_org_and_type(&data.org_uuid, UserOrgType::Owner, &mut conn).await <= 1 {
|
||||||
|
|
||||||
if num_owners <= 1 {
|
|
||||||
err!("Can't change the type of the last owner")
|
err!("Can't change the type of the last owner")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user_to_edit.atype = new_type as i32;
|
// This check is also done at api::organizations::{accept_invite(), _confirm_invite, _activate_user(), edit_user()}, update_user_org_type
|
||||||
user_to_edit.save(&conn)
|
// It returns different error messages per function.
|
||||||
|
if new_type < UserOrgType::Admin {
|
||||||
|
match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, &user_to_edit.org_uuid, true, &mut conn).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(OrgPolicyErr::TwoFactorMissing) => {
|
||||||
|
err!("You cannot modify this user to this type because it has no two-step login method activated");
|
||||||
|
}
|
||||||
|
Err(OrgPolicyErr::SingleOrgEnforced) => {
|
||||||
|
err!("You cannot modify this user to this type because it is a member of an organization which forbids it");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log_event(
|
||||||
|
EventType::OrganizationUserUpdated as i32,
|
||||||
|
&user_to_edit.uuid,
|
||||||
|
data.org_uuid,
|
||||||
|
String::from(ACTING_ADMIN_USER),
|
||||||
|
14, // Use UnknownBrowser type
|
||||||
|
&token.ip.ip,
|
||||||
|
&mut conn,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
user_to_edit.atype = new_type;
|
||||||
|
user_to_edit.save(&mut conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/update_revision")]
|
#[post("/users/update_revision")]
|
||||||
fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult {
|
async fn update_revision_users(_token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||||
User::update_all_revisions(&conn)
|
User::update_all_revisions(&mut conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/overview")]
|
#[get("/organizations/overview")]
|
||||||
fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
let organizations = Organization::get_all(&conn);
|
let organizations = Organization::get_all(&mut conn).await;
|
||||||
let organizations_json: Vec<Value> = organizations
|
let mut organizations_json = Vec::with_capacity(organizations.len());
|
||||||
.iter()
|
for o in organizations {
|
||||||
.map(|o| {
|
|
||||||
let mut org = o.to_json();
|
let mut org = o.to_json();
|
||||||
org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &conn));
|
org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &mut conn).await);
|
||||||
org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &conn));
|
org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &mut conn).await);
|
||||||
org["attachment_count"] = json!(Attachment::count_by_org(&o.uuid, &conn));
|
org["collection_count"] = json!(Collection::count_by_org(&o.uuid, &mut conn).await);
|
||||||
org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &conn) as i32));
|
org["group_count"] = json!(Group::count_by_org(&o.uuid, &mut conn).await);
|
||||||
org
|
org["event_count"] = json!(Event::count_by_org(&o.uuid, &mut conn).await);
|
||||||
})
|
org["attachment_count"] = json!(Attachment::count_by_org(&o.uuid, &mut conn).await);
|
||||||
.collect();
|
org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &mut conn).await as i32));
|
||||||
|
organizations_json.push(org);
|
||||||
|
}
|
||||||
|
|
||||||
let text = AdminTemplateData::with_data("admin/organizations", json!(organizations_json)).render()?;
|
let text = AdminTemplateData::new("admin/organizations", json!(organizations_json)).render()?;
|
||||||
Ok(Html(text))
|
Ok(Html(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/organizations/<uuid>/delete")]
|
#[post("/organizations/<uuid>/delete")]
|
||||||
fn delete_organization(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
async fn delete_organization(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||||
let org = Organization::find_by_uuid(&uuid, &conn).map_res("Organization doesn't exist")?;
|
let org = Organization::find_by_uuid(&uuid, &mut conn).await.map_res("Organization doesn't exist")?;
|
||||||
org.delete(&conn)
|
org.delete(&mut conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -462,62 +558,46 @@ struct GitCommit {
|
|||||||
sha: String,
|
sha: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
#[derive(Deserialize)]
|
||||||
let github_api = get_reqwest_client();
|
struct TimeApi {
|
||||||
|
year: u16,
|
||||||
Ok(github_api.get(url).send()?.error_for_status()?.json::<T>()?)
|
month: u8,
|
||||||
|
day: u8,
|
||||||
|
hour: u8,
|
||||||
|
minute: u8,
|
||||||
|
seconds: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_http_access() -> bool {
|
async fn get_json_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
||||||
|
let json_api = get_reqwest_client();
|
||||||
|
|
||||||
|
Ok(json_api.get(url).send().await?.error_for_status()?.json::<T>().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn has_http_access() -> bool {
|
||||||
let http_access = get_reqwest_client();
|
let http_access = get_reqwest_client();
|
||||||
|
|
||||||
match http_access.head("https://github.com/dani-garcia/vaultwarden").send() {
|
match http_access.head("https://github.com/dani-garcia/vaultwarden").send().await {
|
||||||
Ok(r) => r.status().is_success(),
|
Ok(r) => r.status().is_success(),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/diagnostics")]
|
use cached::proc_macro::cached;
|
||||||
fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> {
|
/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already.
|
||||||
use crate::util::read_file_string;
|
/// It will cache this function for 300 seconds (5 minutes) which should prevent the exhaustion of the rate limit.
|
||||||
use chrono::prelude::*;
|
#[cached(time = 300, sync_writes = true)]
|
||||||
use std::net::ToSocketAddrs;
|
async fn get_release_info(has_http_access: bool, running_within_docker: bool) -> (String, String, String) {
|
||||||
|
|
||||||
// Get current running versions
|
|
||||||
let web_vault_version: WebVaultVersion =
|
|
||||||
match read_file_string(&format!("{}/{}", CONFIG.web_vault_folder(), "vw-version.json")) {
|
|
||||||
Ok(s) => serde_json::from_str(&s)?,
|
|
||||||
_ => match read_file_string(&format!("{}/{}", CONFIG.web_vault_folder(), "version.json")) {
|
|
||||||
Ok(s) => serde_json::from_str(&s)?,
|
|
||||||
_ => WebVaultVersion {
|
|
||||||
version: String::from("Version file missing"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute some environment checks
|
|
||||||
let running_within_docker = is_running_in_docker();
|
|
||||||
let has_http_access = has_http_access();
|
|
||||||
let uses_proxy = env::var_os("HTTP_PROXY").is_some()
|
|
||||||
|| env::var_os("http_proxy").is_some()
|
|
||||||
|| env::var_os("HTTPS_PROXY").is_some()
|
|
||||||
|| env::var_os("https_proxy").is_some();
|
|
||||||
|
|
||||||
// Check if we are able to resolve DNS entries
|
|
||||||
let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) {
|
|
||||||
Ok(Some(a)) => a.ip().to_string(),
|
|
||||||
_ => "Could not resolve domain name.".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
|
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
|
||||||
// TODO: Maybe we need to cache this using a LazyStatic or something. Github only allows 60 requests per hour, and we use 3 here already.
|
if has_http_access {
|
||||||
let (latest_release, latest_commit, latest_web_build) = if has_http_access {
|
|
||||||
(
|
(
|
||||||
match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest") {
|
match get_json_api::<GitRelease>("https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest")
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(r) => r.tag_name,
|
Ok(r) => r.tag_name,
|
||||||
_ => "-".to_string(),
|
_ => "-".to_string(),
|
||||||
},
|
},
|
||||||
match get_github_api::<GitCommit>("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main") {
|
match get_json_api::<GitCommit>("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main").await {
|
||||||
Ok(mut c) => {
|
Ok(mut c) => {
|
||||||
c.sha.truncate(8);
|
c.sha.truncate(8);
|
||||||
c.sha
|
c.sha
|
||||||
@@ -529,9 +609,11 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu
|
|||||||
if running_within_docker {
|
if running_within_docker {
|
||||||
"-".to_string()
|
"-".to_string()
|
||||||
} else {
|
} else {
|
||||||
match get_github_api::<GitRelease>(
|
match get_json_api::<GitRelease>(
|
||||||
"https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest",
|
"https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest",
|
||||||
) {
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(r) => r.tag_name.trim_start_matches('v').to_string(),
|
Ok(r) => r.tag_name.trim_start_matches('v').to_string(),
|
||||||
_ => "-".to_string(),
|
_ => "-".to_string(),
|
||||||
}
|
}
|
||||||
@@ -539,8 +621,61 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
("-".to_string(), "-".to_string(), "-".to_string())
|
("-".to_string(), "-".to_string(), "-".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_ntp_time(has_http_access: bool) -> String {
|
||||||
|
if has_http_access {
|
||||||
|
if let Ok(ntp_time) = get_json_api::<TimeApi>("https://www.timeapi.io/api/Time/current/zone?timeZone=UTC").await
|
||||||
|
{
|
||||||
|
return format!(
|
||||||
|
"{year}-{month:02}-{day:02} {hour:02}:{minute:02}:{seconds:02} UTC",
|
||||||
|
year = ntp_time.year,
|
||||||
|
month = ntp_time.month,
|
||||||
|
day = ntp_time.day,
|
||||||
|
hour = ntp_time.hour,
|
||||||
|
minute = ntp_time.minute,
|
||||||
|
seconds = ntp_time.seconds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String::from("Unable to fetch NTP time.")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/diagnostics")]
|
||||||
|
async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
|
||||||
|
// Get current running versions
|
||||||
|
let web_vault_version: WebVaultVersion =
|
||||||
|
match std::fs::read_to_string(format!("{}/{}", CONFIG.web_vault_folder(), "vw-version.json")) {
|
||||||
|
Ok(s) => serde_json::from_str(&s)?,
|
||||||
|
_ => match std::fs::read_to_string(format!("{}/{}", CONFIG.web_vault_folder(), "version.json")) {
|
||||||
|
Ok(s) => serde_json::from_str(&s)?,
|
||||||
|
_ => WebVaultVersion {
|
||||||
|
version: String::from("Version file missing"),
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Execute some environment checks
|
||||||
|
let running_within_docker = is_running_in_docker();
|
||||||
|
let has_http_access = has_http_access().await;
|
||||||
|
let uses_proxy = env::var_os("HTTP_PROXY").is_some()
|
||||||
|
|| env::var_os("http_proxy").is_some()
|
||||||
|
|| env::var_os("HTTPS_PROXY").is_some()
|
||||||
|
|| env::var_os("https_proxy").is_some();
|
||||||
|
|
||||||
|
// Check if we are able to resolve DNS entries
|
||||||
|
let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) {
|
||||||
|
Ok(Some(a)) => a.ip().to_string(),
|
||||||
|
_ => "Unable to resolve domain name.".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (latest_release, latest_commit, latest_web_build) =
|
||||||
|
get_release_info(has_http_access, running_within_docker).await;
|
||||||
|
|
||||||
let ip_header_name = match &ip_header.0 {
|
let ip_header_name = match &ip_header.0 {
|
||||||
Some(h) => h,
|
Some(h) => h,
|
||||||
_ => "",
|
_ => "",
|
||||||
@@ -548,13 +683,14 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu
|
|||||||
|
|
||||||
let diagnostics_json = json!({
|
let diagnostics_json = json!({
|
||||||
"dns_resolved": dns_resolved,
|
"dns_resolved": dns_resolved,
|
||||||
|
"current_release": VERSION,
|
||||||
"latest_release": latest_release,
|
"latest_release": latest_release,
|
||||||
"latest_commit": latest_commit,
|
"latest_commit": latest_commit,
|
||||||
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
||||||
"web_vault_version": web_vault_version.version,
|
"web_vault_version": web_vault_version.version.trim_start_matches('v'),
|
||||||
"latest_web_build": latest_web_build,
|
"latest_web_build": latest_web_build,
|
||||||
"running_within_docker": running_within_docker,
|
"running_within_docker": running_within_docker,
|
||||||
"docker_base_image": docker_base_image(),
|
"docker_base_image": if running_within_docker { docker_base_image() } else { "Not applicable" },
|
||||||
"has_http_access": has_http_access,
|
"has_http_access": has_http_access,
|
||||||
"ip_header_exists": &ip_header.0.is_some(),
|
"ip_header_exists": &ip_header.0.is_some(),
|
||||||
"ip_header_match": ip_header_name == CONFIG.ip_header(),
|
"ip_header_match": ip_header_name == CONFIG.ip_header(),
|
||||||
@@ -562,14 +698,17 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu
|
|||||||
"ip_header_config": &CONFIG.ip_header(),
|
"ip_header_config": &CONFIG.ip_header(),
|
||||||
"uses_proxy": uses_proxy,
|
"uses_proxy": uses_proxy,
|
||||||
"db_type": *DB_TYPE,
|
"db_type": *DB_TYPE,
|
||||||
"db_version": get_sql_server_version(&conn),
|
"db_version": get_sql_server_version(&mut conn).await,
|
||||||
"admin_url": format!("{}/diagnostics", admin_url(Referer(None))),
|
"admin_url": format!("{}/diagnostics", admin_url()),
|
||||||
"overrides": &CONFIG.get_overrides().join(", "),
|
"overrides": &CONFIG.get_overrides().join(", "),
|
||||||
|
"host_arch": std::env::consts::ARCH,
|
||||||
|
"host_os": std::env::consts::OS,
|
||||||
"server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(),
|
"server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(),
|
||||||
"server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference
|
"server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the server date/time check as late as possible to minimize the time difference
|
||||||
|
"ntp_time": get_ntp_time(has_http_access).await, // Run the ntp check as late as possible to minimize the time difference
|
||||||
});
|
});
|
||||||
|
|
||||||
let text = AdminTemplateData::with_data("admin/diagnostics", diagnostics_json).render()?;
|
let text = AdminTemplateData::new("admin/diagnostics", diagnostics_json).render()?;
|
||||||
Ok(Html(text))
|
Ok(Html(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,43 +730,50 @@ fn delete_config(_token: AdminToken) -> EmptyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/config/backup_db")]
|
#[post("/config/backup_db")]
|
||||||
fn backup_db(_token: AdminToken, conn: DbConn) -> EmptyResult {
|
async fn backup_db(_token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||||
if *CAN_BACKUP {
|
if *CAN_BACKUP {
|
||||||
backup_database(&conn)
|
backup_database(&mut conn).await
|
||||||
} else {
|
} else {
|
||||||
err!("Can't back up current DB (Only SQLite supports this feature)");
|
err!("Can't back up current DB (Only SQLite supports this feature)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AdminToken {}
|
pub struct AdminToken {
|
||||||
|
ip: ClientIp,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for AdminToken {
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for AdminToken {
|
||||||
type Error = &'static str;
|
type Error = &'static str;
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
|
let ip = match ClientIp::from_request(request).await {
|
||||||
|
Outcome::Success(ip) => ip,
|
||||||
|
_ => err_handler!("Error getting Client IP"),
|
||||||
|
};
|
||||||
|
|
||||||
if CONFIG.disable_admin_token() {
|
if CONFIG.disable_admin_token() {
|
||||||
Outcome::Success(AdminToken {})
|
Outcome::Success(Self {
|
||||||
|
ip,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
let mut cookies = request.cookies();
|
let cookies = request.cookies();
|
||||||
|
|
||||||
let access_token = match cookies.get(COOKIE_NAME) {
|
let access_token = match cookies.get(COOKIE_NAME) {
|
||||||
Some(cookie) => cookie.value(),
|
Some(cookie) => cookie.value(),
|
||||||
None => return Outcome::Forward(()), // If there is no cookie, redirect to login
|
None => return Outcome::Failure((Status::Unauthorized, "Unauthorized")),
|
||||||
};
|
|
||||||
|
|
||||||
let ip = match request.guard::<ClientIp>() {
|
|
||||||
Outcome::Success(ip) => ip.ip,
|
|
||||||
_ => err_handler!("Error getting Client IP"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if decode_admin(access_token).is_err() {
|
if decode_admin(access_token).is_err() {
|
||||||
// Remove admin cookie
|
// Remove admin cookie
|
||||||
cookies.remove(Cookie::named(COOKIE_NAME));
|
cookies.remove(Cookie::build(COOKIE_NAME, "").path(admin_path()).finish());
|
||||||
error!("Invalid or expired admin JWT. IP: {}.", ip);
|
error!("Invalid or expired admin JWT. IP: {}.", &ip.ip);
|
||||||
return Outcome::Forward(());
|
return Outcome::Failure((Status::Unauthorized, "Session expired"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Outcome::Success(AdminToken {})
|
Outcome::Success(Self {
|
||||||
|
ip,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use rocket_contrib::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType},
|
api::{
|
||||||
|
core::log_user_event, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType,
|
||||||
|
},
|
||||||
auth::{decode_delete, decode_invite, decode_verify_email, Headers},
|
auth::{decode_delete, decode_invite, decode_verify_email, Headers},
|
||||||
crypto,
|
crypto,
|
||||||
db::{models::*, DbConn},
|
db::{models::*, DbConn},
|
||||||
mail, CONFIG,
|
mail, CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use rocket::{
|
||||||
|
http::Status,
|
||||||
|
request::{FromRequest, Outcome, Request},
|
||||||
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<rocket::Route> {
|
pub fn routes() -> Vec<rocket::Route> {
|
||||||
routes![
|
routes![
|
||||||
register,
|
register,
|
||||||
@@ -36,15 +43,20 @@ pub fn routes() -> Vec<rocket::Route> {
|
|||||||
verify_password,
|
verify_password,
|
||||||
api_key,
|
api_key,
|
||||||
rotate_api_key,
|
rotate_api_key,
|
||||||
|
get_known_device,
|
||||||
|
get_known_device_from_path,
|
||||||
|
put_avatar,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct RegisterData {
|
pub struct RegisterData {
|
||||||
Email: String,
|
Email: String,
|
||||||
Kdf: Option<i32>,
|
Kdf: Option<i32>,
|
||||||
KdfIterations: Option<i32>,
|
KdfIterations: Option<i32>,
|
||||||
|
KdfMemory: Option<i32>,
|
||||||
|
KdfParallelism: Option<i32>,
|
||||||
Key: String,
|
Key: String,
|
||||||
Keys: Option<KeysData>,
|
Keys: Option<KeysData>,
|
||||||
MasterPasswordHash: String,
|
MasterPasswordHash: String,
|
||||||
@@ -62,38 +74,74 @@ struct KeysData {
|
|||||||
PublicKey: String,
|
PublicKey: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trims whitespace from password hints, and converts blank password hints to `None`.
|
||||||
|
fn clean_password_hint(password_hint: &Option<String>) -> Option<String> {
|
||||||
|
match password_hint {
|
||||||
|
None => None,
|
||||||
|
Some(h) => match h.trim() {
|
||||||
|
"" => None,
|
||||||
|
ht => Some(ht.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult {
|
||||||
|
if password_hint.is_some() && !CONFIG.password_hints_allowed() {
|
||||||
|
err!("Password hints have been disabled by the administrator. Remove the hint and try again.");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/accounts/register", data = "<data>")]
|
#[post("/accounts/register", data = "<data>")]
|
||||||
fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
async fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> JsonResult {
|
||||||
|
_register(data, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> JsonResult {
|
||||||
let data: RegisterData = data.into_inner().data;
|
let data: RegisterData = data.into_inner().data;
|
||||||
let email = data.Email.to_lowercase();
|
let email = data.Email.to_lowercase();
|
||||||
|
|
||||||
let mut user = match User::find_by_mail(&email, &conn) {
|
// Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden)
|
||||||
Some(user) => {
|
// This also prevents issues with very long usernames causing to large JWT's. See #2419
|
||||||
if !user.password_hash.is_empty() {
|
if let Some(ref name) = data.Name {
|
||||||
if CONFIG.is_signup_allowed(&email) {
|
if name.len() > 50 {
|
||||||
err!("User already exists")
|
err!("The field Name must be a string with a maximum length of 50.");
|
||||||
} else {
|
|
||||||
err!("Registration not allowed or user already exists")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check against the password hint setting here so if it fails, the user
|
||||||
|
// can retry without losing their invitation below.
|
||||||
|
let password_hint = clean_password_hint(&data.MasterPasswordHint);
|
||||||
|
enforce_password_hint_setting(&password_hint)?;
|
||||||
|
|
||||||
|
let mut verified_by_invite = false;
|
||||||
|
|
||||||
|
let mut user = match User::find_by_mail(&email, &mut conn).await {
|
||||||
|
Some(mut user) => {
|
||||||
|
if !user.password_hash.is_empty() {
|
||||||
|
err!("Registration not allowed or user already exists")
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(token) = data.Token {
|
if let Some(token) = data.Token {
|
||||||
let claims = decode_invite(&token)?;
|
let claims = decode_invite(&token)?;
|
||||||
if claims.email == email {
|
if claims.email == email {
|
||||||
|
// Verify the email address when signing up via a valid invite token
|
||||||
|
verified_by_invite = true;
|
||||||
|
user.verified_at = Some(Utc::now().naive_utc());
|
||||||
user
|
user
|
||||||
} else {
|
} else {
|
||||||
err!("Registration email does not match invite email")
|
err!("Registration email does not match invite email")
|
||||||
}
|
}
|
||||||
} else if Invitation::take(&email, &conn) {
|
} else if Invitation::take(&email, &mut conn).await {
|
||||||
for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &conn).iter_mut() {
|
for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &mut conn).await.iter_mut() {
|
||||||
user_org.status = UserOrgStatus::Accepted as i32;
|
user_org.status = UserOrgStatus::Accepted as i32;
|
||||||
user_org.save(&conn)?;
|
user_org.save(&mut conn).await?;
|
||||||
}
|
}
|
||||||
user
|
user
|
||||||
} else if EmergencyAccess::find_invited_by_grantee_email(&email, &conn).is_some() {
|
} else if CONFIG.is_signup_allowed(&email)
|
||||||
|
|| EmergencyAccess::find_invited_by_grantee_email(&email, &mut conn).await.is_some()
|
||||||
|
{
|
||||||
user
|
user
|
||||||
} else if CONFIG.is_signup_allowed(&email) {
|
|
||||||
err!("Account with this email already exists")
|
|
||||||
} else {
|
} else {
|
||||||
err!("Registration not allowed or user already exists")
|
err!("Registration not allowed or user already exists")
|
||||||
}
|
}
|
||||||
@@ -102,7 +150,7 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
|||||||
// Order is important here; the invitation check must come first
|
// Order is important here; the invitation check must come first
|
||||||
// because the vaultwarden admin can invite anyone, regardless
|
// because the vaultwarden admin can invite anyone, regardless
|
||||||
// of other signup restrictions.
|
// of other signup restrictions.
|
||||||
if Invitation::take(&email, &conn) || CONFIG.is_signup_allowed(&email) {
|
if Invitation::take(&email, &mut conn).await || CONFIG.is_signup_allowed(&email) {
|
||||||
User::new(email.clone())
|
User::new(email.clone())
|
||||||
} else {
|
} else {
|
||||||
err!("Registration not allowed or user already exists")
|
err!("Registration not allowed or user already exists")
|
||||||
@@ -111,85 +159,115 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Make sure we don't leave a lingering invitation.
|
// Make sure we don't leave a lingering invitation.
|
||||||
Invitation::take(&email, &conn);
|
Invitation::take(&email, &mut conn).await;
|
||||||
|
|
||||||
if let Some(client_kdf_iter) = data.KdfIterations {
|
|
||||||
user.client_kdf_iter = client_kdf_iter;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(client_kdf_type) = data.Kdf {
|
if let Some(client_kdf_type) = data.Kdf {
|
||||||
user.client_kdf_type = client_kdf_type;
|
user.client_kdf_type = client_kdf_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
user.set_password(&data.MasterPasswordHash, None);
|
if let Some(client_kdf_iter) = data.KdfIterations {
|
||||||
user.akey = data.Key;
|
user.client_kdf_iter = client_kdf_iter;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.client_kdf_memory = data.KdfMemory;
|
||||||
|
user.client_kdf_parallelism = data.KdfParallelism;
|
||||||
|
|
||||||
|
user.set_password(&data.MasterPasswordHash, Some(data.Key), true, None);
|
||||||
|
user.password_hint = password_hint;
|
||||||
|
|
||||||
// Add extra fields if present
|
// Add extra fields if present
|
||||||
if let Some(name) = data.Name {
|
if let Some(name) = data.Name {
|
||||||
user.name = name;
|
user.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(hint) = data.MasterPasswordHint {
|
|
||||||
user.password_hint = Some(hint);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(keys) = data.Keys {
|
if let Some(keys) = data.Keys {
|
||||||
user.private_key = Some(keys.EncryptedPrivateKey);
|
user.private_key = Some(keys.EncryptedPrivateKey);
|
||||||
user.public_key = Some(keys.PublicKey);
|
user.public_key = Some(keys.PublicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
if CONFIG.signups_verify() {
|
if CONFIG.signups_verify() && !verified_by_invite {
|
||||||
if let Err(e) = mail::send_welcome_must_verify(&user.email, &user.uuid) {
|
if let Err(e) = mail::send_welcome_must_verify(&user.email, &user.uuid).await {
|
||||||
error!("Error sending welcome email: {:#?}", e);
|
error!("Error sending welcome email: {:#?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.last_verifying_at = Some(user.created_at);
|
user.last_verifying_at = Some(user.created_at);
|
||||||
} else if let Err(e) = mail::send_welcome(&user.email) {
|
} else if let Err(e) = mail::send_welcome(&user.email).await {
|
||||||
error!("Error sending welcome email: {:#?}", e);
|
error!("Error sending welcome email: {:#?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user.save(&conn)
|
user.save(&mut conn).await?;
|
||||||
|
Ok(Json(json!({
|
||||||
|
"Object": "register",
|
||||||
|
"CaptchaBypassToken": "",
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/accounts/profile")]
|
#[get("/accounts/profile")]
|
||||||
fn profile(headers: Headers, conn: DbConn) -> Json<Value> {
|
async fn profile(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
||||||
Json(headers.user.to_json(&conn))
|
Json(headers.user.to_json(&mut conn).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct ProfileData {
|
struct ProfileData {
|
||||||
#[serde(rename = "Culture")]
|
// Culture: String, // Ignored, always use en-US
|
||||||
_Culture: String, // Ignored, always use en-US
|
// MasterPasswordHint: Option<String>, // Ignored, has been moved to ChangePassData
|
||||||
MasterPasswordHint: Option<String>,
|
|
||||||
Name: String,
|
Name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/accounts/profile", data = "<data>")]
|
#[put("/accounts/profile", data = "<data>")]
|
||||||
fn put_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn put_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
post_profile(data, headers, conn)
|
post_profile(data, headers, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/profile", data = "<data>")]
|
#[post("/accounts/profile", data = "<data>")]
|
||||||
fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let data: ProfileData = data.into_inner().data;
|
let data: ProfileData = data.into_inner().data;
|
||||||
|
|
||||||
let mut user = headers.user;
|
// Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden)
|
||||||
|
// This also prevents issues with very long usernames causing to large JWT's. See #2419
|
||||||
|
if data.Name.len() > 50 {
|
||||||
|
err!("The field Name must be a string with a maximum length of 50.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut user = headers.user;
|
||||||
user.name = data.Name;
|
user.name = data.Name;
|
||||||
user.password_hint = match data.MasterPasswordHint {
|
|
||||||
Some(ref h) if h.is_empty() => None,
|
user.save(&mut conn).await?;
|
||||||
_ => data.MasterPasswordHint,
|
Ok(Json(user.to_json(&mut conn).await))
|
||||||
};
|
}
|
||||||
user.save(&conn)?;
|
|
||||||
Ok(Json(user.to_json(&conn)))
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct AvatarData {
|
||||||
|
AvatarColor: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/accounts/avatar", data = "<data>")]
|
||||||
|
async fn put_avatar(data: JsonUpcase<AvatarData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
|
let data: AvatarData = data.into_inner().data;
|
||||||
|
|
||||||
|
// It looks like it only supports the 6 hex color format.
|
||||||
|
// If you try to add the short value it will not show that color.
|
||||||
|
// Check and force 7 chars, including the #.
|
||||||
|
if let Some(color) = &data.AvatarColor {
|
||||||
|
if color.len() != 7 {
|
||||||
|
err!("The field AvatarColor must be a HTML/Hex color code with a length of 7 characters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut user = headers.user;
|
||||||
|
user.avatar_color = data.AvatarColor;
|
||||||
|
|
||||||
|
user.save(&mut conn).await?;
|
||||||
|
Ok(Json(user.to_json(&mut conn).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users/<uuid>/public-key")]
|
#[get("/users/<uuid>/public-key")]
|
||||||
fn get_public_keys(uuid: String, _headers: Headers, conn: DbConn) -> JsonResult {
|
async fn get_public_keys(uuid: String, _headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let user = match User::find_by_uuid(&uuid, &conn) {
|
let user = match User::find_by_uuid(&uuid, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("User doesn't exist"),
|
None => err!("User doesn't exist"),
|
||||||
};
|
};
|
||||||
@@ -202,7 +280,7 @@ fn get_public_keys(uuid: String, _headers: Headers, conn: DbConn) -> JsonResult
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/keys", data = "<data>")]
|
#[post("/accounts/keys", data = "<data>")]
|
||||||
fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let data: KeysData = data.into_inner().data;
|
let data: KeysData = data.into_inner().data;
|
||||||
|
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
@@ -210,7 +288,7 @@ fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, conn: DbConn) -> Json
|
|||||||
user.private_key = Some(data.EncryptedPrivateKey);
|
user.private_key = Some(data.EncryptedPrivateKey);
|
||||||
user.public_key = Some(data.PublicKey);
|
user.public_key = Some(data.PublicKey);
|
||||||
|
|
||||||
user.save(&conn)?;
|
user.save(&mut conn).await?;
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"PrivateKey": user.private_key,
|
"PrivateKey": user.private_key,
|
||||||
@@ -224,11 +302,17 @@ fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, conn: DbConn) -> Json
|
|||||||
struct ChangePassData {
|
struct ChangePassData {
|
||||||
MasterPasswordHash: String,
|
MasterPasswordHash: String,
|
||||||
NewMasterPasswordHash: String,
|
NewMasterPasswordHash: String,
|
||||||
|
MasterPasswordHint: Option<String>,
|
||||||
Key: String,
|
Key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/password", data = "<data>")]
|
#[post("/accounts/password", data = "<data>")]
|
||||||
fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
async fn post_password(
|
||||||
|
data: JsonUpcase<ChangePassData>,
|
||||||
|
headers: Headers,
|
||||||
|
mut conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> EmptyResult {
|
||||||
let data: ChangePassData = data.into_inner().data;
|
let data: ChangePassData = data.into_inner().data;
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
@@ -236,12 +320,27 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
|
|||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.password_hint = clean_password_hint(&data.MasterPasswordHint);
|
||||||
|
enforce_password_hint_setting(&user.password_hint)?;
|
||||||
|
|
||||||
|
log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn)
|
||||||
|
.await;
|
||||||
|
|
||||||
user.set_password(
|
user.set_password(
|
||||||
&data.NewMasterPasswordHash,
|
&data.NewMasterPasswordHash,
|
||||||
|
Some(data.Key),
|
||||||
|
true,
|
||||||
Some(vec![String::from("post_rotatekey"), String::from("get_contacts"), String::from("get_public_keys")]),
|
Some(vec![String::from("post_rotatekey"), String::from("get_contacts"), String::from("get_public_keys")]),
|
||||||
);
|
);
|
||||||
user.akey = data.Key;
|
|
||||||
user.save(&conn)
|
let save_result = user.save(&mut conn).await;
|
||||||
|
|
||||||
|
// Prevent loging out the client where the user requested this endpoint from.
|
||||||
|
// If you do logout the user it will causes issues at the client side.
|
||||||
|
// Adding the device uuid will prevent this.
|
||||||
|
nt.send_logout(&user, Some(headers.device.uuid)).await;
|
||||||
|
|
||||||
|
save_result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -249,6 +348,8 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
|
|||||||
struct ChangeKdfData {
|
struct ChangeKdfData {
|
||||||
Kdf: i32,
|
Kdf: i32,
|
||||||
KdfIterations: i32,
|
KdfIterations: i32,
|
||||||
|
KdfMemory: Option<i32>,
|
||||||
|
KdfParallelism: Option<i32>,
|
||||||
|
|
||||||
MasterPasswordHash: String,
|
MasterPasswordHash: String,
|
||||||
NewMasterPasswordHash: String,
|
NewMasterPasswordHash: String,
|
||||||
@@ -256,7 +357,7 @@ struct ChangeKdfData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/kdf", data = "<data>")]
|
#[post("/accounts/kdf", data = "<data>")]
|
||||||
fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
async fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||||
let data: ChangeKdfData = data.into_inner().data;
|
let data: ChangeKdfData = data.into_inner().data;
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
@@ -264,11 +365,42 @@ fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbConn) ->
|
|||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if data.Kdf == UserKdfType::Pbkdf2 as i32 && data.KdfIterations < 100_000 {
|
||||||
|
err!("PBKDF2 KDF iterations must be at least 100000.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Kdf == UserKdfType::Argon2id as i32 {
|
||||||
|
if data.KdfIterations < 1 {
|
||||||
|
err!("Argon2 KDF iterations must be at least 1.")
|
||||||
|
}
|
||||||
|
if let Some(m) = data.KdfMemory {
|
||||||
|
if !(15..=1024).contains(&m) {
|
||||||
|
err!("Argon2 memory must be between 15 MB and 1024 MB.")
|
||||||
|
}
|
||||||
|
user.client_kdf_memory = data.KdfMemory;
|
||||||
|
} else {
|
||||||
|
err!("Argon2 memory parameter is required.")
|
||||||
|
}
|
||||||
|
if let Some(p) = data.KdfParallelism {
|
||||||
|
if !(1..=16).contains(&p) {
|
||||||
|
err!("Argon2 parallelism must be between 1 and 16.")
|
||||||
|
}
|
||||||
|
user.client_kdf_parallelism = data.KdfParallelism;
|
||||||
|
} else {
|
||||||
|
err!("Argon2 parallelism parameter is required.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user.client_kdf_memory = None;
|
||||||
|
user.client_kdf_parallelism = None;
|
||||||
|
}
|
||||||
user.client_kdf_iter = data.KdfIterations;
|
user.client_kdf_iter = data.KdfIterations;
|
||||||
user.client_kdf_type = data.Kdf;
|
user.client_kdf_type = data.Kdf;
|
||||||
user.set_password(&data.NewMasterPasswordHash, None);
|
user.set_password(&data.NewMasterPasswordHash, Some(data.Key), true, None);
|
||||||
user.akey = data.Key;
|
let save_result = user.save(&mut conn).await;
|
||||||
user.save(&conn)
|
|
||||||
|
nt.send_logout(&user, Some(headers.device.uuid)).await;
|
||||||
|
|
||||||
|
save_result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -291,18 +423,24 @@ struct KeyData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/key", data = "<data>")]
|
#[post("/accounts/key", data = "<data>")]
|
||||||
fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||||
let data: KeyData = data.into_inner().data;
|
let data: KeyData = data.into_inner().data;
|
||||||
|
|
||||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate the import before continuing
|
||||||
|
// Bitwarden does not process the import if there is one item invalid.
|
||||||
|
// Since we check for the size of the encrypted note length, we need to do that here to pre-validate it.
|
||||||
|
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
|
||||||
|
Cipher::validate_notes(&data.Ciphers)?;
|
||||||
|
|
||||||
let user_uuid = &headers.user.uuid;
|
let user_uuid = &headers.user.uuid;
|
||||||
|
|
||||||
// Update folder data
|
// Update folder data
|
||||||
for folder_data in data.Folders {
|
for folder_data in data.Folders {
|
||||||
let mut saved_folder = match Folder::find_by_uuid(&folder_data.Id, &conn) {
|
let mut saved_folder = match Folder::find_by_uuid(&folder_data.Id, &mut conn).await {
|
||||||
Some(folder) => folder,
|
Some(folder) => folder,
|
||||||
None => err!("Folder doesn't exist"),
|
None => err!("Folder doesn't exist"),
|
||||||
};
|
};
|
||||||
@@ -312,14 +450,14 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, nt:
|
|||||||
}
|
}
|
||||||
|
|
||||||
saved_folder.name = folder_data.Name;
|
saved_folder.name = folder_data.Name;
|
||||||
saved_folder.save(&conn)?
|
saved_folder.save(&mut conn).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cipher data
|
// Update cipher data
|
||||||
use super::ciphers::update_cipher_from_data;
|
use super::ciphers::update_cipher_from_data;
|
||||||
|
|
||||||
for cipher_data in data.Ciphers {
|
for cipher_data in data.Ciphers {
|
||||||
let mut saved_cipher = match Cipher::find_by_uuid(cipher_data.Id.as_ref().unwrap(), &conn) {
|
let mut saved_cipher = match Cipher::find_by_uuid(cipher_data.Id.as_ref().unwrap(), &mut conn).await {
|
||||||
Some(cipher) => cipher,
|
Some(cipher) => cipher,
|
||||||
None => err!("Cipher doesn't exist"),
|
None => err!("Cipher doesn't exist"),
|
||||||
};
|
};
|
||||||
@@ -330,7 +468,9 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, nt:
|
|||||||
|
|
||||||
// Prevent triggering cipher updates via WebSockets by settings UpdateType::None
|
// Prevent triggering cipher updates via WebSockets by settings UpdateType::None
|
||||||
// The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.
|
// The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.
|
||||||
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &conn, &nt, UpdateType::None)?
|
// We force the users to logout after the user has been saved to try and prevent these issues.
|
||||||
|
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &mut conn, &nt, UpdateType::None)
|
||||||
|
.await?
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update user data
|
// Update user data
|
||||||
@@ -340,11 +480,23 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, nt:
|
|||||||
user.private_key = Some(data.PrivateKey);
|
user.private_key = Some(data.PrivateKey);
|
||||||
user.reset_security_stamp();
|
user.reset_security_stamp();
|
||||||
|
|
||||||
user.save(&conn)
|
let save_result = user.save(&mut conn).await;
|
||||||
|
|
||||||
|
// Prevent loging out the client where the user requested this endpoint from.
|
||||||
|
// If you do logout the user it will causes issues at the client side.
|
||||||
|
// Adding the device uuid will prevent this.
|
||||||
|
nt.send_logout(&user, Some(headers.device.uuid)).await;
|
||||||
|
|
||||||
|
save_result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/security-stamp", data = "<data>")]
|
#[post("/accounts/security-stamp", data = "<data>")]
|
||||||
fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
async fn post_sstamp(
|
||||||
|
data: JsonUpcase<PasswordData>,
|
||||||
|
headers: Headers,
|
||||||
|
mut conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> EmptyResult {
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
@@ -352,9 +504,13 @@ fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -
|
|||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
Device::delete_all_by_user(&user.uuid, &conn)?;
|
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
|
||||||
user.reset_security_stamp();
|
user.reset_security_stamp();
|
||||||
user.save(&conn)
|
let save_result = user.save(&mut conn).await;
|
||||||
|
|
||||||
|
nt.send_logout(&user, None).await;
|
||||||
|
|
||||||
|
save_result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -365,7 +521,7 @@ struct EmailTokenData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/email-token", data = "<data>")]
|
#[post("/accounts/email-token", data = "<data>")]
|
||||||
fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
async fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||||
let data: EmailTokenData = data.into_inner().data;
|
let data: EmailTokenData = data.into_inner().data;
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
@@ -373,7 +529,7 @@ fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: Db
|
|||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
if User::find_by_mail(&data.NewEmail, &conn).is_some() {
|
if User::find_by_mail(&data.NewEmail, &mut conn).await.is_some() {
|
||||||
err!("Email already in use");
|
err!("Email already in use");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,14 +540,14 @@ fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: Db
|
|||||||
let token = crypto::generate_email_token(6);
|
let token = crypto::generate_email_token(6);
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
if let Err(e) = mail::send_change_email(&data.NewEmail, &token) {
|
if let Err(e) = mail::send_change_email(&data.NewEmail, &token).await {
|
||||||
error!("Error sending change-email email: {:#?}", e);
|
error!("Error sending change-email email: {:#?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user.email_new = Some(data.NewEmail);
|
user.email_new = Some(data.NewEmail);
|
||||||
user.email_new_token = Some(token);
|
user.email_new_token = Some(token);
|
||||||
user.save(&conn)
|
user.save(&mut conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -406,7 +562,12 @@ struct ChangeEmailData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/email", data = "<data>")]
|
#[post("/accounts/email", data = "<data>")]
|
||||||
fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
async fn post_email(
|
||||||
|
data: JsonUpcase<ChangeEmailData>,
|
||||||
|
headers: Headers,
|
||||||
|
mut conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> EmptyResult {
|
||||||
let data: ChangeEmailData = data.into_inner().data;
|
let data: ChangeEmailData = data.into_inner().data;
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
@@ -414,7 +575,7 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
|
|||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
if User::find_by_mail(&data.NewEmail, &conn).is_some() {
|
if User::find_by_mail(&data.NewEmail, &mut conn).await.is_some() {
|
||||||
err!("Email already in use");
|
err!("Email already in use");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,21 +607,24 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
|
|||||||
user.email_new = None;
|
user.email_new = None;
|
||||||
user.email_new_token = None;
|
user.email_new_token = None;
|
||||||
|
|
||||||
user.set_password(&data.NewMasterPasswordHash, None);
|
user.set_password(&data.NewMasterPasswordHash, Some(data.Key), true, None);
|
||||||
user.akey = data.Key;
|
|
||||||
|
|
||||||
user.save(&conn)
|
let save_result = user.save(&mut conn).await;
|
||||||
|
|
||||||
|
nt.send_logout(&user, None).await;
|
||||||
|
|
||||||
|
save_result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/verify-email")]
|
#[post("/accounts/verify-email")]
|
||||||
fn post_verify_email(headers: Headers) -> EmptyResult {
|
async fn post_verify_email(headers: Headers) -> EmptyResult {
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
if !CONFIG.mail_enabled() {
|
if !CONFIG.mail_enabled() {
|
||||||
err!("Cannot verify email address");
|
err!("Cannot verify email address");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = mail::send_verify_email(&user.email, &user.uuid) {
|
if let Err(e) = mail::send_verify_email(&user.email, &user.uuid).await {
|
||||||
error!("Error sending verify_email email: {:#?}", e);
|
error!("Error sending verify_email email: {:#?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,10 +639,10 @@ struct VerifyEmailTokenData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/verify-email-token", data = "<data>")]
|
#[post("/accounts/verify-email-token", data = "<data>")]
|
||||||
fn post_verify_email_token(data: JsonUpcase<VerifyEmailTokenData>, conn: DbConn) -> EmptyResult {
|
async fn post_verify_email_token(data: JsonUpcase<VerifyEmailTokenData>, mut conn: DbConn) -> EmptyResult {
|
||||||
let data: VerifyEmailTokenData = data.into_inner().data;
|
let data: VerifyEmailTokenData = data.into_inner().data;
|
||||||
|
|
||||||
let mut user = match User::find_by_uuid(&data.UserId, &conn) {
|
let mut user = match User::find_by_uuid(&data.UserId, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("User doesn't exist"),
|
None => err!("User doesn't exist"),
|
||||||
};
|
};
|
||||||
@@ -493,7 +657,7 @@ fn post_verify_email_token(data: JsonUpcase<VerifyEmailTokenData>, conn: DbConn)
|
|||||||
user.verified_at = Some(Utc::now().naive_utc());
|
user.verified_at = Some(Utc::now().naive_utc());
|
||||||
user.last_verifying_at = None;
|
user.last_verifying_at = None;
|
||||||
user.login_verify_count = 0;
|
user.login_verify_count = 0;
|
||||||
if let Err(e) = user.save(&conn) {
|
if let Err(e) = user.save(&mut conn).await {
|
||||||
error!("Error saving email verification: {:#?}", e);
|
error!("Error saving email verification: {:#?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,14 +671,12 @@ struct DeleteRecoverData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/delete-recover", data = "<data>")]
|
#[post("/accounts/delete-recover", data = "<data>")]
|
||||||
fn post_delete_recover(data: JsonUpcase<DeleteRecoverData>, conn: DbConn) -> EmptyResult {
|
async fn post_delete_recover(data: JsonUpcase<DeleteRecoverData>, mut conn: DbConn) -> EmptyResult {
|
||||||
let data: DeleteRecoverData = data.into_inner().data;
|
let data: DeleteRecoverData = data.into_inner().data;
|
||||||
|
|
||||||
let user = User::find_by_mail(&data.Email, &conn);
|
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
if let Some(user) = user {
|
if let Some(user) = User::find_by_mail(&data.Email, &mut conn).await {
|
||||||
if let Err(e) = mail::send_delete_account(&user.email, &user.uuid) {
|
if let Err(e) = mail::send_delete_account(&user.email, &user.uuid).await {
|
||||||
error!("Error sending delete account email: {:#?}", e);
|
error!("Error sending delete account email: {:#?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -536,10 +698,10 @@ struct DeleteRecoverTokenData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/delete-recover-token", data = "<data>")]
|
#[post("/accounts/delete-recover-token", data = "<data>")]
|
||||||
fn post_delete_recover_token(data: JsonUpcase<DeleteRecoverTokenData>, conn: DbConn) -> EmptyResult {
|
async fn post_delete_recover_token(data: JsonUpcase<DeleteRecoverTokenData>, mut conn: DbConn) -> EmptyResult {
|
||||||
let data: DeleteRecoverTokenData = data.into_inner().data;
|
let data: DeleteRecoverTokenData = data.into_inner().data;
|
||||||
|
|
||||||
let user = match User::find_by_uuid(&data.UserId, &conn) {
|
let user = match User::find_by_uuid(&data.UserId, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("User doesn't exist"),
|
None => err!("User doesn't exist"),
|
||||||
};
|
};
|
||||||
@@ -551,16 +713,16 @@ fn post_delete_recover_token(data: JsonUpcase<DeleteRecoverTokenData>, conn: DbC
|
|||||||
if claims.sub != user.uuid {
|
if claims.sub != user.uuid {
|
||||||
err!("Invalid claim");
|
err!("Invalid claim");
|
||||||
}
|
}
|
||||||
user.delete(&conn)
|
user.delete(&mut conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/delete", data = "<data>")]
|
#[post("/accounts/delete", data = "<data>")]
|
||||||
fn post_delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
async fn post_delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||||
delete_account(data, headers, conn)
|
delete_account(data, headers, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/accounts", data = "<data>")]
|
#[delete("/accounts", data = "<data>")]
|
||||||
fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
async fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
@@ -568,13 +730,13 @@ fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn
|
|||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
user.delete(&conn)
|
user.delete(&mut conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/accounts/revision-date")]
|
#[get("/accounts/revision-date")]
|
||||||
fn revision_date(headers: Headers) -> String {
|
fn revision_date(headers: Headers) -> JsonResult {
|
||||||
let revision_date = headers.user.updated_at.timestamp_millis();
|
let revision_date = headers.user.updated_at.timestamp_millis();
|
||||||
revision_date.to_string()
|
Ok(Json(json!(revision_date)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -584,7 +746,7 @@ struct PasswordHintData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/password-hint", data = "<data>")]
|
#[post("/accounts/password-hint", data = "<data>")]
|
||||||
fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResult {
|
async fn password_hint(data: JsonUpcase<PasswordHintData>, mut conn: DbConn) -> EmptyResult {
|
||||||
if !CONFIG.mail_enabled() && !CONFIG.show_password_hint() {
|
if !CONFIG.mail_enabled() && !CONFIG.show_password_hint() {
|
||||||
err!("This server is not configured to provide password hints.");
|
err!("This server is not configured to provide password hints.");
|
||||||
}
|
}
|
||||||
@@ -594,19 +756,18 @@ fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResul
|
|||||||
let data: PasswordHintData = data.into_inner().data;
|
let data: PasswordHintData = data.into_inner().data;
|
||||||
let email = &data.Email;
|
let email = &data.Email;
|
||||||
|
|
||||||
match User::find_by_mail(email, &conn) {
|
match User::find_by_mail(email, &mut conn).await {
|
||||||
None => {
|
None => {
|
||||||
// To prevent user enumeration, act as if the user exists.
|
// To prevent user enumeration, act as if the user exists.
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
// There is still a timing side channel here in that the code
|
// There is still a timing side channel here in that the code
|
||||||
// paths that send mail take noticeably longer than ones that
|
// paths that send mail take noticeably longer than ones that
|
||||||
// don't. Add a randomized sleep to mitigate this somewhat.
|
// don't. Add a randomized sleep to mitigate this somewhat.
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{rngs::SmallRng, Rng, SeedableRng};
|
||||||
let mut rng = thread_rng();
|
let mut rng = SmallRng::from_entropy();
|
||||||
let base = 1000;
|
|
||||||
let delta: i32 = 100;
|
let delta: i32 = 100;
|
||||||
let sleep_ms = (base + rng.gen_range(-delta..=delta)) as u64;
|
let sleep_ms = (1_000 + rng.gen_range(-delta..=delta)) as u64;
|
||||||
std::thread::sleep(std::time::Duration::from_millis(sleep_ms));
|
tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
err!(NO_HINT);
|
err!(NO_HINT);
|
||||||
@@ -615,10 +776,10 @@ fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResul
|
|||||||
Some(user) => {
|
Some(user) => {
|
||||||
let hint: Option<String> = user.password_hint;
|
let hint: Option<String> = user.password_hint;
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_password_hint(email, hint)?;
|
mail::send_password_hint(email, hint).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
} else if let Some(hint) = hint {
|
} else if let Some(hint) = hint {
|
||||||
err!(format!("Your password hint is: {}", hint));
|
err!(format!("Your password hint is: {hint}"));
|
||||||
} else {
|
} else {
|
||||||
err!(NO_HINT);
|
err!(NO_HINT);
|
||||||
}
|
}
|
||||||
@@ -628,23 +789,31 @@ fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResul
|
|||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct PreloginData {
|
pub struct PreloginData {
|
||||||
Email: String,
|
Email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/prelogin", data = "<data>")]
|
#[post("/accounts/prelogin", data = "<data>")]
|
||||||
fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Value> {
|
async fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Value> {
|
||||||
|
_prelogin(data, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn _prelogin(data: JsonUpcase<PreloginData>, mut conn: DbConn) -> Json<Value> {
|
||||||
let data: PreloginData = data.into_inner().data;
|
let data: PreloginData = data.into_inner().data;
|
||||||
|
|
||||||
let (kdf_type, kdf_iter) = match User::find_by_mail(&data.Email, &conn) {
|
let (kdf_type, kdf_iter, kdf_mem, kdf_para) = match User::find_by_mail(&data.Email, &mut conn).await {
|
||||||
Some(user) => (user.client_kdf_type, user.client_kdf_iter),
|
Some(user) => (user.client_kdf_type, user.client_kdf_iter, user.client_kdf_memory, user.client_kdf_parallelism),
|
||||||
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT),
|
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
Json(json!({
|
let result = json!({
|
||||||
"Kdf": kdf_type,
|
"Kdf": kdf_type,
|
||||||
"KdfIterations": kdf_iter
|
"KdfIterations": kdf_iter,
|
||||||
}))
|
"KdfMemory": kdf_mem,
|
||||||
|
"KdfParallelism": kdf_para,
|
||||||
|
});
|
||||||
|
|
||||||
|
Json(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bitwarden/server/blob/master/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs
|
// https://github.com/bitwarden/server/blob/master/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs
|
||||||
@@ -666,7 +835,14 @@ fn verify_password(data: JsonUpcase<SecretVerificationRequest>, headers: Headers
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _api_key(data: JsonUpcase<SecretVerificationRequest>, rotate: bool, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn _api_key(
|
||||||
|
data: JsonUpcase<SecretVerificationRequest>,
|
||||||
|
rotate: bool,
|
||||||
|
headers: Headers,
|
||||||
|
mut conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
|
use crate::util::format_date;
|
||||||
|
|
||||||
let data: SecretVerificationRequest = data.into_inner().data;
|
let data: SecretVerificationRequest = data.into_inner().data;
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
@@ -676,21 +852,81 @@ fn _api_key(data: JsonUpcase<SecretVerificationRequest>, rotate: bool, headers:
|
|||||||
|
|
||||||
if rotate || user.api_key.is_none() {
|
if rotate || user.api_key.is_none() {
|
||||||
user.api_key = Some(crypto::generate_api_key());
|
user.api_key = Some(crypto::generate_api_key());
|
||||||
user.save(&conn).expect("Error saving API key");
|
user.save(&mut conn).await.expect("Error saving API key");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"ApiKey": user.api_key,
|
"ApiKey": user.api_key,
|
||||||
|
"RevisionDate": format_date(&user.updated_at),
|
||||||
"Object": "apiKey",
|
"Object": "apiKey",
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/api-key", data = "<data>")]
|
#[post("/accounts/api-key", data = "<data>")]
|
||||||
fn api_key(data: JsonUpcase<SecretVerificationRequest>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn api_key(data: JsonUpcase<SecretVerificationRequest>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
_api_key(data, false, headers, conn)
|
_api_key(data, false, headers, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/rotate-api-key", data = "<data>")]
|
#[post("/accounts/rotate-api-key", data = "<data>")]
|
||||||
fn rotate_api_key(data: JsonUpcase<SecretVerificationRequest>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn rotate_api_key(data: JsonUpcase<SecretVerificationRequest>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
_api_key(data, true, headers, conn)
|
_api_key(data, true, headers, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// This variant is deprecated: https://github.com/bitwarden/server/pull/2682
|
||||||
|
#[get("/devices/knowndevice/<email>/<uuid>")]
|
||||||
|
async fn get_known_device_from_path(email: String, uuid: String, mut conn: DbConn) -> JsonResult {
|
||||||
|
// This endpoint doesn't have auth header
|
||||||
|
let mut result = false;
|
||||||
|
if let Some(user) = User::find_by_mail(&email, &mut conn).await {
|
||||||
|
result = Device::find_by_uuid_and_user(&uuid, &user.uuid, &mut conn).await.is_some();
|
||||||
|
}
|
||||||
|
Ok(Json(json!(result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/devices/knowndevice")]
|
||||||
|
async fn get_known_device(device: KnownDevice, conn: DbConn) -> JsonResult {
|
||||||
|
get_known_device_from_path(device.email, device.uuid, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
|
struct KnownDevice {
|
||||||
|
email: String,
|
||||||
|
uuid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for KnownDevice {
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
|
let email = if let Some(email_b64) = req.headers().get_one("X-Request-Email") {
|
||||||
|
let email_bytes = match data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes()) {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(_) => {
|
||||||
|
return Outcome::Failure((
|
||||||
|
Status::BadRequest,
|
||||||
|
"X-Request-Email value failed to decode as base64url",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match String::from_utf8(email_bytes) {
|
||||||
|
Ok(email) => email,
|
||||||
|
Err(_) => {
|
||||||
|
return Outcome::Failure((Status::BadRequest, "X-Request-Email value failed to decode as UTF-8"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Outcome::Failure((Status::BadRequest, "X-Request-Email value is required"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let uuid = if let Some(uuid) = req.headers().get_one("X-Device-Identifier") {
|
||||||
|
uuid.to_string()
|
||||||
|
} else {
|
||||||
|
return Outcome::Failure((Status::BadRequest, "X-Device-Identifier value is required"));
|
||||||
|
};
|
||||||
|
|
||||||
|
Outcome::Success(KnownDevice {
|
||||||
|
email,
|
||||||
|
uuid,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use rocket::Route;
|
use rocket::{serde::json::Json, Route};
|
||||||
use rocket_contrib::json::Json;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::borrow::Borrow;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString},
|
api::{
|
||||||
|
core::{CipherSyncData, CipherSyncType},
|
||||||
|
EmptyResult, JsonResult, JsonUpcase, NumberOrString,
|
||||||
|
},
|
||||||
auth::{decode_emergency_access_invite, Headers},
|
auth::{decode_emergency_access_invite, Headers},
|
||||||
db::{models::*, DbConn, DbPool},
|
db::{models::*, DbConn, DbPool},
|
||||||
mail, CONFIG,
|
mail, CONFIG,
|
||||||
@@ -36,13 +37,14 @@ pub fn routes() -> Vec<Route> {
|
|||||||
// region get
|
// region get
|
||||||
|
|
||||||
#[get("/emergency-access/trusted")]
|
#[get("/emergency-access/trusted")]
|
||||||
fn get_contacts(headers: Headers, conn: DbConn) -> JsonResult {
|
async fn get_contacts(headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let emergency_access_list = EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &conn);
|
let emergency_access_list = EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &mut conn).await;
|
||||||
|
let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len());
|
||||||
let emergency_access_list_json: Vec<Value> =
|
for ea in emergency_access_list {
|
||||||
emergency_access_list.iter().map(|e| e.to_json_grantee_details(&conn)).collect();
|
emergency_access_list_json.push(ea.to_json_grantee_details(&mut conn).await);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Data": emergency_access_list_json,
|
"Data": emergency_access_list_json,
|
||||||
@@ -52,13 +54,14 @@ fn get_contacts(headers: Headers, conn: DbConn) -> JsonResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/emergency-access/granted")]
|
#[get("/emergency-access/granted")]
|
||||||
fn get_grantees(headers: Headers, conn: DbConn) -> JsonResult {
|
async fn get_grantees(headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let emergency_access_list = EmergencyAccess::find_all_by_grantee_uuid(&headers.user.uuid, &conn);
|
let emergency_access_list = EmergencyAccess::find_all_by_grantee_uuid(&headers.user.uuid, &mut conn).await;
|
||||||
|
let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len());
|
||||||
let emergency_access_list_json: Vec<Value> =
|
for ea in emergency_access_list {
|
||||||
emergency_access_list.iter().map(|e| e.to_json_grantor_details(&conn)).collect();
|
emergency_access_list_json.push(ea.to_json_grantor_details(&mut conn).await);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Data": emergency_access_list_json,
|
"Data": emergency_access_list_json,
|
||||||
@@ -68,11 +71,11 @@ fn get_grantees(headers: Headers, conn: DbConn) -> JsonResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/emergency-access/<emer_id>")]
|
#[get("/emergency-access/<emer_id>")]
|
||||||
fn get_emergency_access(emer_id: String, conn: DbConn) -> JsonResult {
|
async fn get_emergency_access(emer_id: String, mut conn: DbConn) -> JsonResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
match EmergencyAccess::find_by_uuid(&emer_id, &conn) {
|
match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||||
Some(emergency_access) => Ok(Json(emergency_access.to_json_grantee_details(&conn))),
|
Some(emergency_access) => Ok(Json(emergency_access.to_json_grantee_details(&mut conn).await)),
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +84,7 @@ fn get_emergency_access(emer_id: String, conn: DbConn) -> JsonResult {
|
|||||||
|
|
||||||
// region put/post
|
// region put/post
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct EmergencyAccessUpdateData {
|
struct EmergencyAccessUpdateData {
|
||||||
Type: NumberOrString,
|
Type: NumberOrString,
|
||||||
@@ -90,17 +93,25 @@ struct EmergencyAccessUpdateData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/emergency-access/<emer_id>", data = "<data>")]
|
#[put("/emergency-access/<emer_id>", data = "<data>")]
|
||||||
fn put_emergency_access(emer_id: String, data: JsonUpcase<EmergencyAccessUpdateData>, conn: DbConn) -> JsonResult {
|
async fn put_emergency_access(
|
||||||
post_emergency_access(emer_id, data, conn)
|
emer_id: String,
|
||||||
|
data: JsonUpcase<EmergencyAccessUpdateData>,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
|
post_emergency_access(emer_id, data, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>", data = "<data>")]
|
#[post("/emergency-access/<emer_id>", data = "<data>")]
|
||||||
fn post_emergency_access(emer_id: String, data: JsonUpcase<EmergencyAccessUpdateData>, conn: DbConn) -> JsonResult {
|
async fn post_emergency_access(
|
||||||
|
emer_id: String,
|
||||||
|
data: JsonUpcase<EmergencyAccessUpdateData>,
|
||||||
|
mut conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let data: EmergencyAccessUpdateData = data.into_inner().data;
|
let data: EmergencyAccessUpdateData = data.into_inner().data;
|
||||||
|
|
||||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) {
|
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||||
Some(emergency_access) => emergency_access,
|
Some(emergency_access) => emergency_access,
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
@@ -112,9 +123,11 @@ fn post_emergency_access(emer_id: String, data: JsonUpcase<EmergencyAccessUpdate
|
|||||||
|
|
||||||
emergency_access.atype = new_type;
|
emergency_access.atype = new_type;
|
||||||
emergency_access.wait_time_days = data.WaitTimeDays;
|
emergency_access.wait_time_days = data.WaitTimeDays;
|
||||||
|
if data.KeyEncrypted.is_some() {
|
||||||
emergency_access.key_encrypted = data.KeyEncrypted;
|
emergency_access.key_encrypted = data.KeyEncrypted;
|
||||||
|
}
|
||||||
|
|
||||||
emergency_access.save(&conn)?;
|
emergency_access.save(&mut conn).await?;
|
||||||
Ok(Json(emergency_access.to_json()))
|
Ok(Json(emergency_access.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,12 +136,12 @@ fn post_emergency_access(emer_id: String, data: JsonUpcase<EmergencyAccessUpdate
|
|||||||
// region delete
|
// region delete
|
||||||
|
|
||||||
#[delete("/emergency-access/<emer_id>")]
|
#[delete("/emergency-access/<emer_id>")]
|
||||||
fn delete_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
async fn delete_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let grantor_user = headers.user;
|
let grantor_user = headers.user;
|
||||||
|
|
||||||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) {
|
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||||
Some(emer) => {
|
Some(emer) => {
|
||||||
if emer.grantor_uuid != grantor_user.uuid && emer.grantee_uuid != Some(grantor_user.uuid) {
|
if emer.grantor_uuid != grantor_user.uuid && emer.grantee_uuid != Some(grantor_user.uuid) {
|
||||||
err!("Emergency access not valid.")
|
err!("Emergency access not valid.")
|
||||||
@@ -137,20 +150,20 @@ fn delete_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> E
|
|||||||
}
|
}
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
emergency_access.delete(&conn)?;
|
emergency_access.delete(&mut conn).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/delete")]
|
#[post("/emergency-access/<emer_id>/delete")]
|
||||||
fn post_delete_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
async fn post_delete_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||||
delete_emergency_access(emer_id, headers, conn)
|
delete_emergency_access(emer_id, headers, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region invite
|
// region invite
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct EmergencyAccessInviteData {
|
struct EmergencyAccessInviteData {
|
||||||
Email: String,
|
Email: String,
|
||||||
@@ -159,7 +172,7 @@ struct EmergencyAccessInviteData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/emergency-access/invite", data = "<data>")]
|
#[post("/emergency-access/invite", data = "<data>")]
|
||||||
fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
async fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let data: EmergencyAccessInviteData = data.into_inner().data;
|
let data: EmergencyAccessInviteData = data.into_inner().data;
|
||||||
@@ -180,10 +193,10 @@ fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Headers, co
|
|||||||
err!("You can not set yourself as an emergency contact.")
|
err!("You can not set yourself as an emergency contact.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let grantee_user = match User::find_by_mail(&email, &conn) {
|
let grantee_user = match User::find_by_mail(&email, &mut conn).await {
|
||||||
None => {
|
None => {
|
||||||
if !CONFIG.invitations_allowed() {
|
if !CONFIG.invitations_allowed() {
|
||||||
err!(format!("Grantee user does not exist: {}", email))
|
err!(format!("Grantee user does not exist: {}", &email))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !CONFIG.is_email_domain_allowed(&email) {
|
if !CONFIG.is_email_domain_allowed(&email) {
|
||||||
@@ -191,12 +204,12 @@ fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Headers, co
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !CONFIG.mail_enabled() {
|
if !CONFIG.mail_enabled() {
|
||||||
let invitation = Invitation::new(email.clone());
|
let invitation = Invitation::new(&email);
|
||||||
invitation.save(&conn)?;
|
invitation.save(&mut conn).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut user = User::new(email.clone());
|
let mut user = User::new(email.clone());
|
||||||
user.save(&conn)?;
|
user.save(&mut conn).await?;
|
||||||
user
|
user
|
||||||
}
|
}
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
@@ -206,39 +219,34 @@ fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Headers, co
|
|||||||
&grantor_user.uuid,
|
&grantor_user.uuid,
|
||||||
&grantee_user.uuid,
|
&grantee_user.uuid,
|
||||||
&grantee_user.email,
|
&grantee_user.email,
|
||||||
&conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
|
.await
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
err!(format!("Grantee user already invited: {}", email))
|
err!(format!("Grantee user already invited: {}", &grantee_user.email))
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut new_emergency_access = EmergencyAccess::new(
|
let mut new_emergency_access =
|
||||||
grantor_user.uuid.clone(),
|
EmergencyAccess::new(grantor_user.uuid, grantee_user.email, emergency_access_status, new_type, wait_time_days);
|
||||||
Some(grantee_user.email.clone()),
|
new_emergency_access.save(&mut conn).await?;
|
||||||
emergency_access_status,
|
|
||||||
new_type,
|
|
||||||
wait_time_days,
|
|
||||||
);
|
|
||||||
new_emergency_access.save(&conn)?;
|
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_emergency_access_invite(
|
mail::send_emergency_access_invite(
|
||||||
&grantee_user.email,
|
&new_emergency_access.email.expect("Grantee email does not exists"),
|
||||||
&grantee_user.uuid,
|
&grantee_user.uuid,
|
||||||
Some(new_emergency_access.uuid),
|
&new_emergency_access.uuid,
|
||||||
Some(grantor_user.name.clone()),
|
&grantor_user.name,
|
||||||
Some(grantor_user.email),
|
&grantor_user.email,
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
// Automatically mark user as accepted if no email invites
|
// Automatically mark user as accepted if no email invites
|
||||||
match User::find_by_mail(&email, &conn) {
|
match User::find_by_mail(&email, &mut conn).await {
|
||||||
Some(user) => {
|
Some(user) => match accept_invite_process(user.uuid, &mut new_emergency_access, &email, &mut conn).await {
|
||||||
match accept_invite_process(user.uuid, new_emergency_access.uuid, Some(email), conn.borrow()) {
|
Ok(v) => v,
|
||||||
Ok(v) => (v),
|
|
||||||
Err(e) => err!(e.to_string()),
|
Err(e) => err!(e.to_string()),
|
||||||
}
|
},
|
||||||
}
|
|
||||||
None => err!("Grantee user not found."),
|
None => err!("Grantee user not found."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,10 +255,10 @@ fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Headers, co
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/reinvite")]
|
#[post("/emergency-access/<emer_id>/reinvite")]
|
||||||
fn resend_invite(emer_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
async fn resend_invite(emer_id: String, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) {
|
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||||
Some(emer) => emer,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
@@ -268,7 +276,7 @@ fn resend_invite(emer_id: String, headers: Headers, conn: DbConn) -> EmptyResult
|
|||||||
None => err!("Email not valid."),
|
None => err!("Email not valid."),
|
||||||
};
|
};
|
||||||
|
|
||||||
let grantee_user = match User::find_by_mail(&email, &conn) {
|
let grantee_user = match User::find_by_mail(&email, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Grantee user not found."),
|
None => err!("Grantee user not found."),
|
||||||
};
|
};
|
||||||
@@ -279,19 +287,20 @@ fn resend_invite(emer_id: String, headers: Headers, conn: DbConn) -> EmptyResult
|
|||||||
mail::send_emergency_access_invite(
|
mail::send_emergency_access_invite(
|
||||||
&email,
|
&email,
|
||||||
&grantor_user.uuid,
|
&grantor_user.uuid,
|
||||||
Some(emergency_access.uuid),
|
&emergency_access.uuid,
|
||||||
Some(grantor_user.name.clone()),
|
&grantor_user.name,
|
||||||
Some(grantor_user.email),
|
&grantor_user.email,
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
if Invitation::find_by_mail(&email, &conn).is_none() {
|
if Invitation::find_by_mail(&email, &mut conn).await.is_none() {
|
||||||
let invitation = Invitation::new(email);
|
let invitation = Invitation::new(&email);
|
||||||
invitation.save(&conn)?;
|
invitation.save(&mut conn).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically mark user as accepted if no email invites
|
// Automatically mark user as accepted if no email invites
|
||||||
match accept_invite_process(grantee_user.uuid, emergency_access.uuid, emergency_access.email, conn.borrow()) {
|
match accept_invite_process(grantee_user.uuid, &mut emergency_access, &email, &mut conn).await {
|
||||||
Ok(v) => (v),
|
Ok(v) => v,
|
||||||
Err(e) => err!(e.to_string()),
|
Err(e) => err!(e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,43 +315,54 @@ struct AcceptData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/accept", data = "<data>")]
|
#[post("/emergency-access/<emer_id>/accept", data = "<data>")]
|
||||||
fn accept_invite(emer_id: String, data: JsonUpcase<AcceptData>, conn: DbConn) -> EmptyResult {
|
async fn accept_invite(
|
||||||
|
emer_id: String,
|
||||||
|
data: JsonUpcase<AcceptData>,
|
||||||
|
headers: Headers,
|
||||||
|
mut conn: DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let data: AcceptData = data.into_inner().data;
|
let data: AcceptData = data.into_inner().data;
|
||||||
let token = &data.Token;
|
let token = &data.Token;
|
||||||
let claims = decode_emergency_access_invite(token)?;
|
let claims = decode_emergency_access_invite(token)?;
|
||||||
|
|
||||||
let grantee_user = match User::find_by_mail(&claims.email, &conn) {
|
// This can happen if the user who received the invite used a different email to signup.
|
||||||
|
// Since we do not know if this is intented, we error out here and do nothing with the invite.
|
||||||
|
if claims.email != headers.user.email {
|
||||||
|
err!("Claim email does not match current users email")
|
||||||
|
}
|
||||||
|
|
||||||
|
let grantee_user = match User::find_by_mail(&claims.email, &mut conn).await {
|
||||||
Some(user) => {
|
Some(user) => {
|
||||||
Invitation::take(&claims.email, &conn);
|
Invitation::take(&claims.email, &mut conn).await;
|
||||||
user
|
user
|
||||||
}
|
}
|
||||||
None => err!("Invited user not found"),
|
None => err!("Invited user not found"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) {
|
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||||
Some(emer) => emer,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
|
|
||||||
// get grantor user to send Accepted email
|
// get grantor user to send Accepted email
|
||||||
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) {
|
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Grantor user not found."),
|
None => err!("Grantor user not found."),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (claims.emer_id.is_some() && emer_id == claims.emer_id.unwrap())
|
if emer_id == claims.emer_id
|
||||||
&& (claims.grantor_name.is_some() && grantor_user.name == claims.grantor_name.unwrap())
|
&& grantor_user.name == claims.grantor_name
|
||||||
&& (claims.grantor_email.is_some() && grantor_user.email == claims.grantor_email.unwrap())
|
&& grantor_user.email == claims.grantor_email
|
||||||
{
|
{
|
||||||
match accept_invite_process(grantee_user.uuid.clone(), emer_id, Some(grantee_user.email.clone()), &conn) {
|
match accept_invite_process(grantee_user.uuid, &mut emergency_access, &grantee_user.email, &mut conn).await {
|
||||||
Ok(v) => (v),
|
Ok(v) => v,
|
||||||
Err(e) => err!(e.to_string()),
|
Err(e) => err!(e.to_string()),
|
||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_emergency_access_invite_accepted(&grantor_user.email, &grantee_user.email)?;
|
mail::send_emergency_access_invite_accepted(&grantor_user.email, &grantee_user.email).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -351,14 +371,13 @@ fn accept_invite(emer_id: String, data: JsonUpcase<AcceptData>, conn: DbConn) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept_invite_process(grantee_uuid: String, emer_id: String, email: Option<String>, conn: &DbConn) -> EmptyResult {
|
async fn accept_invite_process(
|
||||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, conn) {
|
grantee_uuid: String,
|
||||||
Some(emer) => emer,
|
emergency_access: &mut EmergencyAccess,
|
||||||
None => err!("Emergency access not valid."),
|
grantee_email: &str,
|
||||||
};
|
conn: &mut DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
let emer_email = emergency_access.email;
|
if emergency_access.email.is_none() || emergency_access.email.as_ref().unwrap() != grantee_email {
|
||||||
if emer_email.is_none() || emer_email != email {
|
|
||||||
err!("User email does not match invite.");
|
err!("User email does not match invite.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +388,7 @@ fn accept_invite_process(grantee_uuid: String, emer_id: String, email: Option<St
|
|||||||
emergency_access.status = EmergencyAccessStatus::Accepted as i32;
|
emergency_access.status = EmergencyAccessStatus::Accepted as i32;
|
||||||
emergency_access.grantee_uuid = Some(grantee_uuid);
|
emergency_access.grantee_uuid = Some(grantee_uuid);
|
||||||
emergency_access.email = None;
|
emergency_access.email = None;
|
||||||
emergency_access.save(conn)
|
emergency_access.save(conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -379,11 +398,11 @@ struct ConfirmData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/confirm", data = "<data>")]
|
#[post("/emergency-access/<emer_id>/confirm", data = "<data>")]
|
||||||
fn confirm_emergency_access(
|
async fn confirm_emergency_access(
|
||||||
emer_id: String,
|
emer_id: String,
|
||||||
data: JsonUpcase<ConfirmData>,
|
data: JsonUpcase<ConfirmData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
@@ -391,7 +410,7 @@ fn confirm_emergency_access(
|
|||||||
let data: ConfirmData = data.into_inner().data;
|
let data: ConfirmData = data.into_inner().data;
|
||||||
let key = data.Key;
|
let key = data.Key;
|
||||||
|
|
||||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) {
|
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||||
Some(emer) => emer,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
@@ -402,13 +421,13 @@ fn confirm_emergency_access(
|
|||||||
err!("Emergency access not valid.")
|
err!("Emergency access not valid.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let grantor_user = match User::find_by_uuid(&confirming_user.uuid, &conn) {
|
let grantor_user = match User::find_by_uuid(&confirming_user.uuid, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Grantor user not found."),
|
None => err!("Grantor user not found."),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
|
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
|
||||||
let grantee_user = match User::find_by_uuid(grantee_uuid, &conn) {
|
let grantee_user = match User::find_by_uuid(grantee_uuid, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Grantee user not found."),
|
None => err!("Grantee user not found."),
|
||||||
};
|
};
|
||||||
@@ -417,10 +436,10 @@ fn confirm_emergency_access(
|
|||||||
emergency_access.key_encrypted = Some(key);
|
emergency_access.key_encrypted = Some(key);
|
||||||
emergency_access.email = None;
|
emergency_access.email = None;
|
||||||
|
|
||||||
emergency_access.save(&conn)?;
|
emergency_access.save(&mut conn).await?;
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_emergency_access_invite_confirmed(&grantee_user.email, &grantor_user.name)?;
|
mail::send_emergency_access_invite_confirmed(&grantee_user.email, &grantor_user.name).await?;
|
||||||
}
|
}
|
||||||
Ok(Json(emergency_access.to_json()))
|
Ok(Json(emergency_access.to_json()))
|
||||||
} else {
|
} else {
|
||||||
@@ -433,22 +452,22 @@ fn confirm_emergency_access(
|
|||||||
// region access emergency access
|
// region access emergency access
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/initiate")]
|
#[post("/emergency-access/<emer_id>/initiate")]
|
||||||
fn initiate_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn initiate_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let initiating_user = headers.user;
|
let initiating_user = headers.user;
|
||||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) {
|
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||||
Some(emer) => emer,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
|
|
||||||
if emergency_access.status != EmergencyAccessStatus::Confirmed as i32
|
if emergency_access.status != EmergencyAccessStatus::Confirmed as i32
|
||||||
|| emergency_access.grantee_uuid != Some(initiating_user.uuid.clone())
|
|| emergency_access.grantee_uuid != Some(initiating_user.uuid)
|
||||||
{
|
{
|
||||||
err!("Emergency access not valid.")
|
err!("Emergency access not valid.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) {
|
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Grantor user not found."),
|
None => err!("Grantor user not found."),
|
||||||
};
|
};
|
||||||
@@ -458,51 +477,51 @@ fn initiate_emergency_access(emer_id: String, headers: Headers, conn: DbConn) ->
|
|||||||
emergency_access.updated_at = now;
|
emergency_access.updated_at = now;
|
||||||
emergency_access.recovery_initiated_at = Some(now);
|
emergency_access.recovery_initiated_at = Some(now);
|
||||||
emergency_access.last_notification_at = Some(now);
|
emergency_access.last_notification_at = Some(now);
|
||||||
emergency_access.save(&conn)?;
|
emergency_access.save(&mut conn).await?;
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_emergency_access_recovery_initiated(
|
mail::send_emergency_access_recovery_initiated(
|
||||||
&grantor_user.email,
|
&grantor_user.email,
|
||||||
&initiating_user.name,
|
&initiating_user.name,
|
||||||
emergency_access.get_type_as_str(),
|
emergency_access.get_type_as_str(),
|
||||||
&emergency_access.wait_time_days.clone().to_string(),
|
&emergency_access.wait_time_days,
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
Ok(Json(emergency_access.to_json()))
|
Ok(Json(emergency_access.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/approve")]
|
#[post("/emergency-access/<emer_id>/approve")]
|
||||||
fn approve_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn approve_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let approving_user = headers.user;
|
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) {
|
|
||||||
Some(emer) => emer,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
|
|
||||||
if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32
|
if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32
|
||||||
|| emergency_access.grantor_uuid != approving_user.uuid
|
|| emergency_access.grantor_uuid != headers.user.uuid
|
||||||
{
|
{
|
||||||
err!("Emergency access not valid.")
|
err!("Emergency access not valid.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let grantor_user = match User::find_by_uuid(&approving_user.uuid, &conn) {
|
let grantor_user = match User::find_by_uuid(&headers.user.uuid, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Grantor user not found."),
|
None => err!("Grantor user not found."),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
|
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
|
||||||
let grantee_user = match User::find_by_uuid(grantee_uuid, &conn) {
|
let grantee_user = match User::find_by_uuid(grantee_uuid, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Grantee user not found."),
|
None => err!("Grantee user not found."),
|
||||||
};
|
};
|
||||||
|
|
||||||
emergency_access.status = EmergencyAccessStatus::RecoveryApproved as i32;
|
emergency_access.status = EmergencyAccessStatus::RecoveryApproved as i32;
|
||||||
emergency_access.save(&conn)?;
|
emergency_access.save(&mut conn).await?;
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name)?;
|
mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name).await?;
|
||||||
}
|
}
|
||||||
Ok(Json(emergency_access.to_json()))
|
Ok(Json(emergency_access.to_json()))
|
||||||
} else {
|
} else {
|
||||||
@@ -511,38 +530,37 @@ fn approve_emergency_access(emer_id: String, headers: Headers, conn: DbConn) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/reject")]
|
#[post("/emergency-access/<emer_id>/reject")]
|
||||||
fn reject_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn reject_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let rejecting_user = headers.user;
|
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) {
|
|
||||||
Some(emer) => emer,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32
|
if (emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32
|
||||||
&& emergency_access.status != EmergencyAccessStatus::RecoveryApproved as i32)
|
&& emergency_access.status != EmergencyAccessStatus::RecoveryApproved as i32)
|
||||||
|| emergency_access.grantor_uuid != rejecting_user.uuid
|
|| emergency_access.grantor_uuid != headers.user.uuid
|
||||||
{
|
{
|
||||||
err!("Emergency access not valid.")
|
err!("Emergency access not valid.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let grantor_user = match User::find_by_uuid(&rejecting_user.uuid, &conn) {
|
let grantor_user = match User::find_by_uuid(&headers.user.uuid, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Grantor user not found."),
|
None => err!("Grantor user not found."),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
|
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
|
||||||
let grantee_user = match User::find_by_uuid(grantee_uuid, &conn) {
|
let grantee_user = match User::find_by_uuid(grantee_uuid, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Grantee user not found."),
|
None => err!("Grantee user not found."),
|
||||||
};
|
};
|
||||||
|
|
||||||
emergency_access.status = EmergencyAccessStatus::Confirmed as i32;
|
emergency_access.status = EmergencyAccessStatus::Confirmed as i32;
|
||||||
emergency_access.save(&conn)?;
|
emergency_access.save(&mut conn).await?;
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_emergency_access_recovery_rejected(&grantee_user.email, &grantor_user.name)?;
|
mail::send_emergency_access_recovery_rejected(&grantee_user.email, &grantor_user.name).await?;
|
||||||
}
|
}
|
||||||
Ok(Json(emergency_access.to_json()))
|
Ok(Json(emergency_access.to_json()))
|
||||||
} else {
|
} else {
|
||||||
@@ -555,24 +573,34 @@ fn reject_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> J
|
|||||||
// region action
|
// region action
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/view")]
|
#[post("/emergency-access/<emer_id>/view")]
|
||||||
fn view_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn view_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let requesting_user = headers.user;
|
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||||
let host = headers.host;
|
|
||||||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) {
|
|
||||||
Some(emer) => emer,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !is_valid_request(&emergency_access, requesting_user.uuid, EmergencyAccessType::View) {
|
if !is_valid_request(&emergency_access, headers.user.uuid, EmergencyAccessType::View) {
|
||||||
err!("Emergency access not valid.")
|
err!("Emergency access not valid.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let ciphers = Cipher::find_owned_by_user(&emergency_access.grantor_uuid, &conn);
|
let ciphers = Cipher::find_owned_by_user(&emergency_access.grantor_uuid, &mut conn).await;
|
||||||
|
let cipher_sync_data = CipherSyncData::new(&emergency_access.grantor_uuid, CipherSyncType::User, &mut conn).await;
|
||||||
|
|
||||||
let ciphers_json: Vec<Value> =
|
let mut ciphers_json = Vec::with_capacity(ciphers.len());
|
||||||
ciphers.iter().map(|c| c.to_json(&host, &emergency_access.grantor_uuid, &conn)).collect();
|
for c in ciphers {
|
||||||
|
ciphers_json.push(
|
||||||
|
c.to_json(
|
||||||
|
&headers.host,
|
||||||
|
&emergency_access.grantor_uuid,
|
||||||
|
Some(&cipher_sync_data),
|
||||||
|
CipherSyncType::User,
|
||||||
|
&mut conn,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Ciphers": ciphers_json,
|
"Ciphers": ciphers_json,
|
||||||
@@ -582,11 +610,11 @@ fn view_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> Jso
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/takeover")]
|
#[post("/emergency-access/<emer_id>/takeover")]
|
||||||
fn takeover_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn takeover_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let requesting_user = headers.user;
|
let requesting_user = headers.user;
|
||||||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) {
|
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||||
Some(emer) => emer,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
@@ -595,20 +623,24 @@ fn takeover_emergency_access(emer_id: String, headers: Headers, conn: DbConn) ->
|
|||||||
err!("Emergency access not valid.")
|
err!("Emergency access not valid.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) {
|
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Grantor user not found."),
|
None => err!("Grantor user not found."),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({
|
let result = json!({
|
||||||
"Kdf": grantor_user.client_kdf_type,
|
"Kdf": grantor_user.client_kdf_type,
|
||||||
"KdfIterations": grantor_user.client_kdf_iter,
|
"KdfIterations": grantor_user.client_kdf_iter,
|
||||||
|
"KdfMemory": grantor_user.client_kdf_memory,
|
||||||
|
"KdfParallelism": grantor_user.client_kdf_parallelism,
|
||||||
"KeyEncrypted": &emergency_access.key_encrypted,
|
"KeyEncrypted": &emergency_access.key_encrypted,
|
||||||
"Object": "emergencyAccessTakeover",
|
"Object": "emergencyAccessTakeover",
|
||||||
})))
|
});
|
||||||
|
|
||||||
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct EmergencyAccessPasswordData {
|
struct EmergencyAccessPasswordData {
|
||||||
NewMasterPasswordHash: String,
|
NewMasterPasswordHash: String,
|
||||||
@@ -616,20 +648,20 @@ struct EmergencyAccessPasswordData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/password", data = "<data>")]
|
#[post("/emergency-access/<emer_id>/password", data = "<data>")]
|
||||||
fn password_emergency_access(
|
async fn password_emergency_access(
|
||||||
emer_id: String,
|
emer_id: String,
|
||||||
data: JsonUpcase<EmergencyAccessPasswordData>,
|
data: JsonUpcase<EmergencyAccessPasswordData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let data: EmergencyAccessPasswordData = data.into_inner().data;
|
let data: EmergencyAccessPasswordData = data.into_inner().data;
|
||||||
let new_master_password_hash = &data.NewMasterPasswordHash;
|
let new_master_password_hash = &data.NewMasterPasswordHash;
|
||||||
let key = data.Key;
|
//let key = &data.Key;
|
||||||
|
|
||||||
let requesting_user = headers.user;
|
let requesting_user = headers.user;
|
||||||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) {
|
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||||
Some(emer) => emer,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
@@ -638,26 +670,22 @@ fn password_emergency_access(
|
|||||||
err!("Emergency access not valid.")
|
err!("Emergency access not valid.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) {
|
let mut grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Grantor user not found."),
|
None => err!("Grantor user not found."),
|
||||||
};
|
};
|
||||||
|
|
||||||
// change grantor_user password
|
// change grantor_user password
|
||||||
grantor_user.set_password(new_master_password_hash, None);
|
grantor_user.set_password(new_master_password_hash, Some(data.Key), true, None);
|
||||||
grantor_user.akey = key;
|
grantor_user.save(&mut conn).await?;
|
||||||
grantor_user.save(&conn)?;
|
|
||||||
|
|
||||||
// Disable TwoFactor providers since they will otherwise block logins
|
// Disable TwoFactor providers since they will otherwise block logins
|
||||||
TwoFactor::delete_all_by_user(&grantor_user.uuid, &conn)?;
|
TwoFactor::delete_all_by_user(&grantor_user.uuid, &mut conn).await?;
|
||||||
|
|
||||||
// Removing owner, check that there are at least another owner
|
|
||||||
let user_org_grantor = UserOrganization::find_any_state_by_user(&grantor_user.uuid, &conn);
|
|
||||||
|
|
||||||
// Remove grantor from all organisations unless Owner
|
// Remove grantor from all organisations unless Owner
|
||||||
for user_org in user_org_grantor {
|
for user_org in UserOrganization::find_any_state_by_user(&grantor_user.uuid, &mut conn).await {
|
||||||
if user_org.atype != UserOrgType::Owner as i32 {
|
if user_org.atype != UserOrgType::Owner as i32 {
|
||||||
user_org.delete(&conn)?;
|
user_org.delete(&mut conn).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -666,9 +694,9 @@ fn password_emergency_access(
|
|||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
#[get("/emergency-access/<emer_id>/policies")]
|
#[get("/emergency-access/<emer_id>/policies")]
|
||||||
fn policies_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn policies_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let requesting_user = headers.user;
|
let requesting_user = headers.user;
|
||||||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) {
|
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||||
Some(emer) => emer,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
@@ -677,13 +705,13 @@ fn policies_emergency_access(emer_id: String, headers: Headers, conn: DbConn) ->
|
|||||||
err!("Emergency access not valid.")
|
err!("Emergency access not valid.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) {
|
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Grantor user not found."),
|
None => err!("Grantor user not found."),
|
||||||
};
|
};
|
||||||
|
|
||||||
let policies = OrgPolicy::find_confirmed_by_user(&grantor_user.uuid, &conn);
|
let policies = OrgPolicy::find_confirmed_by_user(&grantor_user.uuid, &mut conn);
|
||||||
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
let policies_json: Vec<Value> = policies.await.iter().map(OrgPolicy::to_json).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Data": policies_json,
|
"Data": policies_json,
|
||||||
@@ -709,44 +737,52 @@ fn check_emergency_access_allowed() -> EmptyResult {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn emergency_request_timeout_job(pool: DbPool) {
|
pub async fn emergency_request_timeout_job(pool: DbPool) {
|
||||||
debug!("Start emergency_request_timeout_job");
|
debug!("Start emergency_request_timeout_job");
|
||||||
if !CONFIG.emergency_access_allowed() {
|
if !CONFIG.emergency_access_allowed() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(conn) = pool.get() {
|
if let Ok(mut conn) = pool.get().await {
|
||||||
let emergency_access_list = EmergencyAccess::find_all_recoveries(&conn);
|
let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&mut conn).await;
|
||||||
|
|
||||||
if emergency_access_list.is_empty() {
|
if emergency_access_list.is_empty() {
|
||||||
debug!("No emergency request timeout to approve");
|
debug!("No emergency request timeout to approve");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
for mut emer in emergency_access_list {
|
for mut emer in emergency_access_list {
|
||||||
if emer.recovery_initiated_at.is_some()
|
// The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None)
|
||||||
&& Utc::now().naive_utc()
|
let recovery_allowed_at =
|
||||||
>= emer.recovery_initiated_at.unwrap() + Duration::days(emer.wait_time_days as i64)
|
emer.recovery_initiated_at.unwrap() + Duration::days(i64::from(emer.wait_time_days));
|
||||||
{
|
if recovery_allowed_at.le(&now) {
|
||||||
emer.status = EmergencyAccessStatus::RecoveryApproved as i32;
|
// Only update the access status
|
||||||
emer.save(&conn).expect("Cannot save emergency access on job");
|
// Updating the whole record could cause issues when the emergency_notification_reminder_job is also active
|
||||||
|
emer.update_access_status_and_save(EmergencyAccessStatus::RecoveryApproved as i32, &now, &mut conn)
|
||||||
|
.await
|
||||||
|
.expect("Unable to update emergency access status");
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
// get grantor user to send Accepted email
|
// get grantor user to send Accepted email
|
||||||
let grantor_user = User::find_by_uuid(&emer.grantor_uuid, &conn).expect("Grantor user not found.");
|
let grantor_user =
|
||||||
|
User::find_by_uuid(&emer.grantor_uuid, &mut conn).await.expect("Grantor user not found");
|
||||||
|
|
||||||
// get grantee user to send Accepted email
|
// get grantee user to send Accepted email
|
||||||
let grantee_user =
|
let grantee_user =
|
||||||
User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid."), &conn)
|
User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &mut conn)
|
||||||
.expect("Grantee user not found.");
|
.await
|
||||||
|
.expect("Grantee user not found");
|
||||||
|
|
||||||
mail::send_emergency_access_recovery_timed_out(
|
mail::send_emergency_access_recovery_timed_out(
|
||||||
&grantor_user.email,
|
&grantor_user.email,
|
||||||
&grantee_user.name.clone(),
|
&grantee_user.name,
|
||||||
emer.get_type_as_str(),
|
emer.get_type_as_str(),
|
||||||
)
|
)
|
||||||
|
.await
|
||||||
.expect("Error on sending email");
|
.expect("Error on sending email");
|
||||||
|
|
||||||
mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name.clone())
|
mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name)
|
||||||
|
.await
|
||||||
.expect("Error on sending email");
|
.expect("Error on sending email");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -756,44 +792,56 @@ pub fn emergency_request_timeout_job(pool: DbPool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn emergency_notification_reminder_job(pool: DbPool) {
|
pub async fn emergency_notification_reminder_job(pool: DbPool) {
|
||||||
debug!("Start emergency_notification_reminder_job");
|
debug!("Start emergency_notification_reminder_job");
|
||||||
if !CONFIG.emergency_access_allowed() {
|
if !CONFIG.emergency_access_allowed() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(conn) = pool.get() {
|
if let Ok(mut conn) = pool.get().await {
|
||||||
let emergency_access_list = EmergencyAccess::find_all_recoveries(&conn);
|
let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&mut conn).await;
|
||||||
|
|
||||||
if emergency_access_list.is_empty() {
|
if emergency_access_list.is_empty() {
|
||||||
debug!("No emergency request reminder notification to send");
|
debug!("No emergency request reminder notification to send");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
for mut emer in emergency_access_list {
|
for mut emer in emergency_access_list {
|
||||||
if (emer.recovery_initiated_at.is_some()
|
// The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None)
|
||||||
&& Utc::now().naive_utc()
|
// Calculate the day before the recovery will become active
|
||||||
>= emer.recovery_initiated_at.unwrap() + Duration::days((emer.wait_time_days as i64) - 1))
|
let final_recovery_reminder_at =
|
||||||
&& (emer.last_notification_at.is_none()
|
emer.recovery_initiated_at.unwrap() + Duration::days(i64::from(emer.wait_time_days - 1));
|
||||||
|| (emer.last_notification_at.is_some()
|
// Calculate if a day has passed since the previous notification, else no notification has been sent before
|
||||||
&& Utc::now().naive_utc() >= emer.last_notification_at.unwrap() + Duration::days(1)))
|
let next_recovery_reminder_at = if let Some(last_notification_at) = emer.last_notification_at {
|
||||||
{
|
last_notification_at + Duration::days(1)
|
||||||
emer.save(&conn).expect("Cannot save emergency access on job");
|
} else {
|
||||||
|
now
|
||||||
|
};
|
||||||
|
if final_recovery_reminder_at.le(&now) && next_recovery_reminder_at.le(&now) {
|
||||||
|
// Only update the last notification date
|
||||||
|
// Updating the whole record could cause issues when the emergency_request_timeout_job is also active
|
||||||
|
emer.update_last_notification_date_and_save(&now, &mut conn)
|
||||||
|
.await
|
||||||
|
.expect("Unable to update emergency access notification date");
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
// get grantor user to send Accepted email
|
// get grantor user to send Accepted email
|
||||||
let grantor_user = User::find_by_uuid(&emer.grantor_uuid, &conn).expect("Grantor user not found.");
|
let grantor_user =
|
||||||
|
User::find_by_uuid(&emer.grantor_uuid, &mut conn).await.expect("Grantor user not found");
|
||||||
|
|
||||||
// get grantee user to send Accepted email
|
// get grantee user to send Accepted email
|
||||||
let grantee_user =
|
let grantee_user =
|
||||||
User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid."), &conn)
|
User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &mut conn)
|
||||||
.expect("Grantee user not found.");
|
.await
|
||||||
|
.expect("Grantee user not found");
|
||||||
|
|
||||||
mail::send_emergency_access_recovery_reminder(
|
mail::send_emergency_access_recovery_reminder(
|
||||||
&grantor_user.email,
|
&grantor_user.email,
|
||||||
&grantee_user.name.clone(),
|
&grantee_user.name,
|
||||||
emer.get_type_as_str(),
|
emer.get_type_as_str(),
|
||||||
&emer.wait_time_days.to_string(), // TODO(jjlin): This should be the number of days left.
|
"1", // This notification is only triggered one day before the activation
|
||||||
)
|
)
|
||||||
|
.await
|
||||||
.expect("Error on sending email");
|
.expect("Error on sending email");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
336
src/api/core/events.rs
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use rocket::{form::FromForm, serde::json::Json, Route};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::{EmptyResult, JsonResult, JsonUpcaseVec},
|
||||||
|
auth::{AdminHeaders, Headers},
|
||||||
|
db::{
|
||||||
|
models::{Cipher, Event, UserOrganization},
|
||||||
|
DbConn, DbPool,
|
||||||
|
},
|
||||||
|
util::parse_date,
|
||||||
|
CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// ###############################################################################################################
|
||||||
|
/// /api routes
|
||||||
|
pub fn routes() -> Vec<Route> {
|
||||||
|
routes![get_org_events, get_cipher_events, get_user_events,]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromForm)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct EventRange {
|
||||||
|
start: String,
|
||||||
|
end: String,
|
||||||
|
#[field(name = "continuationToken")]
|
||||||
|
continuation_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upstream: https://github.com/bitwarden/server/blob/9ecf69d9cabce732cf2c57976dd9afa5728578fb/src/Api/Controllers/EventsController.cs#LL84C35-L84C41
|
||||||
|
#[get("/organizations/<org_id>/events?<data..>")]
|
||||||
|
async fn get_org_events(org_id: String, data: EventRange, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
||||||
|
// Return an empty vec when we org events are disabled.
|
||||||
|
// This prevents client errors
|
||||||
|
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
||||||
|
Vec::with_capacity(0)
|
||||||
|
} else {
|
||||||
|
let start_date = parse_date(&data.start);
|
||||||
|
let end_date = if let Some(before_date) = &data.continuation_token {
|
||||||
|
parse_date(before_date)
|
||||||
|
} else {
|
||||||
|
parse_date(&data.end)
|
||||||
|
};
|
||||||
|
|
||||||
|
Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &mut conn)
|
||||||
|
.await
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.to_json())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"Data": events_json,
|
||||||
|
"Object": "list",
|
||||||
|
"ContinuationToken": get_continuation_token(&events_json),
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/ciphers/<cipher_id>/events?<data..>")]
|
||||||
|
async fn get_cipher_events(cipher_id: String, data: EventRange, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
|
// Return an empty vec when we org events are disabled.
|
||||||
|
// This prevents client errors
|
||||||
|
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
||||||
|
Vec::with_capacity(0)
|
||||||
|
} else {
|
||||||
|
let mut events_json = Vec::with_capacity(0);
|
||||||
|
if UserOrganization::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &mut conn).await {
|
||||||
|
let start_date = parse_date(&data.start);
|
||||||
|
let end_date = if let Some(before_date) = &data.continuation_token {
|
||||||
|
parse_date(before_date)
|
||||||
|
} else {
|
||||||
|
parse_date(&data.end)
|
||||||
|
};
|
||||||
|
|
||||||
|
events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &mut conn)
|
||||||
|
.await
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.to_json())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
events_json
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"Data": events_json,
|
||||||
|
"Object": "list",
|
||||||
|
"ContinuationToken": get_continuation_token(&events_json),
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/organizations/<org_id>/users/<user_org_id>/events?<data..>")]
|
||||||
|
async fn get_user_events(
|
||||||
|
org_id: String,
|
||||||
|
user_org_id: String,
|
||||||
|
data: EventRange,
|
||||||
|
_headers: AdminHeaders,
|
||||||
|
mut conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
|
// Return an empty vec when we org events are disabled.
|
||||||
|
// This prevents client errors
|
||||||
|
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
||||||
|
Vec::with_capacity(0)
|
||||||
|
} else {
|
||||||
|
let start_date = parse_date(&data.start);
|
||||||
|
let end_date = if let Some(before_date) = &data.continuation_token {
|
||||||
|
parse_date(before_date)
|
||||||
|
} else {
|
||||||
|
parse_date(&data.end)
|
||||||
|
};
|
||||||
|
|
||||||
|
Event::find_by_org_and_user_org(&org_id, &user_org_id, &start_date, &end_date, &mut conn)
|
||||||
|
.await
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.to_json())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"Data": events_json,
|
||||||
|
"Object": "list",
|
||||||
|
"ContinuationToken": get_continuation_token(&events_json),
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_continuation_token(events_json: &Vec<Value>) -> Option<&str> {
|
||||||
|
// When the length of the vec equals the max page_size there probably is more data
|
||||||
|
// When it is less, then all events are loaded.
|
||||||
|
if events_json.len() as i64 == Event::PAGE_SIZE {
|
||||||
|
if let Some(last_event) = events_json.last() {
|
||||||
|
last_event["date"].as_str()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ###############################################################################################################
|
||||||
|
/// /events routes
|
||||||
|
pub fn main_routes() -> Vec<Route> {
|
||||||
|
routes![post_events_collect,]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct EventCollection {
|
||||||
|
// Mandatory
|
||||||
|
Type: i32,
|
||||||
|
Date: String,
|
||||||
|
|
||||||
|
// Optional
|
||||||
|
CipherId: Option<String>,
|
||||||
|
OrganizationId: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upstream:
|
||||||
|
// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Events/Controllers/CollectController.cs
|
||||||
|
// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs
|
||||||
|
#[post("/collect", format = "application/json", data = "<data>")]
|
||||||
|
async fn post_events_collect(data: JsonUpcaseVec<EventCollection>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||||
|
if !CONFIG.org_events_enabled() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
for event in data.iter().map(|d| &d.data) {
|
||||||
|
let event_date = parse_date(&event.Date);
|
||||||
|
match event.Type {
|
||||||
|
1000..=1099 => {
|
||||||
|
_log_user_event(
|
||||||
|
event.Type,
|
||||||
|
&headers.user.uuid,
|
||||||
|
headers.device.atype,
|
||||||
|
Some(event_date),
|
||||||
|
&headers.ip.ip,
|
||||||
|
&mut conn,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
1600..=1699 => {
|
||||||
|
if let Some(org_uuid) = &event.OrganizationId {
|
||||||
|
_log_event(
|
||||||
|
event.Type,
|
||||||
|
org_uuid,
|
||||||
|
String::from(org_uuid),
|
||||||
|
&headers.user.uuid,
|
||||||
|
headers.device.atype,
|
||||||
|
Some(event_date),
|
||||||
|
&headers.ip.ip,
|
||||||
|
&mut conn,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(cipher_uuid) = &event.CipherId {
|
||||||
|
if let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &mut conn).await {
|
||||||
|
if let Some(org_uuid) = cipher.organization_uuid {
|
||||||
|
_log_event(
|
||||||
|
event.Type,
|
||||||
|
cipher_uuid,
|
||||||
|
org_uuid,
|
||||||
|
&headers.user.uuid,
|
||||||
|
headers.device.atype,
|
||||||
|
Some(event_date),
|
||||||
|
&headers.ip.ip,
|
||||||
|
&mut conn,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn log_user_event(event_type: i32, user_uuid: &str, device_type: i32, ip: &IpAddr, conn: &mut DbConn) {
|
||||||
|
if !CONFIG.org_events_enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_log_user_event(event_type, user_uuid, device_type, None, ip, conn).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn _log_user_event(
|
||||||
|
event_type: i32,
|
||||||
|
user_uuid: &str,
|
||||||
|
device_type: i32,
|
||||||
|
event_date: Option<NaiveDateTime>,
|
||||||
|
ip: &IpAddr,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) {
|
||||||
|
let orgs = UserOrganization::get_org_uuid_by_user(user_uuid, conn).await;
|
||||||
|
let mut events: Vec<Event> = Vec::with_capacity(orgs.len() + 1); // We need an event per org and one without an org
|
||||||
|
|
||||||
|
// Upstream saves the event also without any org_uuid.
|
||||||
|
let mut event = Event::new(event_type, event_date);
|
||||||
|
event.user_uuid = Some(String::from(user_uuid));
|
||||||
|
event.act_user_uuid = Some(String::from(user_uuid));
|
||||||
|
event.device_type = Some(device_type);
|
||||||
|
event.ip_address = Some(ip.to_string());
|
||||||
|
events.push(event);
|
||||||
|
|
||||||
|
// For each org a user is a member of store these events per org
|
||||||
|
for org_uuid in orgs {
|
||||||
|
let mut event = Event::new(event_type, event_date);
|
||||||
|
event.user_uuid = Some(String::from(user_uuid));
|
||||||
|
event.org_uuid = Some(org_uuid);
|
||||||
|
event.act_user_uuid = Some(String::from(user_uuid));
|
||||||
|
event.device_type = Some(device_type);
|
||||||
|
event.ip_address = Some(ip.to_string());
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
Event::save_user_event(events, conn).await.unwrap_or(());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn log_event(
|
||||||
|
event_type: i32,
|
||||||
|
source_uuid: &str,
|
||||||
|
org_uuid: String,
|
||||||
|
act_user_uuid: String,
|
||||||
|
device_type: i32,
|
||||||
|
ip: &IpAddr,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) {
|
||||||
|
if !CONFIG.org_events_enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_log_event(event_type, source_uuid, org_uuid, &act_user_uuid, device_type, None, ip, conn).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn _log_event(
|
||||||
|
event_type: i32,
|
||||||
|
source_uuid: &str,
|
||||||
|
org_uuid: String,
|
||||||
|
act_user_uuid: &str,
|
||||||
|
device_type: i32,
|
||||||
|
event_date: Option<NaiveDateTime>,
|
||||||
|
ip: &IpAddr,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) {
|
||||||
|
// Create a new empty event
|
||||||
|
let mut event = Event::new(event_type, event_date);
|
||||||
|
match event_type {
|
||||||
|
// 1000..=1099 Are user events, they need to be logged via log_user_event()
|
||||||
|
// Collection Events
|
||||||
|
1100..=1199 => {
|
||||||
|
event.cipher_uuid = Some(String::from(source_uuid));
|
||||||
|
}
|
||||||
|
// Collection Events
|
||||||
|
1300..=1399 => {
|
||||||
|
event.collection_uuid = Some(String::from(source_uuid));
|
||||||
|
}
|
||||||
|
// Group Events
|
||||||
|
1400..=1499 => {
|
||||||
|
event.group_uuid = Some(String::from(source_uuid));
|
||||||
|
}
|
||||||
|
// Org User Events
|
||||||
|
1500..=1599 => {
|
||||||
|
event.org_user_uuid = Some(String::from(source_uuid));
|
||||||
|
}
|
||||||
|
// 1600..=1699 Are organizational events, and they do not need the source_uuid
|
||||||
|
// Policy Events
|
||||||
|
1700..=1799 => {
|
||||||
|
event.policy_uuid = Some(String::from(source_uuid));
|
||||||
|
}
|
||||||
|
// Ignore others
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.org_uuid = Some(org_uuid);
|
||||||
|
event.act_user_uuid = Some(String::from(act_user_uuid));
|
||||||
|
event.device_type = Some(device_type);
|
||||||
|
event.ip_address = Some(ip.to_string());
|
||||||
|
event.save(conn).await.unwrap_or(());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn event_cleanup_job(pool: DbPool) {
|
||||||
|
debug!("Start events cleanup job");
|
||||||
|
if CONFIG.events_days_retain().is_none() {
|
||||||
|
debug!("events_days_retain is not configured, abort");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(mut conn) = pool.get().await {
|
||||||
|
Event::clean_events(&mut conn).await.ok();
|
||||||
|
} else {
|
||||||
|
error!("Failed to get DB connection while trying to cleanup the events table")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use rocket_contrib::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -12,9 +12,8 @@ pub fn routes() -> Vec<rocket::Route> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/folders")]
|
#[get("/folders")]
|
||||||
fn get_folders(headers: Headers, conn: DbConn) -> Json<Value> {
|
async fn get_folders(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
||||||
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
let folders = Folder::find_by_user(&headers.user.uuid, &mut conn).await;
|
||||||
|
|
||||||
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
|
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
|
||||||
|
|
||||||
Json(json!({
|
Json(json!({
|
||||||
@@ -25,8 +24,8 @@ fn get_folders(headers: Headers, conn: DbConn) -> Json<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/folders/<uuid>")]
|
#[get("/folders/<uuid>")]
|
||||||
fn get_folder(uuid: String, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn get_folder(uuid: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let folder = match Folder::find_by_uuid(&uuid, &conn) {
|
let folder = match Folder::find_by_uuid(&uuid, &mut conn).await {
|
||||||
Some(folder) => folder,
|
Some(folder) => folder,
|
||||||
_ => err!("Invalid folder"),
|
_ => err!("Invalid folder"),
|
||||||
};
|
};
|
||||||
@@ -45,27 +44,39 @@ pub struct FolderData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/folders", data = "<data>")]
|
#[post("/folders", data = "<data>")]
|
||||||
fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
async fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||||
let data: FolderData = data.into_inner().data;
|
let data: FolderData = data.into_inner().data;
|
||||||
|
|
||||||
let mut folder = Folder::new(headers.user.uuid, data.Name);
|
let mut folder = Folder::new(headers.user.uuid, data.Name);
|
||||||
|
|
||||||
folder.save(&conn)?;
|
folder.save(&mut conn).await?;
|
||||||
nt.send_folder_update(UpdateType::FolderCreate, &folder);
|
nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.uuid).await;
|
||||||
|
|
||||||
Ok(Json(folder.to_json()))
|
Ok(Json(folder.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/folders/<uuid>", data = "<data>")]
|
#[post("/folders/<uuid>", data = "<data>")]
|
||||||
fn post_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
async fn post_folder(
|
||||||
put_folder(uuid, data, headers, conn, nt)
|
uuid: String,
|
||||||
|
data: JsonUpcase<FolderData>,
|
||||||
|
headers: Headers,
|
||||||
|
conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
put_folder(uuid, data, headers, conn, nt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/folders/<uuid>", data = "<data>")]
|
#[put("/folders/<uuid>", data = "<data>")]
|
||||||
fn put_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
async fn put_folder(
|
||||||
|
uuid: String,
|
||||||
|
data: JsonUpcase<FolderData>,
|
||||||
|
headers: Headers,
|
||||||
|
mut conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
let data: FolderData = data.into_inner().data;
|
let data: FolderData = data.into_inner().data;
|
||||||
|
|
||||||
let mut folder = match Folder::find_by_uuid(&uuid, &conn) {
|
let mut folder = match Folder::find_by_uuid(&uuid, &mut conn).await {
|
||||||
Some(folder) => folder,
|
Some(folder) => folder,
|
||||||
_ => err!("Invalid folder"),
|
_ => err!("Invalid folder"),
|
||||||
};
|
};
|
||||||
@@ -76,20 +87,20 @@ fn put_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn
|
|||||||
|
|
||||||
folder.name = data.Name;
|
folder.name = data.Name;
|
||||||
|
|
||||||
folder.save(&conn)?;
|
folder.save(&mut conn).await?;
|
||||||
nt.send_folder_update(UpdateType::FolderUpdate, &folder);
|
nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.uuid).await;
|
||||||
|
|
||||||
Ok(Json(folder.to_json()))
|
Ok(Json(folder.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/folders/<uuid>/delete")]
|
#[post("/folders/<uuid>/delete")]
|
||||||
fn delete_folder_post(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
async fn delete_folder_post(uuid: String, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||||
delete_folder(uuid, headers, conn, nt)
|
delete_folder(uuid, headers, conn, nt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/folders/<uuid>")]
|
#[delete("/folders/<uuid>")]
|
||||||
fn delete_folder(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
async fn delete_folder(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||||
let folder = match Folder::find_by_uuid(&uuid, &conn) {
|
let folder = match Folder::find_by_uuid(&uuid, &mut conn).await {
|
||||||
Some(folder) => folder,
|
Some(folder) => folder,
|
||||||
_ => err!("Invalid folder"),
|
_ => err!("Invalid folder"),
|
||||||
};
|
};
|
||||||
@@ -99,8 +110,8 @@ fn delete_folder(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> Em
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete the actual folder entry
|
// Delete the actual folder entry
|
||||||
folder.delete(&conn)?;
|
folder.delete(&mut conn).await?;
|
||||||
|
|
||||||
nt.send_folder_update(UpdateType::FolderDelete, &folder);
|
nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device.uuid).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,44 @@
|
|||||||
mod accounts;
|
pub mod accounts;
|
||||||
mod ciphers;
|
mod ciphers;
|
||||||
mod emergency_access;
|
mod emergency_access;
|
||||||
|
mod events;
|
||||||
mod folders;
|
mod folders;
|
||||||
mod organizations;
|
mod organizations;
|
||||||
mod sends;
|
mod sends;
|
||||||
pub mod two_factor;
|
pub mod two_factor;
|
||||||
|
|
||||||
pub use ciphers::purge_trashed_ciphers;
|
pub use ciphers::{purge_trashed_ciphers, CipherData, CipherSyncData, CipherSyncType};
|
||||||
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
|
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
|
||||||
|
pub use events::{event_cleanup_job, log_event, log_user_event};
|
||||||
pub use sends::purge_sends;
|
pub use sends::purge_sends;
|
||||||
pub use two_factor::send_incomplete_2fa_notifications;
|
pub use two_factor::send_incomplete_2fa_notifications;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut mod_routes =
|
let mut device_token_routes = routes![clear_device_token, put_device_token];
|
||||||
routes![clear_device_token, put_device_token, get_eq_domains, post_eq_domains, put_eq_domains, hibp_breach,];
|
let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains];
|
||||||
|
let mut hibp_routes = routes![hibp_breach];
|
||||||
|
let mut meta_routes = routes![alive, now, version, config];
|
||||||
|
|
||||||
let mut routes = Vec::new();
|
let mut routes = Vec::new();
|
||||||
routes.append(&mut accounts::routes());
|
routes.append(&mut accounts::routes());
|
||||||
routes.append(&mut ciphers::routes());
|
routes.append(&mut ciphers::routes());
|
||||||
routes.append(&mut emergency_access::routes());
|
routes.append(&mut emergency_access::routes());
|
||||||
|
routes.append(&mut events::routes());
|
||||||
routes.append(&mut folders::routes());
|
routes.append(&mut folders::routes());
|
||||||
routes.append(&mut organizations::routes());
|
routes.append(&mut organizations::routes());
|
||||||
routes.append(&mut two_factor::routes());
|
routes.append(&mut two_factor::routes());
|
||||||
routes.append(&mut sends::routes());
|
routes.append(&mut sends::routes());
|
||||||
routes.append(&mut mod_routes);
|
routes.append(&mut device_token_routes);
|
||||||
|
routes.append(&mut eq_domains_routes);
|
||||||
|
routes.append(&mut hibp_routes);
|
||||||
|
routes.append(&mut meta_routes);
|
||||||
|
|
||||||
|
routes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn events_routes() -> Vec<Route> {
|
||||||
|
let mut routes = Vec::new();
|
||||||
|
routes.append(&mut events::main_routes());
|
||||||
|
|
||||||
routes
|
routes
|
||||||
}
|
}
|
||||||
@@ -31,12 +46,11 @@ pub fn routes() -> Vec<Route> {
|
|||||||
//
|
//
|
||||||
// Move this somewhere else
|
// Move this somewhere else
|
||||||
//
|
//
|
||||||
use rocket::Route;
|
use rocket::{serde::json::Json, Catcher, Route};
|
||||||
use rocket_contrib::json::Json;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{JsonResult, JsonUpcase},
|
api::{JsonResult, JsonUpcase, Notify, UpdateType},
|
||||||
auth::Headers,
|
auth::Headers,
|
||||||
db::DbConn,
|
db::DbConn,
|
||||||
error::Error,
|
error::Error,
|
||||||
@@ -121,7 +135,12 @@ struct EquivDomainData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/settings/domains", data = "<data>")]
|
#[post("/settings/domains", data = "<data>")]
|
||||||
fn post_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn post_eq_domains(
|
||||||
|
data: JsonUpcase<EquivDomainData>,
|
||||||
|
headers: Headers,
|
||||||
|
mut conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
let data: EquivDomainData = data.into_inner().data;
|
let data: EquivDomainData = data.into_inner().data;
|
||||||
|
|
||||||
let excluded_globals = data.ExcludedGlobalEquivalentDomains.unwrap_or_default();
|
let excluded_globals = data.ExcludedGlobalEquivalentDomains.unwrap_or_default();
|
||||||
@@ -133,34 +152,40 @@ fn post_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: Db
|
|||||||
user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| "[]".to_string());
|
user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| "[]".to_string());
|
||||||
user.equivalent_domains = to_string(&equivalent_domains).unwrap_or_else(|_| "[]".to_string());
|
user.equivalent_domains = to_string(&equivalent_domains).unwrap_or_else(|_| "[]".to_string());
|
||||||
|
|
||||||
user.save(&conn)?;
|
user.save(&mut conn).await?;
|
||||||
|
|
||||||
|
nt.send_user_update(UpdateType::SyncSettings, &user).await;
|
||||||
|
|
||||||
Ok(Json(json!({})))
|
Ok(Json(json!({})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/settings/domains", data = "<data>")]
|
#[put("/settings/domains", data = "<data>")]
|
||||||
fn put_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn put_eq_domains(
|
||||||
post_eq_domains(data, headers, conn)
|
data: JsonUpcase<EquivDomainData>,
|
||||||
|
headers: Headers,
|
||||||
|
conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
post_eq_domains(data, headers, conn, nt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/hibp/breach?<username>")]
|
#[get("/hibp/breach?<username>")]
|
||||||
fn hibp_breach(username: String) -> JsonResult {
|
async fn hibp_breach(username: String) -> JsonResult {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://haveibeenpwned.com/api/v3/breachedaccount/{}?truncateResponse=false&includeUnverified=false",
|
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
|
||||||
username
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
||||||
let hibp_client = get_reqwest_client();
|
let hibp_client = get_reqwest_client();
|
||||||
|
|
||||||
let res = hibp_client.get(&url).header("hibp-api-key", api_key).send()?;
|
let res = hibp_client.get(&url).header("hibp-api-key", api_key).send().await?;
|
||||||
|
|
||||||
// If we get a 404, return a 404, it means no breached accounts
|
// If we get a 404, return a 404, it means no breached accounts
|
||||||
if res.status() == 404 {
|
if res.status() == 404 {
|
||||||
return Err(Error::empty().with_code(404));
|
return Err(Error::empty().with_code(404));
|
||||||
}
|
}
|
||||||
|
|
||||||
let value: Value = res.error_for_status()?.json()?;
|
let value: Value = res.error_for_status()?.json().await?;
|
||||||
Ok(Json(value))
|
Ok(Json(value))
|
||||||
} else {
|
} else {
|
||||||
Ok(Json(json!([{
|
Ok(Json(json!([{
|
||||||
@@ -169,7 +194,7 @@ fn hibp_breach(username: String) -> JsonResult {
|
|||||||
"Domain": "haveibeenpwned.com",
|
"Domain": "haveibeenpwned.com",
|
||||||
"BreachDate": "2019-08-18T00:00:00Z",
|
"BreachDate": "2019-08-18T00:00:00Z",
|
||||||
"AddedDate": "2019-08-18T00:00:00Z",
|
"AddedDate": "2019-08-18T00:00:00Z",
|
||||||
"Description": format!("Go to: <a href=\"https://haveibeenpwned.com/account/{account}\" target=\"_blank\" rel=\"noreferrer\">https://haveibeenpwned.com/account/{account}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noreferrer\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>", account=username),
|
"Description": format!("Go to: <a href=\"https://haveibeenpwned.com/account/{username}\" target=\"_blank\" rel=\"noreferrer\">https://haveibeenpwned.com/account/{username}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noreferrer\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>"),
|
||||||
"LogoPath": "vw_static/hibp.png",
|
"LogoPath": "vw_static/hibp.png",
|
||||||
"PwnCount": 0,
|
"PwnCount": 0,
|
||||||
"DataClasses": [
|
"DataClasses": [
|
||||||
@@ -178,3 +203,55 @@ fn hibp_breach(username: String) -> JsonResult {
|
|||||||
}])))
|
}])))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We use DbConn here to let the alive healthcheck also verify the database connection.
|
||||||
|
#[get("/alive")]
|
||||||
|
fn alive(_conn: DbConn) -> Json<String> {
|
||||||
|
now()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/now")]
|
||||||
|
pub fn now() -> Json<String> {
|
||||||
|
Json(crate::util::format_date(&chrono::Utc::now().naive_utc()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/version")]
|
||||||
|
fn version() -> Json<&'static str> {
|
||||||
|
Json(crate::VERSION.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/config")]
|
||||||
|
fn config() -> Json<Value> {
|
||||||
|
let domain = crate::CONFIG.domain();
|
||||||
|
Json(json!({
|
||||||
|
"version": crate::VERSION,
|
||||||
|
"gitHash": option_env!("GIT_REV"),
|
||||||
|
"server": {
|
||||||
|
"name": "Vaultwarden",
|
||||||
|
"url": "https://github.com/dani-garcia/vaultwarden"
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"vault": domain,
|
||||||
|
"api": format!("{domain}/api"),
|
||||||
|
"identity": format!("{domain}/identity"),
|
||||||
|
"notifications": format!("{domain}/notifications"),
|
||||||
|
"sso": "",
|
||||||
|
},
|
||||||
|
"object": "config",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn catchers() -> Vec<Catcher> {
|
||||||
|
catchers![api_not_found]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[catch(404)]
|
||||||
|
fn api_not_found() -> Json<Value> {
|
||||||
|
Json(json!({
|
||||||
|
"error": {
|
||||||
|
"code": 404,
|
||||||
|
"reason": "Not Found",
|
||||||
|
"description": "The requested resource could not be found."
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
use std::{io::Read, path::Path};
|
use std::path::Path;
|
||||||
|
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use multipart::server::{save::SavedData, Multipart, SaveResult};
|
use rocket::form::Form;
|
||||||
use rocket::{http::ContentType, response::NamedFile, Data};
|
use rocket::fs::NamedFile;
|
||||||
use rocket_contrib::json::Json;
|
use rocket::fs::TempFile;
|
||||||
|
use rocket::serde::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, UpdateType},
|
api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, UpdateType},
|
||||||
auth::{Headers, Host},
|
auth::{ClientIp, Headers, Host},
|
||||||
db::{models::*, DbConn, DbPool},
|
db::{models::*, DbConn, DbPool},
|
||||||
util::SafeString,
|
util::SafeString,
|
||||||
CONFIG,
|
CONFIG,
|
||||||
@@ -16,6 +17,9 @@ use crate::{
|
|||||||
|
|
||||||
const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available";
|
const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available";
|
||||||
|
|
||||||
|
// The max file size allowed by Bitwarden clients and add an extra 5% to avoid issues
|
||||||
|
const SIZE_525_MB: u64 = 550_502_400;
|
||||||
|
|
||||||
pub fn routes() -> Vec<rocket::Route> {
|
pub fn routes() -> Vec<rocket::Route> {
|
||||||
routes![
|
routes![
|
||||||
get_sends,
|
get_sends,
|
||||||
@@ -27,14 +31,16 @@ pub fn routes() -> Vec<rocket::Route> {
|
|||||||
put_send,
|
put_send,
|
||||||
delete_send,
|
delete_send,
|
||||||
put_remove_password,
|
put_remove_password,
|
||||||
download_send
|
download_send,
|
||||||
|
post_send_file_v2,
|
||||||
|
post_send_file_v2_data
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn purge_sends(pool: DbPool) {
|
pub async fn purge_sends(pool: DbPool) {
|
||||||
debug!("Purging sends");
|
debug!("Purging sends");
|
||||||
if let Ok(conn) = pool.get() {
|
if let Ok(mut conn) = pool.get().await {
|
||||||
Send::purge(&conn);
|
Send::purge(&mut conn).await;
|
||||||
} else {
|
} else {
|
||||||
error!("Failed to get DB connection while purging sends")
|
error!("Failed to get DB connection while purging sends")
|
||||||
}
|
}
|
||||||
@@ -57,6 +63,7 @@ struct SendData {
|
|||||||
Notes: Option<String>,
|
Notes: Option<String>,
|
||||||
Text: Option<Value>,
|
Text: Option<Value>,
|
||||||
File: Option<Value>,
|
File: Option<Value>,
|
||||||
|
FileLength: Option<NumberOrString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enforces the `Disable Send` policy. A non-owner/admin user belonging to
|
/// Enforces the `Disable Send` policy. A non-owner/admin user belonging to
|
||||||
@@ -67,10 +74,11 @@ struct SendData {
|
|||||||
///
|
///
|
||||||
/// There is also a Vaultwarden-specific `sends_allowed` config setting that
|
/// There is also a Vaultwarden-specific `sends_allowed` config setting that
|
||||||
/// controls this policy globally.
|
/// controls this policy globally.
|
||||||
fn enforce_disable_send_policy(headers: &Headers, conn: &DbConn) -> EmptyResult {
|
async fn enforce_disable_send_policy(headers: &Headers, conn: &mut DbConn) -> EmptyResult {
|
||||||
let user_uuid = &headers.user.uuid;
|
let user_uuid = &headers.user.uuid;
|
||||||
let policy_type = OrgPolicyType::DisableSend;
|
if !CONFIG.sends_allowed()
|
||||||
if !CONFIG.sends_allowed() || OrgPolicy::is_applicable_to_user(user_uuid, policy_type, conn) {
|
|| OrgPolicy::is_applicable_to_user(user_uuid, OrgPolicyType::DisableSend, None, conn).await
|
||||||
|
{
|
||||||
err!("Due to an Enterprise Policy, you are only able to delete an existing Send.")
|
err!("Due to an Enterprise Policy, you are only able to delete an existing Send.")
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -82,10 +90,10 @@ fn enforce_disable_send_policy(headers: &Headers, conn: &DbConn) -> EmptyResult
|
|||||||
/// but is allowed to remove this option from an existing Send.
|
/// but is allowed to remove this option from an existing Send.
|
||||||
///
|
///
|
||||||
/// Ref: https://bitwarden.com/help/article/policies/#send-options
|
/// Ref: https://bitwarden.com/help/article/policies/#send-options
|
||||||
fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, conn: &DbConn) -> EmptyResult {
|
async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, conn: &mut DbConn) -> EmptyResult {
|
||||||
let user_uuid = &headers.user.uuid;
|
let user_uuid = &headers.user.uuid;
|
||||||
let hide_email = data.HideEmail.unwrap_or(false);
|
let hide_email = data.HideEmail.unwrap_or(false);
|
||||||
if hide_email && OrgPolicy::is_hide_email_disabled(user_uuid, conn) {
|
if hide_email && OrgPolicy::is_hide_email_disabled(user_uuid, conn).await {
|
||||||
err!(
|
err!(
|
||||||
"Due to an Enterprise Policy, you are not allowed to hide your email address \
|
"Due to an Enterprise Policy, you are not allowed to hide your email address \
|
||||||
from recipients when creating or editing a Send."
|
from recipients when creating or editing a Send."
|
||||||
@@ -134,9 +142,9 @@ fn create_send(data: SendData, user_uuid: String) -> ApiResult<Send> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/sends")]
|
#[get("/sends")]
|
||||||
fn get_sends(headers: Headers, conn: DbConn) -> Json<Value> {
|
async fn get_sends(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
||||||
let sends = Send::find_by_user(&headers.user.uuid, &conn);
|
let sends = Send::find_by_user(&headers.user.uuid, &mut conn);
|
||||||
let sends_json: Vec<Value> = sends.iter().map(|s| s.to_json()).collect();
|
let sends_json: Vec<Value> = sends.await.iter().map(|s| s.to_json()).collect();
|
||||||
|
|
||||||
Json(json!({
|
Json(json!({
|
||||||
"Data": sends_json,
|
"Data": sends_json,
|
||||||
@@ -146,8 +154,8 @@ fn get_sends(headers: Headers, conn: DbConn) -> Json<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/sends/<uuid>")]
|
#[get("/sends/<uuid>")]
|
||||||
fn get_send(uuid: String, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn get_send(uuid: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let send = match Send::find_by_uuid(&uuid, &conn) {
|
let send = match Send::find_by_uuid(&uuid, &mut conn).await {
|
||||||
Some(send) => send,
|
Some(send) => send,
|
||||||
None => err!("Send not found"),
|
None => err!("Send not found"),
|
||||||
};
|
};
|
||||||
@@ -160,50 +168,53 @@ fn get_send(uuid: String, headers: Headers, conn: DbConn) -> JsonResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/sends", data = "<data>")]
|
#[post("/sends", data = "<data>")]
|
||||||
fn post_send(data: JsonUpcase<SendData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
async fn post_send(data: JsonUpcase<SendData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||||
enforce_disable_send_policy(&headers, &conn)?;
|
enforce_disable_send_policy(&headers, &mut conn).await?;
|
||||||
|
|
||||||
let data: SendData = data.into_inner().data;
|
let data: SendData = data.into_inner().data;
|
||||||
enforce_disable_hide_email_policy(&data, &headers, &conn)?;
|
enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?;
|
||||||
|
|
||||||
if data.Type == SendType::File as i32 {
|
if data.Type == SendType::File as i32 {
|
||||||
err!("File sends should use /api/sends/file")
|
err!("File sends should use /api/sends/file")
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut send = create_send(data, headers.user.uuid)?;
|
let mut send = create_send(data, headers.user.uuid)?;
|
||||||
send.save(&conn)?;
|
send.save(&mut conn).await?;
|
||||||
nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&conn));
|
nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await).await;
|
||||||
|
|
||||||
Ok(Json(send.to_json()))
|
Ok(Json(send.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(FromForm)]
|
||||||
|
struct UploadData<'f> {
|
||||||
|
model: Json<crate::util::UpCase<SendData>>,
|
||||||
|
data: TempFile<'f>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromForm)]
|
||||||
|
struct UploadDataV2<'f> {
|
||||||
|
data: TempFile<'f>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads (v2).
|
||||||
|
// This method still exists to support older clients, probably need to remove it sometime.
|
||||||
|
// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L164-L167
|
||||||
#[post("/sends/file", format = "multipart/form-data", data = "<data>")]
|
#[post("/sends/file", format = "multipart/form-data", data = "<data>")]
|
||||||
fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||||
enforce_disable_send_policy(&headers, &conn)?;
|
enforce_disable_send_policy(&headers, &mut conn).await?;
|
||||||
|
|
||||||
let boundary = content_type.params().next().expect("No boundary provided").1;
|
let UploadData {
|
||||||
|
model,
|
||||||
|
mut data,
|
||||||
|
} = data.into_inner();
|
||||||
|
let model = model.into_inner().data;
|
||||||
|
|
||||||
let mut mpart = Multipart::with_body(data.open(), boundary);
|
enforce_disable_hide_email_policy(&model, &headers, &mut conn).await?;
|
||||||
|
|
||||||
// First entry is the SendData JSON
|
|
||||||
let mut model_entry = match mpart.read_entry()? {
|
|
||||||
Some(e) if &*e.headers.name == "model" => e,
|
|
||||||
Some(_) => err!("Invalid entry name"),
|
|
||||||
None => err!("No model entry present"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut buf = String::new();
|
|
||||||
model_entry.data.read_to_string(&mut buf)?;
|
|
||||||
let data = serde_json::from_str::<crate::util::UpCase<SendData>>(&buf)?;
|
|
||||||
enforce_disable_hide_email_policy(&data.data, &headers, &conn)?;
|
|
||||||
|
|
||||||
// Get the file length and add an extra 5% to avoid issues
|
|
||||||
const SIZE_525_MB: u64 = 550_502_400;
|
|
||||||
|
|
||||||
let size_limit = match CONFIG.user_attachment_limit() {
|
let size_limit = match CONFIG.user_attachment_limit() {
|
||||||
Some(0) => err!("File uploads are disabled"),
|
Some(0) => err!("File uploads are disabled"),
|
||||||
Some(limit_kb) => {
|
Some(limit_kb) => {
|
||||||
let left = (limit_kb * 1024) - Attachment::size_by_user(&headers.user.uuid, &conn);
|
let left = (limit_kb * 1024) - Attachment::size_by_user(&headers.user.uuid, &mut conn).await;
|
||||||
if left <= 0 {
|
if left <= 0 {
|
||||||
err!("Attachment storage limit reached! Delete some attachments to free up space")
|
err!("Attachment storage limit reached! Delete some attachments to free up space")
|
||||||
}
|
}
|
||||||
@@ -212,55 +223,126 @@ fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn
|
|||||||
None => SIZE_525_MB,
|
None => SIZE_525_MB,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the Send
|
let mut send = create_send(model, headers.user.uuid)?;
|
||||||
let mut send = create_send(data.data, headers.user.uuid)?;
|
|
||||||
let file_id = crate::crypto::generate_send_id();
|
|
||||||
|
|
||||||
if send.atype != SendType::File as i32 {
|
if send.atype != SendType::File as i32 {
|
||||||
err!("Send content is not a file");
|
err!("Send content is not a file");
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_path = Path::new(&CONFIG.sends_folder()).join(&send.uuid).join(&file_id);
|
let size = data.len();
|
||||||
|
if size > size_limit {
|
||||||
// Read the data entry and save the file
|
err!("Attachment storage limit exceeded with this file");
|
||||||
let mut data_entry = match mpart.read_entry()? {
|
}
|
||||||
Some(e) if &*e.headers.name == "data" => e,
|
|
||||||
Some(_) => err!("Invalid entry name"),
|
let file_id = crate::crypto::generate_send_id();
|
||||||
None => err!("No model entry present"),
|
let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(&send.uuid);
|
||||||
};
|
let file_path = folder_path.join(&file_id);
|
||||||
|
tokio::fs::create_dir_all(&folder_path).await?;
|
||||||
let size = match data_entry.data.save().memory_threshold(0).size_limit(size_limit).with_path(&file_path) {
|
|
||||||
SaveResult::Full(SavedData::File(_, size)) => size as i32,
|
if let Err(_err) = data.persist_to(&file_path).await {
|
||||||
SaveResult::Full(other) => {
|
data.move_copy_to(file_path).await?
|
||||||
std::fs::remove_file(&file_path).ok();
|
|
||||||
err!(format!("Attachment is not a file: {:?}", other));
|
|
||||||
}
|
}
|
||||||
SaveResult::Partial(_, reason) => {
|
|
||||||
std::fs::remove_file(&file_path).ok();
|
|
||||||
err!(format!("Attachment storage limit exceeded with this file: {:?}", reason));
|
|
||||||
}
|
|
||||||
SaveResult::Error(e) => {
|
|
||||||
std::fs::remove_file(&file_path).ok();
|
|
||||||
err!(format!("Error: {:?}", e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set ID and sizes
|
|
||||||
let mut data_value: Value = serde_json::from_str(&send.data)?;
|
let mut data_value: Value = serde_json::from_str(&send.data)?;
|
||||||
if let Some(o) = data_value.as_object_mut() {
|
if let Some(o) = data_value.as_object_mut() {
|
||||||
o.insert(String::from("Id"), Value::String(file_id));
|
o.insert(String::from("Id"), Value::String(file_id));
|
||||||
o.insert(String::from("Size"), Value::Number(size.into()));
|
o.insert(String::from("Size"), Value::Number(size.into()));
|
||||||
o.insert(String::from("SizeName"), Value::String(crate::util::get_display_size(size)));
|
o.insert(String::from("SizeName"), Value::String(crate::util::get_display_size(size as i32)));
|
||||||
}
|
}
|
||||||
send.data = serde_json::to_string(&data_value)?;
|
send.data = serde_json::to_string(&data_value)?;
|
||||||
|
|
||||||
// Save the changes in the database
|
// Save the changes in the database
|
||||||
send.save(&conn)?;
|
send.save(&mut conn).await?;
|
||||||
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&conn));
|
nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await).await;
|
||||||
|
|
||||||
Ok(Json(send.to_json()))
|
Ok(Json(send.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L190
|
||||||
|
#[post("/sends/file/v2", data = "<data>")]
|
||||||
|
async fn post_send_file_v2(data: JsonUpcase<SendData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
|
enforce_disable_send_policy(&headers, &mut conn).await?;
|
||||||
|
|
||||||
|
let data = data.into_inner().data;
|
||||||
|
|
||||||
|
if data.Type != SendType::File as i32 {
|
||||||
|
err!("Send content is not a file");
|
||||||
|
}
|
||||||
|
|
||||||
|
enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?;
|
||||||
|
|
||||||
|
let file_length = match &data.FileLength {
|
||||||
|
Some(m) => Some(m.into_i32()?),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let size_limit = match CONFIG.user_attachment_limit() {
|
||||||
|
Some(0) => err!("File uploads are disabled"),
|
||||||
|
Some(limit_kb) => {
|
||||||
|
let left = (limit_kb * 1024) - Attachment::size_by_user(&headers.user.uuid, &mut conn).await;
|
||||||
|
if left <= 0 {
|
||||||
|
err!("Attachment storage limit reached! Delete some attachments to free up space")
|
||||||
|
}
|
||||||
|
std::cmp::Ord::max(left as u64, SIZE_525_MB)
|
||||||
|
}
|
||||||
|
None => SIZE_525_MB,
|
||||||
|
};
|
||||||
|
|
||||||
|
if file_length.is_some() && file_length.unwrap() as u64 > size_limit {
|
||||||
|
err!("Attachment storage limit exceeded with this file");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut send = create_send(data, headers.user.uuid)?;
|
||||||
|
|
||||||
|
let file_id = crate::crypto::generate_send_id();
|
||||||
|
|
||||||
|
let mut data_value: Value = serde_json::from_str(&send.data)?;
|
||||||
|
if let Some(o) = data_value.as_object_mut() {
|
||||||
|
o.insert(String::from("Id"), Value::String(file_id.clone()));
|
||||||
|
o.insert(String::from("Size"), Value::Number(file_length.unwrap().into()));
|
||||||
|
o.insert(String::from("SizeName"), Value::String(crate::util::get_display_size(file_length.unwrap())));
|
||||||
|
}
|
||||||
|
send.data = serde_json::to_string(&data_value)?;
|
||||||
|
send.save(&mut conn).await?;
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"fileUploadType": 0, // 0 == Direct | 1 == Azure
|
||||||
|
"object": "send-fileUpload",
|
||||||
|
"url": format!("/sends/{}/file/{}", send.uuid, file_id),
|
||||||
|
"sendResponse": send.to_json()
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L243
|
||||||
|
#[post("/sends/<send_uuid>/file/<file_id>", format = "multipart/form-data", data = "<data>")]
|
||||||
|
async fn post_send_file_v2_data(
|
||||||
|
send_uuid: String,
|
||||||
|
file_id: String,
|
||||||
|
data: Form<UploadDataV2<'_>>,
|
||||||
|
headers: Headers,
|
||||||
|
mut conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> EmptyResult {
|
||||||
|
enforce_disable_send_policy(&headers, &mut conn).await?;
|
||||||
|
|
||||||
|
let mut data = data.into_inner();
|
||||||
|
|
||||||
|
if let Some(send) = Send::find_by_uuid(&send_uuid, &mut conn).await {
|
||||||
|
let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(&send_uuid);
|
||||||
|
let file_path = folder_path.join(&file_id);
|
||||||
|
tokio::fs::create_dir_all(&folder_path).await?;
|
||||||
|
|
||||||
|
if let Err(_err) = data.data.persist_to(&file_path).await {
|
||||||
|
data.data.move_copy_to(file_path).await?
|
||||||
|
}
|
||||||
|
|
||||||
|
nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await).await;
|
||||||
|
} else {
|
||||||
|
err!("Send not found. Unable to save the file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub struct SendAccessData {
|
pub struct SendAccessData {
|
||||||
@@ -268,8 +350,14 @@ pub struct SendAccessData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/sends/access/<access_id>", data = "<data>")]
|
#[post("/sends/access/<access_id>", data = "<data>")]
|
||||||
fn post_access(access_id: String, data: JsonUpcase<SendAccessData>, conn: DbConn) -> JsonResult {
|
async fn post_access(
|
||||||
let mut send = match Send::find_by_access_id(&access_id, &conn) {
|
access_id: String,
|
||||||
|
data: JsonUpcase<SendAccessData>,
|
||||||
|
mut conn: DbConn,
|
||||||
|
ip: ClientIp,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
let mut send = match Send::find_by_access_id(&access_id, &mut conn).await {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
||||||
};
|
};
|
||||||
@@ -297,8 +385,8 @@ fn post_access(access_id: String, data: JsonUpcase<SendAccessData>, conn: DbConn
|
|||||||
if send.password_hash.is_some() {
|
if send.password_hash.is_some() {
|
||||||
match data.into_inner().data.Password {
|
match data.into_inner().data.Password {
|
||||||
Some(ref p) if send.check_password(p) => { /* Nothing to do here */ }
|
Some(ref p) if send.check_password(p) => { /* Nothing to do here */ }
|
||||||
Some(_) => err!("Invalid password."),
|
Some(_) => err!("Invalid password", format!("IP: {}.", ip.ip)),
|
||||||
None => err_code!("Password not provided", 401),
|
None => err_code!("Password not provided", format!("IP: {}.", ip.ip), 401),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,20 +395,23 @@ fn post_access(access_id: String, data: JsonUpcase<SendAccessData>, conn: DbConn
|
|||||||
send.access_count += 1;
|
send.access_count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
send.save(&conn)?;
|
send.save(&mut conn).await?;
|
||||||
|
|
||||||
Ok(Json(send.to_json_access(&conn)))
|
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await;
|
||||||
|
|
||||||
|
Ok(Json(send.to_json_access(&mut conn).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")]
|
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")]
|
||||||
fn post_access_file(
|
async fn post_access_file(
|
||||||
send_id: String,
|
send_id: String,
|
||||||
file_id: String,
|
file_id: String,
|
||||||
data: JsonUpcase<SendAccessData>,
|
data: JsonUpcase<SendAccessData>,
|
||||||
host: Host,
|
host: Host,
|
||||||
conn: DbConn,
|
mut conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
let mut send = match Send::find_by_uuid(&send_id, &conn) {
|
let mut send = match Send::find_by_uuid(&send_id, &mut conn).await {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
||||||
};
|
};
|
||||||
@@ -355,7 +446,9 @@ fn post_access_file(
|
|||||||
|
|
||||||
send.access_count += 1;
|
send.access_count += 1;
|
||||||
|
|
||||||
send.save(&conn)?;
|
send.save(&mut conn).await?;
|
||||||
|
|
||||||
|
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await;
|
||||||
|
|
||||||
let token_claims = crate::auth::generate_send_claims(&send_id, &file_id);
|
let token_claims = crate::auth::generate_send_claims(&send_id, &file_id);
|
||||||
let token = crate::auth::encode_jwt(&token_claims);
|
let token = crate::auth::encode_jwt(&token_claims);
|
||||||
@@ -367,23 +460,29 @@ fn post_access_file(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/sends/<send_id>/<file_id>?<t>")]
|
#[get("/sends/<send_id>/<file_id>?<t>")]
|
||||||
fn download_send(send_id: SafeString, file_id: SafeString, t: String) -> Option<NamedFile> {
|
async fn download_send(send_id: SafeString, file_id: SafeString, t: String) -> Option<NamedFile> {
|
||||||
if let Ok(claims) = crate::auth::decode_send(&t) {
|
if let Ok(claims) = crate::auth::decode_send(&t) {
|
||||||
if claims.sub == format!("{}/{}", send_id, file_id) {
|
if claims.sub == format!("{send_id}/{file_id}") {
|
||||||
return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).ok();
|
return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/sends/<id>", data = "<data>")]
|
#[put("/sends/<id>", data = "<data>")]
|
||||||
fn put_send(id: String, data: JsonUpcase<SendData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
async fn put_send(
|
||||||
enforce_disable_send_policy(&headers, &conn)?;
|
id: String,
|
||||||
|
data: JsonUpcase<SendData>,
|
||||||
|
headers: Headers,
|
||||||
|
mut conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
enforce_disable_send_policy(&headers, &mut conn).await?;
|
||||||
|
|
||||||
let data: SendData = data.into_inner().data;
|
let data: SendData = data.into_inner().data;
|
||||||
enforce_disable_hide_email_policy(&data, &headers, &conn)?;
|
enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?;
|
||||||
|
|
||||||
let mut send = match Send::find_by_uuid(&id, &conn) {
|
let mut send = match Send::find_by_uuid(&id, &mut conn).await {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => err!("Send not found"),
|
None => err!("Send not found"),
|
||||||
};
|
};
|
||||||
@@ -430,15 +529,15 @@ fn put_send(id: String, data: JsonUpcase<SendData>, headers: Headers, conn: DbCo
|
|||||||
send.set_password(Some(&password));
|
send.set_password(Some(&password));
|
||||||
}
|
}
|
||||||
|
|
||||||
send.save(&conn)?;
|
send.save(&mut conn).await?;
|
||||||
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&conn));
|
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await;
|
||||||
|
|
||||||
Ok(Json(send.to_json()))
|
Ok(Json(send.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/sends/<id>")]
|
#[delete("/sends/<id>")]
|
||||||
fn delete_send(id: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
async fn delete_send(id: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||||
let send = match Send::find_by_uuid(&id, &conn) {
|
let send = match Send::find_by_uuid(&id, &mut conn).await {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => err!("Send not found"),
|
None => err!("Send not found"),
|
||||||
};
|
};
|
||||||
@@ -447,17 +546,17 @@ fn delete_send(id: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyR
|
|||||||
err!("Send is not owned by user")
|
err!("Send is not owned by user")
|
||||||
}
|
}
|
||||||
|
|
||||||
send.delete(&conn)?;
|
send.delete(&mut conn).await?;
|
||||||
nt.send_send_update(UpdateType::SyncSendDelete, &send, &send.update_users_revision(&conn));
|
nt.send_send_update(UpdateType::SyncSendDelete, &send, &send.update_users_revision(&mut conn).await).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/sends/<id>/remove-password")]
|
#[put("/sends/<id>/remove-password")]
|
||||||
fn put_remove_password(id: String, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
async fn put_remove_password(id: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||||
enforce_disable_send_policy(&headers, &conn)?;
|
enforce_disable_send_policy(&headers, &mut conn).await?;
|
||||||
|
|
||||||
let mut send = match Send::find_by_uuid(&id, &conn) {
|
let mut send = match Send::find_by_uuid(&id, &mut conn).await {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => err!("Send not found"),
|
None => err!("Send not found"),
|
||||||
};
|
};
|
||||||
@@ -467,8 +566,8 @@ fn put_remove_password(id: String, headers: Headers, conn: DbConn, nt: Notify) -
|
|||||||
}
|
}
|
||||||
|
|
||||||
send.set_password(None);
|
send.set_password(None);
|
||||||
send.save(&conn)?;
|
send.save(&mut conn).await?;
|
||||||
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&conn));
|
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await;
|
||||||
|
|
||||||
Ok(Json(send.to_json()))
|
Ok(Json(send.to_json()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
use data_encoding::BASE32;
|
use data_encoding::BASE32;
|
||||||
|
use rocket::serde::json::Json;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData,
|
core::log_user_event, core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase,
|
||||||
|
NumberOrString, PasswordData,
|
||||||
},
|
},
|
||||||
auth::{ClientIp, Headers},
|
auth::{ClientIp, Headers},
|
||||||
crypto,
|
crypto,
|
||||||
db::{
|
db::{
|
||||||
models::{TwoFactor, TwoFactorType},
|
models::{EventType, TwoFactor, TwoFactorType},
|
||||||
DbConn,
|
DbConn,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -21,7 +22,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-authenticator", data = "<data>")]
|
#[post("/two-factor/get-authenticator", data = "<data>")]
|
||||||
fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
@@ -30,11 +31,11 @@ fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn
|
|||||||
}
|
}
|
||||||
|
|
||||||
let type_ = TwoFactorType::Authenticator as i32;
|
let type_ = TwoFactorType::Authenticator as i32;
|
||||||
let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn);
|
let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await;
|
||||||
|
|
||||||
let (enabled, key) = match twofactor {
|
let (enabled, key) = match twofactor {
|
||||||
Some(tf) => (true, tf.data),
|
Some(tf) => (true, tf.data),
|
||||||
_ => (false, BASE32.encode(&crypto::get_random(vec![0u8; 20]))),
|
_ => (false, crypto::encode_random_bytes::<20>(BASE32)),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
@@ -53,11 +54,10 @@ struct EnableAuthenticatorData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/authenticator", data = "<data>")]
|
#[post("/two-factor/authenticator", data = "<data>")]
|
||||||
fn activate_authenticator(
|
async fn activate_authenticator(
|
||||||
data: JsonUpcase<EnableAuthenticatorData>,
|
data: JsonUpcase<EnableAuthenticatorData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
ip: ClientIp,
|
mut conn: DbConn,
|
||||||
conn: DbConn,
|
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
let data: EnableAuthenticatorData = data.into_inner().data;
|
let data: EnableAuthenticatorData = data.into_inner().data;
|
||||||
let password_hash = data.MasterPasswordHash;
|
let password_hash = data.MasterPasswordHash;
|
||||||
@@ -81,9 +81,11 @@ fn activate_authenticator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate the token provided with the key, and save new twofactor
|
// Validate the token provided with the key, and save new twofactor
|
||||||
validate_totp_code(&user.uuid, &token, &key.to_uppercase(), &ip, &conn)?;
|
validate_totp_code(&user.uuid, &token, &key.to_uppercase(), &headers.ip, &mut conn).await?;
|
||||||
|
|
||||||
_generate_recover_code(&mut user, &conn);
|
_generate_recover_code(&mut user, &mut conn).await;
|
||||||
|
|
||||||
|
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await;
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
@@ -93,30 +95,35 @@ fn activate_authenticator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/two-factor/authenticator", data = "<data>")]
|
#[put("/two-factor/authenticator", data = "<data>")]
|
||||||
fn activate_authenticator_put(
|
async fn activate_authenticator_put(
|
||||||
data: JsonUpcase<EnableAuthenticatorData>,
|
data: JsonUpcase<EnableAuthenticatorData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
ip: ClientIp,
|
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
activate_authenticator(data, headers, ip, conn)
|
activate_authenticator(data, headers, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_totp_code_str(
|
pub async fn validate_totp_code_str(
|
||||||
user_uuid: &str,
|
user_uuid: &str,
|
||||||
totp_code: &str,
|
totp_code: &str,
|
||||||
secret: &str,
|
secret: &str,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
conn: &DbConn,
|
conn: &mut DbConn,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
if !totp_code.chars().all(char::is_numeric) {
|
if !totp_code.chars().all(char::is_numeric) {
|
||||||
err!("TOTP code is not a number");
|
err!("TOTP code is not a number");
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_totp_code(user_uuid, totp_code, secret, ip, conn)
|
validate_totp_code(user_uuid, totp_code, secret, ip, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_totp_code(user_uuid: &str, totp_code: &str, secret: &str, ip: &ClientIp, conn: &DbConn) -> EmptyResult {
|
pub async fn validate_totp_code(
|
||||||
|
user_uuid: &str,
|
||||||
|
totp_code: &str,
|
||||||
|
secret: &str,
|
||||||
|
ip: &ClientIp,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
use totp_lite::{totp_custom, Sha1};
|
use totp_lite::{totp_custom, Sha1};
|
||||||
|
|
||||||
let decoded_secret = match BASE32.decode(secret.as_bytes()) {
|
let decoded_secret = match BASE32.decode(secret.as_bytes()) {
|
||||||
@@ -124,7 +131,8 @@ pub fn validate_totp_code(user_uuid: &str, totp_code: &str, secret: &str, ip: &C
|
|||||||
Err(_) => err!("Invalid TOTP secret"),
|
Err(_) => err!("Invalid TOTP secret"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut twofactor = match TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::Authenticator as i32, conn) {
|
let mut twofactor =
|
||||||
|
match TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::Authenticator as i32, conn).await {
|
||||||
Some(tf) => tf,
|
Some(tf) => tf,
|
||||||
_ => TwoFactor::new(user_uuid.to_string(), TwoFactorType::Authenticator, secret.to_string()),
|
_ => TwoFactor::new(user_uuid.to_string(), TwoFactorType::Authenticator, secret.to_string()),
|
||||||
};
|
};
|
||||||
@@ -132,7 +140,7 @@ pub fn validate_totp_code(user_uuid: &str, totp_code: &str, secret: &str, ip: &C
|
|||||||
// The amount of steps back and forward in time
|
// The amount of steps back and forward in time
|
||||||
// Also check if we need to disable time drifted TOTP codes.
|
// Also check if we need to disable time drifted TOTP codes.
|
||||||
// If that is the case, we set the steps to 0 so only the current TOTP is valid.
|
// If that is the case, we set the steps to 0 so only the current TOTP is valid.
|
||||||
let steps = !CONFIG.authenticator_disable_time_drift() as i64;
|
let steps = i64::from(!CONFIG.authenticator_disable_time_drift());
|
||||||
|
|
||||||
// Get the current system time in UNIX Epoch (UTC)
|
// Get the current system time in UNIX Epoch (UTC)
|
||||||
let current_time = chrono::Utc::now();
|
let current_time = chrono::Utc::now();
|
||||||
@@ -147,7 +155,7 @@ pub fn validate_totp_code(user_uuid: &str, totp_code: &str, secret: &str, ip: &C
|
|||||||
let generated = totp_custom::<Sha1>(30, 6, &decoded_secret, time);
|
let generated = totp_custom::<Sha1>(30, 6, &decoded_secret, time);
|
||||||
|
|
||||||
// Check the the given code equals the generated and if the time_step is larger then the one last used.
|
// Check the the given code equals the generated and if the time_step is larger then the one last used.
|
||||||
if generated == totp_code && time_step > twofactor.last_used as i64 {
|
if generated == totp_code && time_step > i64::from(twofactor.last_used) {
|
||||||
// If the step does not equals 0 the time is drifted either server or client side.
|
// If the step does not equals 0 the time is drifted either server or client side.
|
||||||
if step != 0 {
|
if step != 0 {
|
||||||
warn!("TOTP Time drift detected. The step offset is {}", step);
|
warn!("TOTP Time drift detected. The step offset is {}", step);
|
||||||
@@ -156,14 +164,24 @@ pub fn validate_totp_code(user_uuid: &str, totp_code: &str, secret: &str, ip: &C
|
|||||||
// Save the last used time step so only totp time steps higher then this one are allowed.
|
// Save the last used time step so only totp time steps higher then this one are allowed.
|
||||||
// This will also save a newly created twofactor if the code is correct.
|
// This will also save a newly created twofactor if the code is correct.
|
||||||
twofactor.last_used = time_step as i32;
|
twofactor.last_used = time_step as i32;
|
||||||
twofactor.save(conn)?;
|
twofactor.save(conn).await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else if generated == totp_code && time_step <= twofactor.last_used as i64 {
|
} else if generated == totp_code && time_step <= i64::from(twofactor.last_used) {
|
||||||
warn!("This TOTP or a TOTP code within {} steps back or forward has already been used!", steps);
|
warn!("This TOTP or a TOTP code within {} steps back or forward has already been used!", steps);
|
||||||
err!(format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip));
|
err!(
|
||||||
|
format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip),
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn2fa
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Else no valide code received, deny access
|
// Else no valide code received, deny access
|
||||||
err!(format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip));
|
err!(
|
||||||
|
format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip),
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn2fa
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use data_encoding::BASE64;
|
use data_encoding::BASE64;
|
||||||
|
use rocket::serde::json::Json;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult, JsonUpcase, PasswordData},
|
api::{
|
||||||
|
core::log_user_event, core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult, JsonUpcase,
|
||||||
|
PasswordData,
|
||||||
|
},
|
||||||
auth::Headers,
|
auth::Headers,
|
||||||
crypto,
|
crypto,
|
||||||
db::{
|
db::{
|
||||||
models::{TwoFactor, TwoFactorType, User},
|
models::{EventType, TwoFactor, TwoFactorType, User},
|
||||||
DbConn,
|
DbConn,
|
||||||
},
|
},
|
||||||
error::MapResult,
|
error::MapResult,
|
||||||
@@ -89,14 +92,14 @@ impl DuoStatus {
|
|||||||
const DISABLED_MESSAGE_DEFAULT: &str = "<To use the global Duo keys, please leave these fields untouched>";
|
const DISABLED_MESSAGE_DEFAULT: &str = "<To use the global Duo keys, please leave these fields untouched>";
|
||||||
|
|
||||||
#[post("/two-factor/get-duo", data = "<data>")]
|
#[post("/two-factor/get-duo", data = "<data>")]
|
||||||
fn get_duo(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn get_duo(data: JsonUpcase<PasswordData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
|
|
||||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||||
err!("Invalid password");
|
err!("Invalid password");
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = get_user_duo_data(&headers.user.uuid, &conn);
|
let data = get_user_duo_data(&headers.user.uuid, &mut conn).await;
|
||||||
|
|
||||||
let (enabled, data) = match data {
|
let (enabled, data) = match data {
|
||||||
DuoStatus::Global(_) => (true, Some(DuoData::secret())),
|
DuoStatus::Global(_) => (true, Some(DuoData::secret())),
|
||||||
@@ -152,7 +155,7 @@ fn check_duo_fields_custom(data: &EnableDuoData) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/duo", data = "<data>")]
|
#[post("/two-factor/duo", data = "<data>")]
|
||||||
fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let data: EnableDuoData = data.into_inner().data;
|
let data: EnableDuoData = data.into_inner().data;
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
@@ -163,7 +166,7 @@ fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn)
|
|||||||
let (data, data_str) = if check_duo_fields_custom(&data) {
|
let (data, data_str) = if check_duo_fields_custom(&data) {
|
||||||
let data_req: DuoData = data.into();
|
let data_req: DuoData = data.into();
|
||||||
let data_str = serde_json::to_string(&data_req)?;
|
let data_str = serde_json::to_string(&data_req)?;
|
||||||
duo_api_request("GET", "/auth/v2/check", "", &data_req).map_res("Failed to validate Duo credentials")?;
|
duo_api_request("GET", "/auth/v2/check", "", &data_req).await.map_res("Failed to validate Duo credentials")?;
|
||||||
(data_req.obscure(), data_str)
|
(data_req.obscure(), data_str)
|
||||||
} else {
|
} else {
|
||||||
(DuoData::secret(), String::new())
|
(DuoData::secret(), String::new())
|
||||||
@@ -171,9 +174,11 @@ fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn)
|
|||||||
|
|
||||||
let type_ = TwoFactorType::Duo;
|
let type_ = TwoFactorType::Duo;
|
||||||
let twofactor = TwoFactor::new(user.uuid.clone(), type_, data_str);
|
let twofactor = TwoFactor::new(user.uuid.clone(), type_, data_str);
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&mut conn).await?;
|
||||||
|
|
||||||
_generate_recover_code(&mut user, &conn);
|
_generate_recover_code(&mut user, &mut conn).await;
|
||||||
|
|
||||||
|
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await;
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
@@ -185,11 +190,11 @@ fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/two-factor/duo", data = "<data>")]
|
#[put("/two-factor/duo", data = "<data>")]
|
||||||
fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
activate_duo(data, headers, conn)
|
activate_duo(data, headers, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
|
async fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
|
||||||
use reqwest::{header, Method};
|
use reqwest::{header, Method};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
@@ -209,7 +214,8 @@ fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> Em
|
|||||||
.basic_auth(username, Some(password))
|
.basic_auth(username, Some(password))
|
||||||
.header(header::USER_AGENT, "vaultwarden:Duo/1.0 (Rust)")
|
.header(header::USER_AGENT, "vaultwarden:Duo/1.0 (Rust)")
|
||||||
.header(header::DATE, date)
|
.header(header::DATE, date)
|
||||||
.send()?
|
.send()
|
||||||
|
.await?
|
||||||
.error_for_status()?;
|
.error_for_status()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -222,11 +228,11 @@ const AUTH_PREFIX: &str = "AUTH";
|
|||||||
const DUO_PREFIX: &str = "TX";
|
const DUO_PREFIX: &str = "TX";
|
||||||
const APP_PREFIX: &str = "APP";
|
const APP_PREFIX: &str = "APP";
|
||||||
|
|
||||||
fn get_user_duo_data(uuid: &str, conn: &DbConn) -> DuoStatus {
|
async fn get_user_duo_data(uuid: &str, conn: &mut DbConn) -> DuoStatus {
|
||||||
let type_ = TwoFactorType::Duo as i32;
|
let type_ = TwoFactorType::Duo as i32;
|
||||||
|
|
||||||
// If the user doesn't have an entry, disabled
|
// If the user doesn't have an entry, disabled
|
||||||
let twofactor = match TwoFactor::find_by_user_and_type(uuid, type_, conn) {
|
let twofactor = match TwoFactor::find_by_user_and_type(uuid, type_, conn).await {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => return DuoStatus::Disabled(DuoData::global().is_some()),
|
None => return DuoStatus::Disabled(DuoData::global().is_some()),
|
||||||
};
|
};
|
||||||
@@ -246,41 +252,47 @@ fn get_user_duo_data(uuid: &str, conn: &DbConn) -> DuoStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// let (ik, sk, ak, host) = get_duo_keys();
|
// let (ik, sk, ak, host) = get_duo_keys();
|
||||||
fn get_duo_keys_email(email: &str, conn: &DbConn) -> ApiResult<(String, String, String, String)> {
|
async fn get_duo_keys_email(email: &str, conn: &mut DbConn) -> ApiResult<(String, String, String, String)> {
|
||||||
let data = User::find_by_mail(email, conn)
|
let data = match User::find_by_mail(email, conn).await {
|
||||||
.and_then(|u| get_user_duo_data(&u.uuid, conn).data())
|
Some(u) => get_user_duo_data(&u.uuid, conn).await.data(),
|
||||||
.or_else(DuoData::global)
|
_ => DuoData::global(),
|
||||||
.map_res("Can't fetch Duo keys")?;
|
}
|
||||||
|
.map_res("Can't fetch Duo Keys")?;
|
||||||
|
|
||||||
Ok((data.ik, data.sk, CONFIG.get_duo_akey(), data.host))
|
Ok((data.ik, data.sk, CONFIG.get_duo_akey(), data.host))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_duo_signature(email: &str, conn: &DbConn) -> ApiResult<(String, String)> {
|
pub async fn generate_duo_signature(email: &str, conn: &mut DbConn) -> ApiResult<(String, String)> {
|
||||||
let now = Utc::now().timestamp();
|
let now = Utc::now().timestamp();
|
||||||
|
|
||||||
let (ik, sk, ak, host) = get_duo_keys_email(email, conn)?;
|
let (ik, sk, ak, host) = get_duo_keys_email(email, conn).await?;
|
||||||
|
|
||||||
let duo_sign = sign_duo_values(&sk, email, &ik, DUO_PREFIX, now + DUO_EXPIRE);
|
let duo_sign = sign_duo_values(&sk, email, &ik, DUO_PREFIX, now + DUO_EXPIRE);
|
||||||
let app_sign = sign_duo_values(&ak, email, &ik, APP_PREFIX, now + APP_EXPIRE);
|
let app_sign = sign_duo_values(&ak, email, &ik, APP_PREFIX, now + APP_EXPIRE);
|
||||||
|
|
||||||
Ok((format!("{}:{}", duo_sign, app_sign), host))
|
Ok((format!("{duo_sign}:{app_sign}"), host))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64) -> String {
|
fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64) -> String {
|
||||||
let val = format!("{}|{}|{}", email, ikey, expire);
|
let val = format!("{email}|{ikey}|{expire}");
|
||||||
let cookie = format!("{}|{}", prefix, BASE64.encode(val.as_bytes()));
|
let cookie = format!("{}|{}", prefix, BASE64.encode(val.as_bytes()));
|
||||||
|
|
||||||
format!("{}|{}", cookie, crypto::hmac_sign(key, &cookie))
|
format!("{}|{}", cookie, crypto::hmac_sign(key, &cookie))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_duo_login(email: &str, response: &str, conn: &DbConn) -> EmptyResult {
|
pub async fn validate_duo_login(email: &str, response: &str, conn: &mut DbConn) -> EmptyResult {
|
||||||
// email is as entered by the user, so it needs to be normalized before
|
// email is as entered by the user, so it needs to be normalized before
|
||||||
// comparison with auth_user below.
|
// comparison with auth_user below.
|
||||||
let email = &email.to_lowercase();
|
let email = &email.to_lowercase();
|
||||||
|
|
||||||
let split: Vec<&str> = response.split(':').collect();
|
let split: Vec<&str> = response.split(':').collect();
|
||||||
if split.len() != 2 {
|
if split.len() != 2 {
|
||||||
err!("Invalid response length");
|
err!(
|
||||||
|
"Invalid response length",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn2fa
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let auth_sig = split[0];
|
let auth_sig = split[0];
|
||||||
@@ -288,13 +300,18 @@ pub fn validate_duo_login(email: &str, response: &str, conn: &DbConn) -> EmptyRe
|
|||||||
|
|
||||||
let now = Utc::now().timestamp();
|
let now = Utc::now().timestamp();
|
||||||
|
|
||||||
let (ik, sk, ak, _host) = get_duo_keys_email(email, conn)?;
|
let (ik, sk, ak, _host) = get_duo_keys_email(email, conn).await?;
|
||||||
|
|
||||||
let auth_user = parse_duo_values(&sk, auth_sig, &ik, AUTH_PREFIX, now)?;
|
let auth_user = parse_duo_values(&sk, auth_sig, &ik, AUTH_PREFIX, now)?;
|
||||||
let app_user = parse_duo_values(&ak, app_sig, &ik, APP_PREFIX, now)?;
|
let app_user = parse_duo_values(&ak, app_sig, &ik, APP_PREFIX, now)?;
|
||||||
|
|
||||||
if !crypto::ct_eq(&auth_user, app_user) || !crypto::ct_eq(&auth_user, email) {
|
if !crypto::ct_eq(&auth_user, app_user) || !crypto::ct_eq(&auth_user, email) {
|
||||||
err!("Error validating duo authentication")
|
err!(
|
||||||
|
"Error validating duo authentication",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn2fa
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -310,7 +327,7 @@ fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) -
|
|||||||
let u_b64 = split[1];
|
let u_b64 = split[1];
|
||||||
let u_sig = split[2];
|
let u_sig = split[2];
|
||||||
|
|
||||||
let sig = crypto::hmac_sign(key, &format!("{}|{}", u_prefix, u_b64));
|
let sig = crypto::hmac_sign(key, &format!("{u_prefix}|{u_b64}"));
|
||||||
|
|
||||||
if !crypto::ct_eq(crypto::hmac_sign(key, &sig), crypto::hmac_sign(key, u_sig)) {
|
if !crypto::ct_eq(crypto::hmac_sign(key, &sig), crypto::hmac_sign(key, u_sig)) {
|
||||||
err!("Duo signatures don't match")
|
err!("Duo signatures don't match")
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
use chrono::{Duration, NaiveDateTime, Utc};
|
use chrono::{Duration, NaiveDateTime, Utc};
|
||||||
|
use rocket::serde::json::Json;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, PasswordData},
|
api::{
|
||||||
|
core::{log_user_event, two_factor::_generate_recover_code},
|
||||||
|
EmptyResult, JsonResult, JsonUpcase, PasswordData,
|
||||||
|
},
|
||||||
auth::Headers,
|
auth::Headers,
|
||||||
crypto,
|
crypto,
|
||||||
db::{
|
db::{
|
||||||
models::{TwoFactor, TwoFactorType},
|
models::{EventType, TwoFactor, TwoFactorType},
|
||||||
DbConn,
|
DbConn,
|
||||||
},
|
},
|
||||||
error::{Error, MapResult},
|
error::{Error, MapResult},
|
||||||
@@ -28,13 +31,13 @@ struct SendEmailLoginData {
|
|||||||
/// User is trying to login and wants to use email 2FA.
|
/// User is trying to login and wants to use email 2FA.
|
||||||
/// Does not require Bearer token
|
/// Does not require Bearer token
|
||||||
#[post("/two-factor/send-email-login", data = "<data>")] // JsonResult
|
#[post("/two-factor/send-email-login", data = "<data>")] // JsonResult
|
||||||
fn send_email_login(data: JsonUpcase<SendEmailLoginData>, conn: DbConn) -> EmptyResult {
|
async fn send_email_login(data: JsonUpcase<SendEmailLoginData>, mut conn: DbConn) -> EmptyResult {
|
||||||
let data: SendEmailLoginData = data.into_inner().data;
|
let data: SendEmailLoginData = data.into_inner().data;
|
||||||
|
|
||||||
use crate::db::models::User;
|
use crate::db::models::User;
|
||||||
|
|
||||||
// Get the user
|
// Get the user
|
||||||
let user = match User::find_by_mail(&data.Email, &conn) {
|
let user = match User::find_by_mail(&data.Email, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Username or password is incorrect. Try again."),
|
None => err!("Username or password is incorrect. Try again."),
|
||||||
};
|
};
|
||||||
@@ -48,31 +51,32 @@ fn send_email_login(data: JsonUpcase<SendEmailLoginData>, conn: DbConn) -> Empty
|
|||||||
err!("Email 2FA is disabled")
|
err!("Email 2FA is disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
send_token(&user.uuid, &conn)?;
|
send_token(&user.uuid, &mut conn).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the token, save the data for later verification and send email to user
|
/// Generate the token, save the data for later verification and send email to user
|
||||||
pub fn send_token(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub async fn send_token(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||||
let type_ = TwoFactorType::Email as i32;
|
let type_ = TwoFactorType::Email as i32;
|
||||||
let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, type_, conn).map_res("Two factor not found")?;
|
let mut twofactor =
|
||||||
|
TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await.map_res("Two factor not found")?;
|
||||||
|
|
||||||
let generated_token = crypto::generate_email_token(CONFIG.email_token_size());
|
let generated_token = crypto::generate_email_token(CONFIG.email_token_size());
|
||||||
|
|
||||||
let mut twofactor_data = EmailTokenData::from_json(&twofactor.data)?;
|
let mut twofactor_data = EmailTokenData::from_json(&twofactor.data)?;
|
||||||
twofactor_data.set_token(generated_token);
|
twofactor_data.set_token(generated_token);
|
||||||
twofactor.data = twofactor_data.to_json();
|
twofactor.data = twofactor_data.to_json();
|
||||||
twofactor.save(conn)?;
|
twofactor.save(conn).await?;
|
||||||
|
|
||||||
mail::send_token(&twofactor_data.email, &twofactor_data.last_token.map_res("Token is empty")?)?;
|
mail::send_token(&twofactor_data.email, &twofactor_data.last_token.map_res("Token is empty")?).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When user clicks on Manage email 2FA show the user the related information
|
/// When user clicks on Manage email 2FA show the user the related information
|
||||||
#[post("/two-factor/get-email", data = "<data>")]
|
#[post("/two-factor/get-email", data = "<data>")]
|
||||||
fn get_email(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn get_email(data: JsonUpcase<PasswordData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
@@ -80,12 +84,13 @@ fn get_email(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) ->
|
|||||||
err!("Invalid password");
|
err!("Invalid password");
|
||||||
}
|
}
|
||||||
|
|
||||||
let (enabled, mfa_email) = match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Email as i32, &conn) {
|
let (enabled, mfa_email) =
|
||||||
|
match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Email as i32, &mut conn).await {
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
let twofactor_data = EmailTokenData::from_json(&x.data)?;
|
let twofactor_data = EmailTokenData::from_json(&x.data)?;
|
||||||
(true, json!(twofactor_data.email))
|
(true, json!(twofactor_data.email))
|
||||||
}
|
}
|
||||||
_ => (false, json!(null)),
|
_ => (false, serde_json::value::Value::Null),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
@@ -105,7 +110,7 @@ struct SendEmailData {
|
|||||||
|
|
||||||
/// Send a verification email to the specified email address to check whether it exists/belongs to user.
|
/// Send a verification email to the specified email address to check whether it exists/belongs to user.
|
||||||
#[post("/two-factor/send-email", data = "<data>")]
|
#[post("/two-factor/send-email", data = "<data>")]
|
||||||
fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
async fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||||
let data: SendEmailData = data.into_inner().data;
|
let data: SendEmailData = data.into_inner().data;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
@@ -119,8 +124,8 @@ fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -
|
|||||||
|
|
||||||
let type_ = TwoFactorType::Email as i32;
|
let type_ = TwoFactorType::Email as i32;
|
||||||
|
|
||||||
if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
|
if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
|
||||||
tf.delete(&conn)?;
|
tf.delete(&mut conn).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let generated_token = crypto::generate_email_token(CONFIG.email_token_size());
|
let generated_token = crypto::generate_email_token(CONFIG.email_token_size());
|
||||||
@@ -128,9 +133,9 @@ fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -
|
|||||||
|
|
||||||
// Uses EmailVerificationChallenge as type to show that it's not verified yet.
|
// Uses EmailVerificationChallenge as type to show that it's not verified yet.
|
||||||
let twofactor = TwoFactor::new(user.uuid, TwoFactorType::EmailVerificationChallenge, twofactor_data.to_json());
|
let twofactor = TwoFactor::new(user.uuid, TwoFactorType::EmailVerificationChallenge, twofactor_data.to_json());
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&mut conn).await?;
|
||||||
|
|
||||||
mail::send_token(&twofactor_data.email, &twofactor_data.last_token.map_res("Token is empty")?)?;
|
mail::send_token(&twofactor_data.email, &twofactor_data.last_token.map_res("Token is empty")?).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -145,7 +150,7 @@ struct EmailData {
|
|||||||
|
|
||||||
/// Verify email belongs to user and can be used for 2FA email codes.
|
/// Verify email belongs to user and can be used for 2FA email codes.
|
||||||
#[put("/two-factor/email", data = "<data>")]
|
#[put("/two-factor/email", data = "<data>")]
|
||||||
fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn email(data: JsonUpcase<EmailData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let data: EmailData = data.into_inner().data;
|
let data: EmailData = data.into_inner().data;
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
@@ -154,7 +159,8 @@ fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
let type_ = TwoFactorType::EmailVerificationChallenge as i32;
|
let type_ = TwoFactorType::EmailVerificationChallenge as i32;
|
||||||
let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).map_res("Two factor not found")?;
|
let mut twofactor =
|
||||||
|
TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await.map_res("Two factor not found")?;
|
||||||
|
|
||||||
let mut email_data = EmailTokenData::from_json(&twofactor.data)?;
|
let mut email_data = EmailTokenData::from_json(&twofactor.data)?;
|
||||||
|
|
||||||
@@ -170,9 +176,11 @@ fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonRes
|
|||||||
email_data.reset_token();
|
email_data.reset_token();
|
||||||
twofactor.atype = TwoFactorType::Email as i32;
|
twofactor.atype = TwoFactorType::Email as i32;
|
||||||
twofactor.data = email_data.to_json();
|
twofactor.data = email_data.to_json();
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&mut conn).await?;
|
||||||
|
|
||||||
_generate_recover_code(&mut user, &conn);
|
_generate_recover_code(&mut user, &mut conn).await;
|
||||||
|
|
||||||
|
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await;
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Email": email_data.email,
|
"Email": email_data.email,
|
||||||
@@ -182,13 +190,19 @@ fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Validate the email code when used as TwoFactor token mechanism
|
/// Validate the email code when used as TwoFactor token mechanism
|
||||||
pub fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: &DbConn) -> EmptyResult {
|
pub async fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: &mut DbConn) -> EmptyResult {
|
||||||
let mut email_data = EmailTokenData::from_json(data)?;
|
let mut email_data = EmailTokenData::from_json(data)?;
|
||||||
let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::Email as i32, conn)
|
let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::Email as i32, conn)
|
||||||
|
.await
|
||||||
.map_res("Two factor not found")?;
|
.map_res("Two factor not found")?;
|
||||||
let issued_token = match &email_data.last_token {
|
let issued_token = match &email_data.last_token {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
_ => err!("No token available"),
|
_ => err!(
|
||||||
|
"No token available",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn2fa
|
||||||
|
}
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !crypto::ct_eq(issued_token, token) {
|
if !crypto::ct_eq(issued_token, token) {
|
||||||
@@ -197,23 +211,34 @@ pub fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: &
|
|||||||
email_data.reset_token();
|
email_data.reset_token();
|
||||||
}
|
}
|
||||||
twofactor.data = email_data.to_json();
|
twofactor.data = email_data.to_json();
|
||||||
twofactor.save(conn)?;
|
twofactor.save(conn).await?;
|
||||||
|
|
||||||
err!("Token is invalid")
|
err!(
|
||||||
|
"Token is invalid",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn2fa
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
email_data.reset_token();
|
email_data.reset_token();
|
||||||
twofactor.data = email_data.to_json();
|
twofactor.data = email_data.to_json();
|
||||||
twofactor.save(conn)?;
|
twofactor.save(conn).await?;
|
||||||
|
|
||||||
let date = NaiveDateTime::from_timestamp(email_data.token_sent, 0);
|
let date = NaiveDateTime::from_timestamp_opt(email_data.token_sent, 0).expect("Email token timestamp invalid.");
|
||||||
let max_time = CONFIG.email_expiration_time() as i64;
|
let max_time = CONFIG.email_expiration_time() as i64;
|
||||||
if date + Duration::seconds(max_time) < Utc::now().naive_utc() {
|
if date + Duration::seconds(max_time) < Utc::now().naive_utc() {
|
||||||
err!("Token has expired")
|
err!(
|
||||||
|
"Token has expired",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn2fa
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data stored in the TwoFactor table in the db
|
/// Data stored in the TwoFactor table in the db
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct EmailTokenData {
|
pub struct EmailTokenData {
|
||||||
@@ -279,7 +304,7 @@ pub fn obscure_email(email: &str) -> String {
|
|||||||
_ => {
|
_ => {
|
||||||
let stars = "*".repeat(name_size - 2);
|
let stars = "*".repeat(name_size - 2);
|
||||||
name.truncate(2);
|
name.truncate(2);
|
||||||
format!("{}{}", name, stars)
|
format!("{name}{stars}")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use data_encoding::BASE32;
|
use data_encoding::BASE32;
|
||||||
|
use rocket::serde::json::Json;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{JsonResult, JsonUpcase, NumberOrString, PasswordData},
|
api::{core::log_user_event, JsonResult, JsonUpcase, NumberOrString, PasswordData},
|
||||||
auth::Headers,
|
auth::{ClientHeaders, Headers},
|
||||||
crypto,
|
crypto,
|
||||||
db::{models::*, DbConn, DbPool},
|
db::{models::*, DbConn, DbPool},
|
||||||
mail, CONFIG,
|
mail, CONFIG,
|
||||||
@@ -15,17 +15,22 @@ use crate::{
|
|||||||
pub mod authenticator;
|
pub mod authenticator;
|
||||||
pub mod duo;
|
pub mod duo;
|
||||||
pub mod email;
|
pub mod email;
|
||||||
pub mod u2f;
|
|
||||||
pub mod webauthn;
|
pub mod webauthn;
|
||||||
pub mod yubikey;
|
pub mod yubikey;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut routes = routes![get_twofactor, get_recover, recover, disable_twofactor, disable_twofactor_put,];
|
let mut routes = routes![
|
||||||
|
get_twofactor,
|
||||||
|
get_recover,
|
||||||
|
recover,
|
||||||
|
disable_twofactor,
|
||||||
|
disable_twofactor_put,
|
||||||
|
get_device_verification_settings,
|
||||||
|
];
|
||||||
|
|
||||||
routes.append(&mut authenticator::routes());
|
routes.append(&mut authenticator::routes());
|
||||||
routes.append(&mut duo::routes());
|
routes.append(&mut duo::routes());
|
||||||
routes.append(&mut email::routes());
|
routes.append(&mut email::routes());
|
||||||
routes.append(&mut u2f::routes());
|
|
||||||
routes.append(&mut webauthn::routes());
|
routes.append(&mut webauthn::routes());
|
||||||
routes.append(&mut yubikey::routes());
|
routes.append(&mut yubikey::routes());
|
||||||
|
|
||||||
@@ -33,8 +38,8 @@ pub fn routes() -> Vec<Route> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/two-factor")]
|
#[get("/two-factor")]
|
||||||
fn get_twofactor(headers: Headers, conn: DbConn) -> Json<Value> {
|
async fn get_twofactor(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
||||||
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
|
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &mut conn).await;
|
||||||
let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_provider).collect();
|
let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_provider).collect();
|
||||||
|
|
||||||
Json(json!({
|
Json(json!({
|
||||||
@@ -68,13 +73,13 @@ struct RecoverTwoFactor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/recover", data = "<data>")]
|
#[post("/two-factor/recover", data = "<data>")]
|
||||||
fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
|
async fn recover(data: JsonUpcase<RecoverTwoFactor>, client_headers: ClientHeaders, mut conn: DbConn) -> JsonResult {
|
||||||
let data: RecoverTwoFactor = data.into_inner().data;
|
let data: RecoverTwoFactor = data.into_inner().data;
|
||||||
|
|
||||||
use crate::db::models::User;
|
use crate::db::models::User;
|
||||||
|
|
||||||
// Get the user
|
// Get the user
|
||||||
let mut user = match User::find_by_mail(&data.Email, &conn) {
|
let mut user = match User::find_by_mail(&data.Email, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Username or password is incorrect. Try again."),
|
None => err!("Username or password is incorrect. Try again."),
|
||||||
};
|
};
|
||||||
@@ -90,19 +95,28 @@ fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove all twofactors from the user
|
// Remove all twofactors from the user
|
||||||
TwoFactor::delete_all_by_user(&user.uuid, &conn)?;
|
TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?;
|
||||||
|
|
||||||
|
log_user_event(
|
||||||
|
EventType::UserRecovered2fa as i32,
|
||||||
|
&user.uuid,
|
||||||
|
client_headers.device_type,
|
||||||
|
&client_headers.ip.ip,
|
||||||
|
&mut conn,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
// Remove the recovery code, not needed without twofactors
|
// Remove the recovery code, not needed without twofactors
|
||||||
user.totp_recover = None;
|
user.totp_recover = None;
|
||||||
user.save(&conn)?;
|
user.save(&mut conn).await?;
|
||||||
Ok(Json(json!({})))
|
Ok(Json(Value::Object(serde_json::Map::new())))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _generate_recover_code(user: &mut User, conn: &DbConn) {
|
async fn _generate_recover_code(user: &mut User, conn: &mut DbConn) {
|
||||||
if user.totp_recover.is_none() {
|
if user.totp_recover.is_none() {
|
||||||
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
|
let totp_recover = crypto::encode_random_bytes::<20>(BASE32);
|
||||||
user.totp_recover = Some(totp_recover);
|
user.totp_recover = Some(totp_recover);
|
||||||
user.save(conn).ok();
|
user.save(conn).await.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +128,7 @@ struct DisableTwoFactorData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/disable", data = "<data>")]
|
#[post("/two-factor/disable", data = "<data>")]
|
||||||
fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let data: DisableTwoFactorData = data.into_inner().data;
|
let data: DisableTwoFactorData = data.into_inner().data;
|
||||||
let password_hash = data.MasterPasswordHash;
|
let password_hash = data.MasterPasswordHash;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
@@ -125,23 +139,26 @@ fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, c
|
|||||||
|
|
||||||
let type_ = data.Type.into_i32()?;
|
let type_ = data.Type.into_i32()?;
|
||||||
|
|
||||||
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
|
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
|
||||||
twofactor.delete(&conn)?;
|
twofactor.delete(&mut conn).await?;
|
||||||
|
log_user_event(EventType::UserDisabled2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let twofactor_disabled = TwoFactor::find_by_user(&user.uuid, &conn).is_empty();
|
let twofactor_disabled = TwoFactor::find_by_user(&user.uuid, &mut conn).await.is_empty();
|
||||||
|
|
||||||
if twofactor_disabled {
|
if twofactor_disabled {
|
||||||
let policy_type = OrgPolicyType::TwoFactorAuthentication;
|
for user_org in
|
||||||
let org_list = UserOrganization::find_by_user_and_policy(&user.uuid, policy_type, &conn);
|
UserOrganization::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, &mut conn)
|
||||||
|
.await
|
||||||
for user_org in org_list.into_iter() {
|
.into_iter()
|
||||||
|
{
|
||||||
if user_org.atype < UserOrgType::Admin {
|
if user_org.atype < UserOrgType::Admin {
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
let org = Organization::find_by_uuid(&user_org.org_uuid, &conn).unwrap();
|
let org = Organization::find_by_uuid(&user_org.org_uuid, &mut conn).await.unwrap();
|
||||||
mail::send_2fa_removed_from_org(&user.email, &org.name)?;
|
mail::send_2fa_removed_from_org(&user.email, &org.name).await?;
|
||||||
}
|
}
|
||||||
user_org.delete(&conn)?;
|
user_org.delete(&mut conn).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,18 +171,18 @@ fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/two-factor/disable", data = "<data>")]
|
#[put("/two-factor/disable", data = "<data>")]
|
||||||
fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
disable_twofactor(data, headers, conn)
|
disable_twofactor(data, headers, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_incomplete_2fa_notifications(pool: DbPool) {
|
pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
|
||||||
debug!("Sending notifications for incomplete 2FA logins");
|
debug!("Sending notifications for incomplete 2FA logins");
|
||||||
|
|
||||||
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
|
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let conn = match pool.get() {
|
let mut conn = match pool.get().await {
|
||||||
Ok(conn) => conn,
|
Ok(conn) => conn,
|
||||||
_ => {
|
_ => {
|
||||||
error!("Failed to get DB connection in send_incomplete_2fa_notifications()");
|
error!("Failed to get DB connection in send_incomplete_2fa_notifications()");
|
||||||
@@ -175,15 +192,35 @@ pub fn send_incomplete_2fa_notifications(pool: DbPool) {
|
|||||||
|
|
||||||
let now = Utc::now().naive_utc();
|
let now = Utc::now().naive_utc();
|
||||||
let time_limit = Duration::minutes(CONFIG.incomplete_2fa_time_limit());
|
let time_limit = Duration::minutes(CONFIG.incomplete_2fa_time_limit());
|
||||||
let incomplete_logins = TwoFactorIncomplete::find_logins_before(&(now - time_limit), &conn);
|
let time_before = now - time_limit;
|
||||||
|
let incomplete_logins = TwoFactorIncomplete::find_logins_before(&time_before, &mut conn).await;
|
||||||
for login in incomplete_logins {
|
for login in incomplete_logins {
|
||||||
let user = User::find_by_uuid(&login.user_uuid, &conn).expect("User not found");
|
let user = User::find_by_uuid(&login.user_uuid, &mut conn).await.expect("User not found");
|
||||||
info!(
|
info!(
|
||||||
"User {} did not complete a 2FA login within the configured time limit. IP: {}",
|
"User {} did not complete a 2FA login within the configured time limit. IP: {}",
|
||||||
user.email, login.ip_address
|
user.email, login.ip_address
|
||||||
);
|
);
|
||||||
mail::send_incomplete_2fa_login(&user.email, &login.ip_address, &login.login_time, &login.device_name)
|
mail::send_incomplete_2fa_login(&user.email, &login.ip_address, &login.login_time, &login.device_name)
|
||||||
|
.await
|
||||||
.expect("Error sending incomplete 2FA email");
|
.expect("Error sending incomplete 2FA email");
|
||||||
login.delete(&conn).expect("Error deleting incomplete 2FA record");
|
login.delete(&mut conn).await.expect("Error deleting incomplete 2FA record");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This function currently is just a dummy and the actual part is not implemented yet.
|
||||||
|
// This also prevents 404 errors.
|
||||||
|
//
|
||||||
|
// See the following Bitwarden PR's regarding this feature.
|
||||||
|
// https://github.com/bitwarden/clients/pull/2843
|
||||||
|
// https://github.com/bitwarden/clients/pull/2839
|
||||||
|
// https://github.com/bitwarden/server/pull/2016
|
||||||
|
//
|
||||||
|
// The HTML part is hidden via the CSS patches done via the bw_web_build repo
|
||||||
|
#[get("/two-factor/get-device-verification-settings")]
|
||||||
|
fn get_device_verification_settings(_headers: Headers, _conn: DbConn) -> Json<Value> {
|
||||||
|
Json(json!({
|
||||||
|
"isDeviceVerificationSectionEnabled":false,
|
||||||
|
"unknownDeviceVerificationEnabled":false,
|
||||||
|
"object":"deviceVerificationSettings"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,352 +0,0 @@
|
|||||||
use once_cell::sync::Lazy;
|
|
||||||
use rocket::Route;
|
|
||||||
use rocket_contrib::json::Json;
|
|
||||||
use serde_json::Value;
|
|
||||||
use u2f::{
|
|
||||||
messages::{RegisterResponse, SignResponse, U2fSignRequest},
|
|
||||||
protocol::{Challenge, U2f},
|
|
||||||
register::Registration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
api::{
|
|
||||||
core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString,
|
|
||||||
PasswordData,
|
|
||||||
},
|
|
||||||
auth::Headers,
|
|
||||||
db::{
|
|
||||||
models::{TwoFactor, TwoFactorType},
|
|
||||||
DbConn,
|
|
||||||
},
|
|
||||||
error::Error,
|
|
||||||
CONFIG,
|
|
||||||
};
|
|
||||||
|
|
||||||
const U2F_VERSION: &str = "U2F_V2";
|
|
||||||
|
|
||||||
static APP_ID: Lazy<String> = Lazy::new(|| format!("{}/app-id.json", &CONFIG.domain()));
|
|
||||||
static U2F: Lazy<U2f> = Lazy::new(|| U2f::new(APP_ID.clone()));
|
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
|
||||||
routes![generate_u2f, generate_u2f_challenge, activate_u2f, activate_u2f_put, delete_u2f,]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/two-factor/get-u2f", data = "<data>")]
|
|
||||||
fn generate_u2f(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
|
||||||
if !CONFIG.domain_set() {
|
|
||||||
err!("`DOMAIN` environment variable is not set. U2F disabled")
|
|
||||||
}
|
|
||||||
let data: PasswordData = data.into_inner().data;
|
|
||||||
|
|
||||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
let (enabled, keys) = get_u2f_registrations(&headers.user.uuid, &conn)?;
|
|
||||||
let keys_json: Vec<Value> = keys.iter().map(U2FRegistration::to_json).collect();
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
|
||||||
"Enabled": enabled,
|
|
||||||
"Keys": keys_json,
|
|
||||||
"Object": "twoFactorU2f"
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/two-factor/get-u2f-challenge", data = "<data>")]
|
|
||||||
fn generate_u2f_challenge(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
|
||||||
let data: PasswordData = data.into_inner().data;
|
|
||||||
|
|
||||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
let _type = TwoFactorType::U2fRegisterChallenge;
|
|
||||||
let challenge = _create_u2f_challenge(&headers.user.uuid, _type, &conn).challenge;
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
|
||||||
"UserId": headers.user.uuid,
|
|
||||||
"AppId": APP_ID.to_string(),
|
|
||||||
"Challenge": challenge,
|
|
||||||
"Version": U2F_VERSION,
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
struct EnableU2FData {
|
|
||||||
Id: NumberOrString,
|
|
||||||
// 1..5
|
|
||||||
Name: String,
|
|
||||||
MasterPasswordHash: String,
|
|
||||||
DeviceResponse: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// This struct is referenced from the U2F lib
|
|
||||||
// because it doesn't implement Deserialize
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[serde(remote = "Registration")]
|
|
||||||
struct RegistrationDef {
|
|
||||||
key_handle: Vec<u8>,
|
|
||||||
pub_key: Vec<u8>,
|
|
||||||
attestation_cert: Option<Vec<u8>>,
|
|
||||||
device_name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct U2FRegistration {
|
|
||||||
pub id: i32,
|
|
||||||
pub name: String,
|
|
||||||
#[serde(with = "RegistrationDef")]
|
|
||||||
pub reg: Registration,
|
|
||||||
pub counter: u32,
|
|
||||||
compromised: bool,
|
|
||||||
pub migrated: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl U2FRegistration {
|
|
||||||
fn to_json(&self) -> Value {
|
|
||||||
json!({
|
|
||||||
"Id": self.id,
|
|
||||||
"Name": self.name,
|
|
||||||
"Compromised": self.compromised,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This struct is copied from the U2F lib
|
|
||||||
// to add an optional error code
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct RegisterResponseCopy {
|
|
||||||
pub registration_data: String,
|
|
||||||
pub version: String,
|
|
||||||
pub client_data: String,
|
|
||||||
|
|
||||||
pub error_code: Option<NumberOrString>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RegisterResponseCopy> for RegisterResponse {
|
|
||||||
fn from(r: RegisterResponseCopy) -> RegisterResponse {
|
|
||||||
RegisterResponse {
|
|
||||||
registration_data: r.registration_data,
|
|
||||||
version: r.version,
|
|
||||||
client_data: r.client_data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/two-factor/u2f", data = "<data>")]
|
|
||||||
fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
|
|
||||||
let data: EnableU2FData = data.into_inner().data;
|
|
||||||
let mut user = headers.user;
|
|
||||||
|
|
||||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
let tf_type = TwoFactorType::U2fRegisterChallenge as i32;
|
|
||||||
let tf_challenge = match TwoFactor::find_by_user_and_type(&user.uuid, tf_type, &conn) {
|
|
||||||
Some(c) => c,
|
|
||||||
None => err!("Can't recover challenge"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
|
|
||||||
tf_challenge.delete(&conn)?;
|
|
||||||
|
|
||||||
let response: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?;
|
|
||||||
|
|
||||||
let error_code = response.error_code.clone().map_or("0".into(), NumberOrString::into_string);
|
|
||||||
|
|
||||||
if error_code != "0" {
|
|
||||||
err!("Error registering U2F token")
|
|
||||||
}
|
|
||||||
|
|
||||||
let registration = U2F.register_response(challenge, response.into())?;
|
|
||||||
let full_registration = U2FRegistration {
|
|
||||||
id: data.Id.into_i32()?,
|
|
||||||
name: data.Name,
|
|
||||||
reg: registration,
|
|
||||||
compromised: false,
|
|
||||||
counter: 0,
|
|
||||||
migrated: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut regs = get_u2f_registrations(&user.uuid, &conn)?.1;
|
|
||||||
|
|
||||||
// TODO: Check that there is no repeat Id
|
|
||||||
regs.push(full_registration);
|
|
||||||
save_u2f_registrations(&user.uuid, ®s, &conn)?;
|
|
||||||
|
|
||||||
_generate_recover_code(&mut user, &conn);
|
|
||||||
|
|
||||||
let keys_json: Vec<Value> = regs.iter().map(U2FRegistration::to_json).collect();
|
|
||||||
Ok(Json(json!({
|
|
||||||
"Enabled": true,
|
|
||||||
"Keys": keys_json,
|
|
||||||
"Object": "twoFactorU2f"
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[put("/two-factor/u2f", data = "<data>")]
|
|
||||||
fn activate_u2f_put(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
|
|
||||||
activate_u2f(data, headers, conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
struct DeleteU2FData {
|
|
||||||
Id: NumberOrString,
|
|
||||||
MasterPasswordHash: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[delete("/two-factor/u2f", data = "<data>")]
|
|
||||||
fn delete_u2f(data: JsonUpcase<DeleteU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
|
|
||||||
let data: DeleteU2FData = data.into_inner().data;
|
|
||||||
|
|
||||||
let id = data.Id.into_i32()?;
|
|
||||||
|
|
||||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
let type_ = TwoFactorType::U2f as i32;
|
|
||||||
let mut tf = match TwoFactor::find_by_user_and_type(&headers.user.uuid, type_, &conn) {
|
|
||||||
Some(tf) => tf,
|
|
||||||
None => err!("U2F data not found!"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut data: Vec<U2FRegistration> = match serde_json::from_str(&tf.data) {
|
|
||||||
Ok(d) => d,
|
|
||||||
Err(_) => err!("Error parsing U2F data"),
|
|
||||||
};
|
|
||||||
|
|
||||||
data.retain(|r| r.id != id);
|
|
||||||
|
|
||||||
let new_data_str = serde_json::to_string(&data)?;
|
|
||||||
|
|
||||||
tf.data = new_data_str;
|
|
||||||
tf.save(&conn)?;
|
|
||||||
|
|
||||||
let keys_json: Vec<Value> = data.iter().map(U2FRegistration::to_json).collect();
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
|
||||||
"Enabled": true,
|
|
||||||
"Keys": keys_json,
|
|
||||||
"Object": "twoFactorU2f"
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge {
|
|
||||||
let challenge = U2F.generate_challenge().unwrap();
|
|
||||||
|
|
||||||
TwoFactor::new(user_uuid.into(), type_, serde_json::to_string(&challenge).unwrap())
|
|
||||||
.save(conn)
|
|
||||||
.expect("Error saving challenge");
|
|
||||||
|
|
||||||
challenge
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_u2f_registrations(user_uuid: &str, regs: &[U2FRegistration], conn: &DbConn) -> EmptyResult {
|
|
||||||
TwoFactor::new(user_uuid.into(), TwoFactorType::U2f, serde_json::to_string(regs)?).save(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_u2f_registrations(user_uuid: &str, conn: &DbConn) -> Result<(bool, Vec<U2FRegistration>), Error> {
|
|
||||||
let type_ = TwoFactorType::U2f as i32;
|
|
||||||
let (enabled, regs) = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) {
|
|
||||||
Some(tf) => (tf.enabled, tf.data),
|
|
||||||
None => return Ok((false, Vec::new())), // If no data, return empty list
|
|
||||||
};
|
|
||||||
|
|
||||||
let data = match serde_json::from_str(®s) {
|
|
||||||
Ok(d) => d,
|
|
||||||
Err(_) => {
|
|
||||||
// If error, try old format
|
|
||||||
let mut old_regs = _old_parse_registrations(®s);
|
|
||||||
|
|
||||||
if old_regs.len() != 1 {
|
|
||||||
err!("The old U2F format only allows one device")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to new format
|
|
||||||
let new_regs = vec![U2FRegistration {
|
|
||||||
id: 1,
|
|
||||||
name: "Unnamed U2F key".into(),
|
|
||||||
reg: old_regs.remove(0),
|
|
||||||
compromised: false,
|
|
||||||
counter: 0,
|
|
||||||
migrated: None,
|
|
||||||
}];
|
|
||||||
|
|
||||||
// Save new format
|
|
||||||
save_u2f_registrations(user_uuid, &new_regs, conn)?;
|
|
||||||
|
|
||||||
new_regs
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((enabled, data))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _old_parse_registrations(registations: &str) -> Vec<Registration> {
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct Helper(#[serde(with = "RegistrationDef")] Registration);
|
|
||||||
|
|
||||||
let regs: Vec<Value> = serde_json::from_str(registations).expect("Can't parse Registration data");
|
|
||||||
|
|
||||||
regs.into_iter().map(|r| serde_json::from_value(r).unwrap()).map(|Helper(r)| r).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> {
|
|
||||||
let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn);
|
|
||||||
|
|
||||||
let registrations: Vec<_> = get_u2f_registrations(user_uuid, conn)?.1.into_iter().map(|r| r.reg).collect();
|
|
||||||
|
|
||||||
if registrations.is_empty() {
|
|
||||||
err!("No U2F devices registered")
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(U2F.sign_request(challenge, registrations))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
|
|
||||||
let challenge_type = TwoFactorType::U2fLoginChallenge as i32;
|
|
||||||
let tf_challenge = TwoFactor::find_by_user_and_type(user_uuid, challenge_type, conn);
|
|
||||||
|
|
||||||
let challenge = match tf_challenge {
|
|
||||||
Some(tf_challenge) => {
|
|
||||||
let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
|
|
||||||
tf_challenge.delete(conn)?;
|
|
||||||
challenge
|
|
||||||
}
|
|
||||||
None => err!("Can't recover login challenge"),
|
|
||||||
};
|
|
||||||
let response: SignResponse = serde_json::from_str(response)?;
|
|
||||||
let mut registrations = get_u2f_registrations(user_uuid, conn)?.1;
|
|
||||||
if registrations.is_empty() {
|
|
||||||
err!("No U2F devices registered")
|
|
||||||
}
|
|
||||||
|
|
||||||
for reg in &mut registrations {
|
|
||||||
let response = U2F.sign_response(challenge.clone(), reg.reg.clone(), response.clone(), reg.counter);
|
|
||||||
match response {
|
|
||||||
Ok(new_counter) => {
|
|
||||||
reg.counter = new_counter;
|
|
||||||
save_u2f_registrations(user_uuid, ®istrations, conn)?;
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Err(u2f::u2ferror::U2fError::CounterTooLow) => {
|
|
||||||
reg.compromised = true;
|
|
||||||
save_u2f_registrations(user_uuid, ®istrations, conn)?;
|
|
||||||
|
|
||||||
err!("This device might be compromised!");
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("E {:#}", e);
|
|
||||||
// break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err!("error verifying response")
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
|
use rocket::serde::json::Json;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use webauthn_rs::{base64_data::Base64UrlSafeData, proto::*, AuthenticationState, RegistrationState, Webauthn};
|
use webauthn_rs::{base64_data::Base64UrlSafeData, proto::*, AuthenticationState, RegistrationState, Webauthn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData,
|
core::{log_user_event, two_factor::_generate_recover_code},
|
||||||
|
EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData,
|
||||||
},
|
},
|
||||||
auth::Headers,
|
auth::Headers,
|
||||||
db::{
|
db::{
|
||||||
models::{TwoFactor, TwoFactorType},
|
models::{EventType, TwoFactor, TwoFactorType},
|
||||||
DbConn,
|
DbConn,
|
||||||
},
|
},
|
||||||
error::Error,
|
error::Error,
|
||||||
@@ -21,6 +22,28 @@ pub fn routes() -> Vec<Route> {
|
|||||||
routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,]
|
routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Some old u2f structs still needed for migrating from u2f to WebAuthn
|
||||||
|
// Both `struct Registration` and `struct U2FRegistration` can be removed if we remove the u2f to WebAuthn migration
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Registration {
|
||||||
|
pub key_handle: Vec<u8>,
|
||||||
|
pub pub_key: Vec<u8>,
|
||||||
|
pub attestation_cert: Option<Vec<u8>>,
|
||||||
|
pub device_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct U2FRegistration {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(with = "Registration")]
|
||||||
|
pub reg: Registration,
|
||||||
|
pub counter: u32,
|
||||||
|
compromised: bool,
|
||||||
|
pub migrated: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
struct WebauthnConfig {
|
struct WebauthnConfig {
|
||||||
url: String,
|
url: String,
|
||||||
origin: Url,
|
origin: Url,
|
||||||
@@ -80,7 +103,7 @@ impl WebauthnRegistration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-webauthn", data = "<data>")]
|
#[post("/two-factor/get-webauthn", data = "<data>")]
|
||||||
fn get_webauthn(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn get_webauthn(data: JsonUpcase<PasswordData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
if !CONFIG.domain_set() {
|
if !CONFIG.domain_set() {
|
||||||
err!("`DOMAIN` environment variable is not set. Webauthn disabled")
|
err!("`DOMAIN` environment variable is not set. Webauthn disabled")
|
||||||
}
|
}
|
||||||
@@ -89,7 +112,7 @@ fn get_webauthn(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn)
|
|||||||
err!("Invalid password");
|
err!("Invalid password");
|
||||||
}
|
}
|
||||||
|
|
||||||
let (enabled, registrations) = get_webauthn_registrations(&headers.user.uuid, &conn)?;
|
let (enabled, registrations) = get_webauthn_registrations(&headers.user.uuid, &mut conn).await?;
|
||||||
let registrations_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();
|
let registrations_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
@@ -100,12 +123,13 @@ fn get_webauthn(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
|
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
|
||||||
fn generate_webauthn_challenge(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn generate_webauthn_challenge(data: JsonUpcase<PasswordData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
if !headers.user.check_valid_password(&data.data.MasterPasswordHash) {
|
if !headers.user.check_valid_password(&data.data.MasterPasswordHash) {
|
||||||
err!("Invalid password");
|
err!("Invalid password");
|
||||||
}
|
}
|
||||||
|
|
||||||
let registrations = get_webauthn_registrations(&headers.user.uuid, &conn)?
|
let registrations = get_webauthn_registrations(&headers.user.uuid, &mut conn)
|
||||||
|
.await?
|
||||||
.1
|
.1
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| r.credential.cred_id) // We return the credentialIds to the clients to avoid double registering
|
.map(|r| r.credential.cred_id) // We return the credentialIds to the clients to avoid double registering
|
||||||
@@ -121,7 +145,7 @@ fn generate_webauthn_challenge(data: JsonUpcase<PasswordData>, headers: Headers,
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let type_ = TwoFactorType::WebauthnRegisterChallenge;
|
let type_ = TwoFactorType::WebauthnRegisterChallenge;
|
||||||
TwoFactor::new(headers.user.uuid, type_, serde_json::to_string(&state)?).save(&conn)?;
|
TwoFactor::new(headers.user.uuid, type_, serde_json::to_string(&state)?).save(&mut conn).await?;
|
||||||
|
|
||||||
let mut challenge_value = serde_json::to_value(challenge.public_key)?;
|
let mut challenge_value = serde_json::to_value(challenge.public_key)?;
|
||||||
challenge_value["status"] = "ok".into();
|
challenge_value["status"] = "ok".into();
|
||||||
@@ -218,7 +242,7 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/webauthn", data = "<data>")]
|
#[post("/two-factor/webauthn", data = "<data>")]
|
||||||
fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let data: EnableWebauthnData = data.into_inner().data;
|
let data: EnableWebauthnData = data.into_inner().data;
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
@@ -228,10 +252,10 @@ fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Headers, con
|
|||||||
|
|
||||||
// Retrieve and delete the saved challenge state
|
// Retrieve and delete the saved challenge state
|
||||||
let type_ = TwoFactorType::WebauthnRegisterChallenge as i32;
|
let type_ = TwoFactorType::WebauthnRegisterChallenge as i32;
|
||||||
let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
|
let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
|
||||||
Some(tf) => {
|
Some(tf) => {
|
||||||
let state: RegistrationState = serde_json::from_str(&tf.data)?;
|
let state: RegistrationState = serde_json::from_str(&tf.data)?;
|
||||||
tf.delete(&conn)?;
|
tf.delete(&mut conn).await?;
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
None => err!("Can't recover challenge"),
|
None => err!("Can't recover challenge"),
|
||||||
@@ -241,7 +265,7 @@ fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Headers, con
|
|||||||
let (credential, _data) =
|
let (credential, _data) =
|
||||||
WebauthnConfig::load().register_credential(&data.DeviceResponse.into(), &state, |_| Ok(false))?;
|
WebauthnConfig::load().register_credential(&data.DeviceResponse.into(), &state, |_| Ok(false))?;
|
||||||
|
|
||||||
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &conn)?.1;
|
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
|
||||||
// TODO: Check for repeated ID's
|
// TODO: Check for repeated ID's
|
||||||
registrations.push(WebauthnRegistration {
|
registrations.push(WebauthnRegistration {
|
||||||
id: data.Id.into_i32()?,
|
id: data.Id.into_i32()?,
|
||||||
@@ -252,8 +276,12 @@ fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Headers, con
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Save the registrations and return them
|
// Save the registrations and return them
|
||||||
TwoFactor::new(user.uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?).save(&conn)?;
|
TwoFactor::new(user.uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?)
|
||||||
_generate_recover_code(&mut user, &conn);
|
.save(&mut conn)
|
||||||
|
.await?;
|
||||||
|
_generate_recover_code(&mut user, &mut conn).await;
|
||||||
|
|
||||||
|
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await;
|
||||||
|
|
||||||
let keys_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();
|
let keys_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
@@ -264,8 +292,8 @@ fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Headers, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/two-factor/webauthn", data = "<data>")]
|
#[put("/two-factor/webauthn", data = "<data>")]
|
||||||
fn activate_webauthn_put(data: JsonUpcase<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn activate_webauthn_put(data: JsonUpcase<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
activate_webauthn(data, headers, conn)
|
activate_webauthn(data, headers, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@@ -276,13 +304,14 @@ struct DeleteU2FData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/two-factor/webauthn", data = "<data>")]
|
#[delete("/two-factor/webauthn", data = "<data>")]
|
||||||
fn delete_webauthn(data: JsonUpcase<DeleteU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn delete_webauthn(data: JsonUpcase<DeleteU2FData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let id = data.data.Id.into_i32()?;
|
let id = data.data.Id.into_i32()?;
|
||||||
if !headers.user.check_valid_password(&data.data.MasterPasswordHash) {
|
if !headers.user.check_valid_password(&data.data.MasterPasswordHash) {
|
||||||
err!("Invalid password");
|
err!("Invalid password");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut tf = match TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::Webauthn as i32, &conn) {
|
let mut tf =
|
||||||
|
match TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::Webauthn as i32, &mut conn).await {
|
||||||
Some(tf) => tf,
|
Some(tf) => tf,
|
||||||
None => err!("Webauthn data not found!"),
|
None => err!("Webauthn data not found!"),
|
||||||
};
|
};
|
||||||
@@ -296,12 +325,13 @@ fn delete_webauthn(data: JsonUpcase<DeleteU2FData>, headers: Headers, conn: DbCo
|
|||||||
|
|
||||||
let removed_item = data.remove(item_pos);
|
let removed_item = data.remove(item_pos);
|
||||||
tf.data = serde_json::to_string(&data)?;
|
tf.data = serde_json::to_string(&data)?;
|
||||||
tf.save(&conn)?;
|
tf.save(&mut conn).await?;
|
||||||
drop(tf);
|
drop(tf);
|
||||||
|
|
||||||
// If entry is migrated from u2f, delete the u2f entry as well
|
// If entry is migrated from u2f, delete the u2f entry as well
|
||||||
if let Some(mut u2f) = TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::U2f as i32, &conn) {
|
if let Some(mut u2f) =
|
||||||
use crate::api::core::two_factor::u2f::U2FRegistration;
|
TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::U2f as i32, &mut conn).await
|
||||||
|
{
|
||||||
let mut data: Vec<U2FRegistration> = match serde_json::from_str(&u2f.data) {
|
let mut data: Vec<U2FRegistration> = match serde_json::from_str(&u2f.data) {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(_) => err!("Error parsing U2F data"),
|
Err(_) => err!("Error parsing U2F data"),
|
||||||
@@ -311,7 +341,7 @@ fn delete_webauthn(data: JsonUpcase<DeleteU2FData>, headers: Headers, conn: DbCo
|
|||||||
let new_data_str = serde_json::to_string(&data)?;
|
let new_data_str = serde_json::to_string(&data)?;
|
||||||
|
|
||||||
u2f.data = new_data_str;
|
u2f.data = new_data_str;
|
||||||
u2f.save(&conn)?;
|
u2f.save(&mut conn).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let keys_json: Vec<Value> = data.iter().map(WebauthnRegistration::to_json).collect();
|
let keys_json: Vec<Value> = data.iter().map(WebauthnRegistration::to_json).collect();
|
||||||
@@ -323,18 +353,21 @@ fn delete_webauthn(data: JsonUpcase<DeleteU2FData>, headers: Headers, conn: DbCo
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_webauthn_registrations(user_uuid: &str, conn: &DbConn) -> Result<(bool, Vec<WebauthnRegistration>), Error> {
|
pub async fn get_webauthn_registrations(
|
||||||
|
user_uuid: &str,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> Result<(bool, Vec<WebauthnRegistration>), Error> {
|
||||||
let type_ = TwoFactorType::Webauthn as i32;
|
let type_ = TwoFactorType::Webauthn as i32;
|
||||||
match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) {
|
match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await {
|
||||||
Some(tf) => Ok((tf.enabled, serde_json::from_str(&tf.data)?)),
|
Some(tf) => Ok((tf.enabled, serde_json::from_str(&tf.data)?)),
|
||||||
None => Ok((false, Vec::new())), // If no data, return empty list
|
None => Ok((false, Vec::new())), // If no data, return empty list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_webauthn_login(user_uuid: &str, conn: &DbConn) -> JsonResult {
|
pub async fn generate_webauthn_login(user_uuid: &str, conn: &mut DbConn) -> JsonResult {
|
||||||
// Load saved credentials
|
// Load saved credentials
|
||||||
let creds: Vec<Credential> =
|
let creds: Vec<Credential> =
|
||||||
get_webauthn_registrations(user_uuid, conn)?.1.into_iter().map(|r| r.credential).collect();
|
get_webauthn_registrations(user_uuid, conn).await?.1.into_iter().map(|r| r.credential).collect();
|
||||||
|
|
||||||
if creds.is_empty() {
|
if creds.is_empty() {
|
||||||
err!("No Webauthn devices registered")
|
err!("No Webauthn devices registered")
|
||||||
@@ -346,27 +379,33 @@ pub fn generate_webauthn_login(user_uuid: &str, conn: &DbConn) -> JsonResult {
|
|||||||
|
|
||||||
// Save the challenge state for later validation
|
// Save the challenge state for later validation
|
||||||
TwoFactor::new(user_uuid.into(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?)
|
TwoFactor::new(user_uuid.into(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?)
|
||||||
.save(conn)?;
|
.save(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Return challenge to the clients
|
// Return challenge to the clients
|
||||||
Ok(Json(serde_json::to_value(response.public_key)?))
|
Ok(Json(serde_json::to_value(response.public_key)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
|
pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut DbConn) -> EmptyResult {
|
||||||
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
|
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
|
||||||
let state = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) {
|
let state = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await {
|
||||||
Some(tf) => {
|
Some(tf) => {
|
||||||
let state: AuthenticationState = serde_json::from_str(&tf.data)?;
|
let state: AuthenticationState = serde_json::from_str(&tf.data)?;
|
||||||
tf.delete(conn)?;
|
tf.delete(conn).await?;
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
None => err!("Can't recover login challenge"),
|
None => err!(
|
||||||
|
"Can't recover login challenge",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn2fa
|
||||||
|
}
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let rsp: crate::util::UpCase<PublicKeyCredentialCopy> = serde_json::from_str(response)?;
|
let rsp: crate::util::UpCase<PublicKeyCredentialCopy> = serde_json::from_str(response)?;
|
||||||
let rsp: PublicKeyCredential = rsp.data.into();
|
let rsp: PublicKeyCredential = rsp.data.into();
|
||||||
|
|
||||||
let mut registrations = get_webauthn_registrations(user_uuid, conn)?.1;
|
let mut registrations = get_webauthn_registrations(user_uuid, conn).await?.1;
|
||||||
|
|
||||||
// If the credential we received is migrated from U2F, enable the U2F compatibility
|
// If the credential we received is migrated from U2F, enable the U2F compatibility
|
||||||
//let use_u2f = registrations.iter().any(|r| r.migrated && r.credential.cred_id == rsp.raw_id.0);
|
//let use_u2f = registrations.iter().any(|r| r.migrated && r.credential.cred_id == rsp.raw_id.0);
|
||||||
@@ -377,10 +416,16 @@ pub fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &DbConn) -
|
|||||||
reg.credential.counter = auth_data.counter;
|
reg.credential.counter = auth_data.counter;
|
||||||
|
|
||||||
TwoFactor::new(user_uuid.to_string(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?)
|
TwoFactor::new(user_uuid.to_string(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?)
|
||||||
.save(conn)?;
|
.save(conn)
|
||||||
|
.await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err!("Credential not present")
|
err!(
|
||||||
|
"Credential not present",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn2fa
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
|
use rocket::serde::json::Json;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use yubico::{config::Config, verify};
|
use yubico::{config::Config, verify};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, PasswordData},
|
api::{
|
||||||
|
core::{log_user_event, two_factor::_generate_recover_code},
|
||||||
|
EmptyResult, JsonResult, JsonUpcase, PasswordData,
|
||||||
|
},
|
||||||
auth::Headers,
|
auth::Headers,
|
||||||
db::{
|
db::{
|
||||||
models::{TwoFactor, TwoFactorType},
|
models::{EventType, TwoFactor, TwoFactorType},
|
||||||
DbConn,
|
DbConn,
|
||||||
},
|
},
|
||||||
error::{Error, MapResult},
|
error::{Error, MapResult},
|
||||||
@@ -44,7 +47,7 @@ fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
|
fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
|
||||||
let mut result = json!({});
|
let mut result = Value::Object(serde_json::Map::new());
|
||||||
|
|
||||||
for (i, key) in yubikeys.into_iter().enumerate() {
|
for (i, key) in yubikeys.into_iter().enumerate() {
|
||||||
result[format!("Key{}", i + 1)] = Value::String(key);
|
result[format!("Key{}", i + 1)] = Value::String(key);
|
||||||
@@ -64,21 +67,23 @@ fn get_yubico_credentials() -> Result<(String, String), Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_yubikey_otp(otp: String) -> EmptyResult {
|
async fn verify_yubikey_otp(otp: String) -> EmptyResult {
|
||||||
let (yubico_id, yubico_secret) = get_yubico_credentials()?;
|
let (yubico_id, yubico_secret) = get_yubico_credentials()?;
|
||||||
|
|
||||||
let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
|
let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
|
||||||
|
|
||||||
match CONFIG.yubico_server() {
|
match CONFIG.yubico_server() {
|
||||||
Some(server) => verify(otp, config.set_api_hosts(vec![server])),
|
Some(server) => {
|
||||||
None => verify(otp, config),
|
tokio::task::spawn_blocking(move || verify(otp, config.set_api_hosts(vec![server]))).await.unwrap()
|
||||||
|
}
|
||||||
|
None => tokio::task::spawn_blocking(move || verify(otp, config)).await.unwrap(),
|
||||||
}
|
}
|
||||||
.map_res("Failed to verify OTP")
|
.map_res("Failed to verify OTP")
|
||||||
.and(Ok(()))
|
.and(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-yubikey", data = "<data>")]
|
#[post("/two-factor/get-yubikey", data = "<data>")]
|
||||||
fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
// Make sure the credentials are set
|
// Make sure the credentials are set
|
||||||
get_yubico_credentials()?;
|
get_yubico_credentials()?;
|
||||||
|
|
||||||
@@ -92,7 +97,7 @@ fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbCo
|
|||||||
let user_uuid = &user.uuid;
|
let user_uuid = &user.uuid;
|
||||||
let yubikey_type = TwoFactorType::YubiKey as i32;
|
let yubikey_type = TwoFactorType::YubiKey as i32;
|
||||||
|
|
||||||
let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn);
|
let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &mut conn).await;
|
||||||
|
|
||||||
if let Some(r) = r {
|
if let Some(r) = r {
|
||||||
let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;
|
let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;
|
||||||
@@ -113,7 +118,7 @@ fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/yubikey", data = "<data>")]
|
#[post("/two-factor/yubikey", data = "<data>")]
|
||||||
fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let data: EnableYubikeyData = data.into_inner().data;
|
let data: EnableYubikeyData = data.into_inner().data;
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
@@ -122,7 +127,8 @@ fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if we already have some data
|
// Check if we already have some data
|
||||||
let mut yubikey_data = match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &conn) {
|
let mut yubikey_data =
|
||||||
|
match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &mut conn).await {
|
||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => TwoFactor::new(user.uuid.clone(), TwoFactorType::YubiKey, String::new()),
|
None => TwoFactor::new(user.uuid.clone(), TwoFactorType::YubiKey, String::new()),
|
||||||
};
|
};
|
||||||
@@ -143,10 +149,10 @@ fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn:
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
verify_yubikey_otp(yubikey.to_owned()).map_res("Invalid Yubikey OTP provided")?;
|
verify_yubikey_otp(yubikey.to_owned()).await.map_res("Invalid Yubikey OTP provided")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (&x[..12]).to_owned()).collect();
|
let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (x[..12]).to_owned()).collect();
|
||||||
|
|
||||||
let yubikey_metadata = YubikeyMetadata {
|
let yubikey_metadata = YubikeyMetadata {
|
||||||
Keys: yubikey_ids,
|
Keys: yubikey_ids,
|
||||||
@@ -154,9 +160,11 @@ fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn:
|
|||||||
};
|
};
|
||||||
|
|
||||||
yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap();
|
yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap();
|
||||||
yubikey_data.save(&conn)?;
|
yubikey_data.save(&mut conn).await?;
|
||||||
|
|
||||||
_generate_recover_code(&mut user, &conn);
|
_generate_recover_code(&mut user, &mut conn).await;
|
||||||
|
|
||||||
|
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await;
|
||||||
|
|
||||||
let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
|
let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
|
||||||
|
|
||||||
@@ -168,11 +176,11 @@ fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn:
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/two-factor/yubikey", data = "<data>")]
|
#[put("/two-factor/yubikey", data = "<data>")]
|
||||||
fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
activate_yubikey(data, headers, conn)
|
activate_yubikey(data, headers, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
|
pub async fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
|
||||||
if response.len() != 44 {
|
if response.len() != 44 {
|
||||||
err!("Invalid Yubikey OTP length");
|
err!("Invalid Yubikey OTP length");
|
||||||
}
|
}
|
||||||
@@ -184,7 +192,7 @@ pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResu
|
|||||||
err!("Given Yubikey is not registered");
|
err!("Given Yubikey is not registered");
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = verify_yubikey_otp(response.to_owned());
|
let result = verify_yubikey_otp(response.to_owned()).await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_answer) => Ok(()),
|
Ok(_answer) => Ok(()),
|
||||||
|
|||||||
619
src/api/icons.rs
@@ -1,20 +1,25 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
net::IpAddr,
|
||||||
fs::{create_dir_all, remove_file, symlink_metadata, File},
|
sync::Arc,
|
||||||
io::prelude::*,
|
|
||||||
net::{IpAddr, ToSocketAddrs},
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
use futures::{stream::StreamExt, TryFutureExt};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::{blocking::Client, blocking::Response, header};
|
use reqwest::{
|
||||||
use rocket::{
|
header::{self, HeaderMap, HeaderValue},
|
||||||
http::ContentType,
|
Client, Response,
|
||||||
response::{Content, Redirect},
|
|
||||||
Route,
|
|
||||||
};
|
};
|
||||||
|
use rocket::{http::ContentType, response::Redirect, Route};
|
||||||
|
use tokio::{
|
||||||
|
fs::{create_dir_all, remove_file, symlink_metadata, File},
|
||||||
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
|
net::lookup_host,
|
||||||
|
};
|
||||||
|
|
||||||
|
use html5gum::{Emitter, EndTag, HtmlString, InfallibleTokenizer, Readable, StartTag, StringReader, Tokenizer};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
@@ -25,48 +30,56 @@ use crate::{
|
|||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
match CONFIG.icon_service().as_str() {
|
match CONFIG.icon_service().as_str() {
|
||||||
"internal" => routes![icon_internal],
|
"internal" => routes![icon_internal],
|
||||||
"bitwarden" => routes![icon_bitwarden],
|
_ => routes![icon_external],
|
||||||
"duckduckgo" => routes![icon_duckduckgo],
|
|
||||||
"google" => routes![icon_google],
|
|
||||||
_ => routes![icon_custom],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static CLIENT: Lazy<Client> = Lazy::new(|| {
|
static CLIENT: Lazy<Client> = Lazy::new(|| {
|
||||||
// Generate the default headers
|
// Generate the default headers
|
||||||
let mut default_headers = header::HeaderMap::new();
|
let mut default_headers = HeaderMap::new();
|
||||||
default_headers
|
default_headers.insert(header::USER_AGENT, HeaderValue::from_static("Links (2.22; Linux X86_64; GNU C; text)"));
|
||||||
.insert(header::USER_AGENT, header::HeaderValue::from_static("Links (2.22; Linux X86_64; GNU C; text)"));
|
default_headers.insert(header::ACCEPT, HeaderValue::from_static("text/html, text/*;q=0.5, image/*, */*;q=0.1"));
|
||||||
default_headers
|
default_headers.insert(header::ACCEPT_LANGUAGE, HeaderValue::from_static("en,*;q=0.1"));
|
||||||
.insert(header::ACCEPT, header::HeaderValue::from_static("text/html, text/*;q=0.5, image/*, */*;q=0.1"));
|
default_headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
|
||||||
default_headers.insert(header::ACCEPT_LANGUAGE, header::HeaderValue::from_static("en,*;q=0.1"));
|
default_headers.insert(header::PRAGMA, HeaderValue::from_static("no-cache"));
|
||||||
default_headers.insert(header::CACHE_CONTROL, header::HeaderValue::from_static("no-cache"));
|
|
||||||
default_headers.insert(header::PRAGMA, header::HeaderValue::from_static("no-cache"));
|
// Generate the cookie store
|
||||||
|
let cookie_store = Arc::new(Jar::default());
|
||||||
|
|
||||||
// Reuse the client between requests
|
// Reuse the client between requests
|
||||||
|
let client = get_reqwest_client_builder()
|
||||||
|
.cookie_provider(Arc::clone(&cookie_store))
|
||||||
|
.timeout(Duration::from_secs(CONFIG.icon_download_timeout()))
|
||||||
|
.default_headers(default_headers.clone());
|
||||||
|
|
||||||
|
match client.build() {
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Possible trust-dns error, trying with trust-dns disabled: '{e}'");
|
||||||
get_reqwest_client_builder()
|
get_reqwest_client_builder()
|
||||||
.cookie_provider(Arc::new(Jar::default()))
|
.cookie_provider(cookie_store)
|
||||||
.timeout(Duration::from_secs(CONFIG.icon_download_timeout()))
|
.timeout(Duration::from_secs(CONFIG.icon_download_timeout()))
|
||||||
.default_headers(default_headers)
|
.default_headers(default_headers)
|
||||||
|
.trust_dns(false)
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to build icon client")
|
.expect("Failed to build client")
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build Regex only once since this takes a lot of time.
|
// Build Regex only once since this takes a lot of time.
|
||||||
static ICON_REL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)icon$|apple.*icon").unwrap());
|
|
||||||
static ICON_REL_BLACKLIST: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)mask-icon").unwrap());
|
|
||||||
static ICON_SIZE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap());
|
static ICON_SIZE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap());
|
||||||
|
|
||||||
// Special HashMap which holds the user defined Regex to speedup matching the regex.
|
// Special HashMap which holds the user defined Regex to speedup matching the regex.
|
||||||
static ICON_BLACKLIST_REGEX: Lazy<RwLock<HashMap<String, Regex>>> = Lazy::new(|| RwLock::new(HashMap::new()));
|
static ICON_BLACKLIST_REGEX: Lazy<dashmap::DashMap<String, Regex>> = Lazy::new(dashmap::DashMap::new);
|
||||||
|
|
||||||
fn icon_redirect(domain: &str, template: &str) -> Option<Redirect> {
|
async fn icon_redirect(domain: &str, template: &str) -> Option<Redirect> {
|
||||||
if !is_valid_domain(domain) {
|
if !is_valid_domain(domain) {
|
||||||
warn!("Invalid domain: {}", domain);
|
warn!("Invalid domain: {}", domain);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_domain_blacklisted(domain) {
|
if check_domain_blacklist_reason(domain).await.is_some() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,47 +97,28 @@ fn icon_redirect(domain: &str, template: &str) -> Option<Redirect> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<domain>/icon.png")]
|
#[get("/<domain>/icon.png")]
|
||||||
fn icon_custom(domain: String) -> Option<Redirect> {
|
async fn icon_external(domain: String) -> Option<Redirect> {
|
||||||
icon_redirect(&domain, &CONFIG.icon_service())
|
icon_redirect(&domain, &CONFIG._icon_service_url()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<domain>/icon.png")]
|
#[get("/<domain>/icon.png")]
|
||||||
fn icon_bitwarden(domain: String) -> Option<Redirect> {
|
async fn icon_internal(domain: String) -> Cached<(ContentType, Vec<u8>)> {
|
||||||
icon_redirect(&domain, "https://icons.bitwarden.net/{}/icon.png")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/<domain>/icon.png")]
|
|
||||||
fn icon_duckduckgo(domain: String) -> Option<Redirect> {
|
|
||||||
icon_redirect(&domain, "https://icons.duckduckgo.com/ip3/{}.ico")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/<domain>/icon.png")]
|
|
||||||
fn icon_google(domain: String) -> Option<Redirect> {
|
|
||||||
icon_redirect(&domain, "https://www.google.com/s2/favicons?domain={}&sz=32")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/<domain>/icon.png")]
|
|
||||||
fn icon_internal(domain: String) -> Cached<Content<Vec<u8>>> {
|
|
||||||
const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png");
|
const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png");
|
||||||
|
|
||||||
if !is_valid_domain(&domain) {
|
if !is_valid_domain(&domain) {
|
||||||
warn!("Invalid domain: {}", domain);
|
warn!("Invalid domain: {}", domain);
|
||||||
return Cached::ttl(
|
return Cached::ttl(
|
||||||
Content(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
|
(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
|
||||||
CONFIG.icon_cache_negttl(),
|
CONFIG.icon_cache_negttl(),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
match get_icon(&domain) {
|
match get_icon(&domain).await {
|
||||||
Some((icon, icon_type)) => {
|
Some((icon, icon_type)) => {
|
||||||
Cached::ttl(Content(ContentType::new("image", icon_type), icon), CONFIG.icon_cache_ttl(), true)
|
Cached::ttl((ContentType::new("image", icon_type), icon), CONFIG.icon_cache_ttl(), true)
|
||||||
}
|
}
|
||||||
_ => Cached::ttl(
|
_ => Cached::ttl((ContentType::new("image", "png"), FALLBACK_ICON.to_vec()), CONFIG.icon_cache_negttl(), true),
|
||||||
Content(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
|
|
||||||
CONFIG.icon_cache_negttl(),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +130,7 @@ fn is_valid_domain(domain: &str) -> bool {
|
|||||||
const ALLOWED_CHARS: &str = "_-.";
|
const ALLOWED_CHARS: &str = "_-.";
|
||||||
|
|
||||||
// If parsing the domain fails using Url, it will not work with reqwest.
|
// If parsing the domain fails using Url, it will not work with reqwest.
|
||||||
if let Err(parse_error) = url::Url::parse(format!("https://{}", domain).as_str()) {
|
if let Err(parse_error) = url::Url::parse(format!("https://{domain}").as_str()) {
|
||||||
debug!("Domain parse error: '{}' - {:?}", domain, parse_error);
|
debug!("Domain parse error: '{}' - {:?}", domain, parse_error);
|
||||||
return false;
|
return false;
|
||||||
} else if domain.is_empty()
|
} else if domain.is_empty()
|
||||||
@@ -264,68 +258,65 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_domain_blacklisted(domain: &str) -> bool {
|
#[derive(Debug, Clone)]
|
||||||
let mut is_blacklisted = CONFIG.icon_blacklist_non_global_ips()
|
enum DomainBlacklistReason {
|
||||||
&& (domain, 0)
|
Regex,
|
||||||
.to_socket_addrs()
|
IP,
|
||||||
.map(|x| {
|
|
||||||
for ip_port in x {
|
|
||||||
if !is_global(ip_port.ip()) {
|
|
||||||
warn!("IP {} for domain '{}' is not a global IP!", ip_port.ip(), domain);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
false
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
// Skip the regex check if the previous one is true already
|
use cached::proc_macro::cached;
|
||||||
if !is_blacklisted {
|
#[cached(key = "String", convert = r#"{ domain.to_string() }"#, size = 16, time = 60)]
|
||||||
|
async fn check_domain_blacklist_reason(domain: &str) -> Option<DomainBlacklistReason> {
|
||||||
|
// First check the blacklist regex if there is a match.
|
||||||
|
// This prevents the blocked domain(s) from being leaked via a DNS lookup.
|
||||||
if let Some(blacklist) = CONFIG.icon_blacklist_regex() {
|
if let Some(blacklist) = CONFIG.icon_blacklist_regex() {
|
||||||
let mut regex_hashmap = ICON_BLACKLIST_REGEX.read().unwrap();
|
|
||||||
|
|
||||||
// Use the pre-generate Regex stored in a Lazy HashMap if there's one, else generate it.
|
// Use the pre-generate Regex stored in a Lazy HashMap if there's one, else generate it.
|
||||||
let regex = if let Some(regex) = regex_hashmap.get(&blacklist) {
|
let is_match = if let Some(regex) = ICON_BLACKLIST_REGEX.get(&blacklist) {
|
||||||
regex
|
regex.is_match(domain)
|
||||||
} else {
|
} else {
|
||||||
drop(regex_hashmap);
|
|
||||||
|
|
||||||
let mut regex_hashmap_write = ICON_BLACKLIST_REGEX.write().unwrap();
|
|
||||||
// Clear the current list if the previous key doesn't exists.
|
// Clear the current list if the previous key doesn't exists.
|
||||||
// To prevent growing of the HashMap after someone has changed it via the admin interface.
|
// To prevent growing of the HashMap after someone has changed it via the admin interface.
|
||||||
if regex_hashmap_write.len() >= 1 {
|
if ICON_BLACKLIST_REGEX.len() >= 1 {
|
||||||
regex_hashmap_write.clear();
|
ICON_BLACKLIST_REGEX.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate the regex to store in too the Lazy Static HashMap.
|
// Generate the regex to store in too the Lazy Static HashMap.
|
||||||
let blacklist_regex = Regex::new(&blacklist).unwrap();
|
let blacklist_regex = Regex::new(&blacklist).unwrap();
|
||||||
regex_hashmap_write.insert(blacklist.to_string(), blacklist_regex);
|
let is_match = blacklist_regex.is_match(domain);
|
||||||
drop(regex_hashmap_write);
|
ICON_BLACKLIST_REGEX.insert(blacklist.clone(), blacklist_regex);
|
||||||
|
|
||||||
regex_hashmap = ICON_BLACKLIST_REGEX.read().unwrap();
|
is_match
|
||||||
regex_hashmap.get(&blacklist).unwrap()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use the pre-generate Regex stored in a Lazy HashMap.
|
if is_match {
|
||||||
if regex.is_match(domain) {
|
|
||||||
debug!("Blacklisted domain: {} matched ICON_BLACKLIST_REGEX", domain);
|
debug!("Blacklisted domain: {} matched ICON_BLACKLIST_REGEX", domain);
|
||||||
is_blacklisted = true;
|
return Some(DomainBlacklistReason::Regex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if CONFIG.icon_blacklist_non_global_ips() {
|
||||||
|
if let Ok(s) = lookup_host((domain, 0)).await {
|
||||||
|
for addr in s {
|
||||||
|
if !is_global(addr.ip()) {
|
||||||
|
debug!("IP {} for domain '{}' is not a global IP!", addr.ip(), domain);
|
||||||
|
return Some(DomainBlacklistReason::IP);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is_blacklisted
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
||||||
let path = format!("{}/{}.png", CONFIG.icon_cache_folder(), domain);
|
let path = format!("{}/{}.png", CONFIG.icon_cache_folder(), domain);
|
||||||
|
|
||||||
// Check for expiration of negatively cached copy
|
// Check for expiration of negatively cached copy
|
||||||
if icon_is_negcached(&path) {
|
if icon_is_negcached(&path).await {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(icon) = get_cached_icon(&path) {
|
if let Some(icon) = get_cached_icon(&path).await {
|
||||||
let icon_type = match get_icon_type(&icon) {
|
let icon_type = match get_icon_type(&icon) {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
_ => "x-icon",
|
_ => "x-icon",
|
||||||
@@ -338,31 +329,31 @@ fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the icon, or None in case of error
|
// Get the icon, or None in case of error
|
||||||
match download_icon(domain) {
|
match download_icon(domain).await {
|
||||||
Ok((icon, icon_type)) => {
|
Ok((icon, icon_type)) => {
|
||||||
save_icon(&path, &icon);
|
save_icon(&path, &icon).await;
|
||||||
Some((icon, icon_type.unwrap_or("x-icon").to_string()))
|
Some((icon.to_vec(), icon_type.unwrap_or("x-icon").to_string()))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Unable to download icon: {:?}", e);
|
warn!("Unable to download icon: {:?}", e);
|
||||||
let miss_indicator = path + ".miss";
|
let miss_indicator = path + ".miss";
|
||||||
save_icon(&miss_indicator, &[]);
|
save_icon(&miss_indicator, &[]).await;
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
|
async fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
|
||||||
// Check for expiration of successfully cached copy
|
// Check for expiration of successfully cached copy
|
||||||
if icon_is_expired(path) {
|
if icon_is_expired(path).await {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to read the cached icon, and return it if it exists
|
// Try to read the cached icon, and return it if it exists
|
||||||
if let Ok(mut f) = File::open(path) {
|
if let Ok(mut f) = File::open(path).await {
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
if f.read_to_end(&mut buffer).is_ok() {
|
if f.read_to_end(&mut buffer).await.is_ok() {
|
||||||
return Some(buffer);
|
return Some(buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,22 +361,22 @@ fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_is_expired(path: &str, ttl: u64) -> Result<bool, Error> {
|
async fn file_is_expired(path: &str, ttl: u64) -> Result<bool, Error> {
|
||||||
let meta = symlink_metadata(path)?;
|
let meta = symlink_metadata(path).await?;
|
||||||
let modified = meta.modified()?;
|
let modified = meta.modified()?;
|
||||||
let age = SystemTime::now().duration_since(modified)?;
|
let age = SystemTime::now().duration_since(modified)?;
|
||||||
|
|
||||||
Ok(ttl > 0 && ttl <= age.as_secs())
|
Ok(ttl > 0 && ttl <= age.as_secs())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_is_negcached(path: &str) -> bool {
|
async fn icon_is_negcached(path: &str) -> bool {
|
||||||
let miss_indicator = path.to_owned() + ".miss";
|
let miss_indicator = path.to_owned() + ".miss";
|
||||||
let expired = file_is_expired(&miss_indicator, CONFIG.icon_cache_negttl());
|
let expired = file_is_expired(&miss_indicator, CONFIG.icon_cache_negttl()).await;
|
||||||
|
|
||||||
match expired {
|
match expired {
|
||||||
// No longer negatively cached, drop the marker
|
// No longer negatively cached, drop the marker
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
if let Err(e) = remove_file(&miss_indicator) {
|
if let Err(e) = remove_file(&miss_indicator).await {
|
||||||
error!("Could not remove negative cache indicator for icon {:?}: {:?}", path, e);
|
error!("Could not remove negative cache indicator for icon {:?}: {:?}", path, e);
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
@@ -397,8 +388,8 @@ fn icon_is_negcached(path: &str) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_is_expired(path: &str) -> bool {
|
async fn icon_is_expired(path: &str) -> bool {
|
||||||
let expired = file_is_expired(path, CONFIG.icon_cache_ttl());
|
let expired = file_is_expired(path, CONFIG.icon_cache_ttl()).await;
|
||||||
expired.unwrap_or(true)
|
expired.unwrap_or(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,91 +407,62 @@ impl Icon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterates over the HTML document to find <base href="http://domain.tld">
|
fn get_favicons_node(
|
||||||
/// When found it will stop the iteration and the found base href will be shared deref via `base_href`.
|
dom: InfallibleTokenizer<StringReader<'_>, FaviconEmitter>,
|
||||||
///
|
icons: &mut Vec<Icon>,
|
||||||
/// # Arguments
|
url: &url::Url,
|
||||||
/// * `node` - A Parsed HTML document via html5ever::parse_document()
|
) {
|
||||||
/// * `base_href` - a mutable url::Url which will be overwritten when a base href tag has been found.
|
const TAG_LINK: &[u8] = b"link";
|
||||||
///
|
const TAG_BASE: &[u8] = b"base";
|
||||||
fn get_base_href(node: &std::rc::Rc<markup5ever_rcdom::Node>, base_href: &mut url::Url) -> bool {
|
const TAG_HEAD: &[u8] = b"head";
|
||||||
if let markup5ever_rcdom::NodeData::Element {
|
const ATTR_REL: &[u8] = b"rel";
|
||||||
name,
|
const ATTR_HREF: &[u8] = b"href";
|
||||||
attrs,
|
const ATTR_SIZES: &[u8] = b"sizes";
|
||||||
..
|
|
||||||
} = &node.data
|
|
||||||
{
|
|
||||||
if name.local.as_ref() == "base" {
|
|
||||||
let attrs = attrs.borrow();
|
|
||||||
for attr in attrs.iter() {
|
|
||||||
let attr_name = attr.name.local.as_ref();
|
|
||||||
let attr_value = attr.value.as_ref();
|
|
||||||
|
|
||||||
if attr_name == "href" {
|
let mut base_url = url.clone();
|
||||||
debug!("Found base href: {}", attr_value);
|
let mut icon_tags: Vec<StartTag> = Vec::new();
|
||||||
*base_href = match base_href.join(attr_value) {
|
for token in dom {
|
||||||
Ok(href) => href,
|
match token {
|
||||||
_ => base_href.clone(),
|
FaviconToken::StartTag(tag) => {
|
||||||
|
if *tag.name == TAG_LINK
|
||||||
|
&& tag.attributes.contains_key(ATTR_REL)
|
||||||
|
&& tag.attributes.contains_key(ATTR_HREF)
|
||||||
|
{
|
||||||
|
let rel_value = std::str::from_utf8(tag.attributes.get(ATTR_REL).unwrap())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
if rel_value.contains("icon") && !rel_value.contains("mask-icon") {
|
||||||
|
icon_tags.push(tag);
|
||||||
|
}
|
||||||
|
} else if *tag.name == TAG_BASE && tag.attributes.contains_key(ATTR_HREF) {
|
||||||
|
let href = std::str::from_utf8(tag.attributes.get(ATTR_HREF).unwrap()).unwrap_or_default();
|
||||||
|
debug!("Found base href: {href}");
|
||||||
|
base_url = match base_url.join(href) {
|
||||||
|
Ok(inner_url) => inner_url,
|
||||||
|
_ => url.clone(),
|
||||||
};
|
};
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
FaviconToken::EndTag(tag) => {
|
||||||
}
|
if *tag.name == TAG_HEAD {
|
||||||
}
|
break;
|
||||||
|
|
||||||
// TODO: Might want to limit the recursion depth?
|
|
||||||
for child in node.children.borrow().iter() {
|
|
||||||
// Check if we got a true back and stop the iter.
|
|
||||||
// This means we found a <base> tag and can stop processing the html.
|
|
||||||
if get_base_href(child, base_href) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_favicons_node(node: &std::rc::Rc<markup5ever_rcdom::Node>, icons: &mut Vec<Icon>, url: &url::Url) {
|
|
||||||
if let markup5ever_rcdom::NodeData::Element {
|
|
||||||
name,
|
|
||||||
attrs,
|
|
||||||
..
|
|
||||||
} = &node.data
|
|
||||||
{
|
|
||||||
if name.local.as_ref() == "link" {
|
|
||||||
let mut has_rel = false;
|
|
||||||
let mut href = None;
|
|
||||||
let mut sizes = None;
|
|
||||||
|
|
||||||
let attrs = attrs.borrow();
|
|
||||||
for attr in attrs.iter() {
|
|
||||||
let attr_name = attr.name.local.as_ref();
|
|
||||||
let attr_value = attr.value.as_ref();
|
|
||||||
|
|
||||||
if attr_name == "rel" && ICON_REL_REGEX.is_match(attr_value) && !ICON_REL_BLACKLIST.is_match(attr_value)
|
|
||||||
{
|
|
||||||
has_rel = true;
|
|
||||||
} else if attr_name == "href" {
|
|
||||||
href = Some(attr_value);
|
|
||||||
} else if attr_name == "sizes" {
|
|
||||||
sizes = Some(attr_value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if has_rel {
|
|
||||||
if let Some(inner_href) = href {
|
|
||||||
if let Ok(full_href) = url.join(inner_href).map(String::from) {
|
|
||||||
let priority = get_icon_priority(&full_href, sizes);
|
|
||||||
icons.push(Icon::new(priority, full_href));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Might want to limit the recursion depth?
|
for icon_tag in icon_tags {
|
||||||
for child in node.children.borrow().iter() {
|
if let Some(icon_href) = icon_tag.attributes.get(ATTR_HREF) {
|
||||||
get_favicons_node(child, icons, url);
|
if let Ok(full_href) = base_url.join(std::str::from_utf8(icon_href).unwrap_or_default()) {
|
||||||
|
let sizes = if let Some(v) = icon_tag.attributes.get(ATTR_SIZES) {
|
||||||
|
std::str::from_utf8(v).unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
let priority = get_icon_priority(full_href.as_str(), sizes);
|
||||||
|
icons.push(Icon::new(priority, full_href.to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,16 +480,16 @@ struct IconUrlResult {
|
|||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```
|
/// ```
|
||||||
/// let icon_result = get_icon_url("github.com")?;
|
/// let icon_result = get_icon_url("github.com").await?;
|
||||||
/// let icon_result = get_icon_url("vaultwarden.discourse.group")?;
|
/// let icon_result = get_icon_url("vaultwarden.discourse.group").await?;
|
||||||
/// ```
|
/// ```
|
||||||
fn get_icon_url(domain: &str) -> Result<IconUrlResult, Error> {
|
async fn get_icon_url(domain: &str) -> Result<IconUrlResult, Error> {
|
||||||
// Default URL with secure and insecure schemes
|
// Default URL with secure and insecure schemes
|
||||||
let ssldomain = format!("https://{}", domain);
|
let ssldomain = format!("https://{domain}");
|
||||||
let httpdomain = format!("http://{}", domain);
|
let httpdomain = format!("http://{domain}");
|
||||||
|
|
||||||
// First check the domain as given during the request for both HTTPS and HTTP.
|
// First check the domain as given during the request for both HTTPS and HTTP.
|
||||||
let resp = match get_page(&ssldomain).or_else(|_| get_page(&httpdomain)) {
|
let resp = match get_page(&ssldomain).or_else(|_| get_page(&httpdomain)).await {
|
||||||
Ok(c) => Ok(c),
|
Ok(c) => Ok(c),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let mut sub_resp = Err(e);
|
let mut sub_resp = Err(e);
|
||||||
@@ -542,32 +504,31 @@ fn get_icon_url(domain: &str) -> Result<IconUrlResult, Error> {
|
|||||||
base = domain_parts.next_back().unwrap()
|
base = domain_parts.next_back().unwrap()
|
||||||
);
|
);
|
||||||
if is_valid_domain(&base_domain) {
|
if is_valid_domain(&base_domain) {
|
||||||
let sslbase = format!("https://{}", base_domain);
|
let sslbase = format!("https://{base_domain}");
|
||||||
let httpbase = format!("http://{}", base_domain);
|
let httpbase = format!("http://{base_domain}");
|
||||||
debug!("[get_icon_url]: Trying without subdomains '{}'", base_domain);
|
debug!("[get_icon_url]: Trying without subdomains '{base_domain}'");
|
||||||
|
|
||||||
sub_resp = get_page(&sslbase).or_else(|_| get_page(&httpbase));
|
sub_resp = get_page(&sslbase).or_else(|_| get_page(&httpbase)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the domain is not an IP, and has less then 2 dots, try to add www. infront of it.
|
// When the domain is not an IP, and has less then 2 dots, try to add www. infront of it.
|
||||||
} else if is_ip.is_err() && domain.matches('.').count() < 2 {
|
} else if is_ip.is_err() && domain.matches('.').count() < 2 {
|
||||||
let www_domain = format!("www.{}", domain);
|
let www_domain = format!("www.{domain}");
|
||||||
if is_valid_domain(&www_domain) {
|
if is_valid_domain(&www_domain) {
|
||||||
let sslwww = format!("https://{}", www_domain);
|
let sslwww = format!("https://{www_domain}");
|
||||||
let httpwww = format!("http://{}", www_domain);
|
let httpwww = format!("http://{www_domain}");
|
||||||
debug!("[get_icon_url]: Trying with www. prefix '{}'", www_domain);
|
debug!("[get_icon_url]: Trying with www. prefix '{www_domain}'");
|
||||||
|
|
||||||
sub_resp = get_page(&sslwww).or_else(|_| get_page(&httpwww));
|
sub_resp = get_page(&sslwww).or_else(|_| get_page(&httpwww)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sub_resp
|
sub_resp
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the iconlist
|
// Create the iconlist
|
||||||
let mut iconlist: Vec<Icon> = Vec::new();
|
let mut iconlist: Vec<Icon> = Vec::new();
|
||||||
let mut referer = String::from("");
|
let mut referer = String::new();
|
||||||
|
|
||||||
if let Ok(content) = resp {
|
if let Ok(content) = resp {
|
||||||
// Extract the URL from the respose in case redirects occured (like @ gitlab.com)
|
// Extract the URL from the respose in case redirects occured (like @ gitlab.com)
|
||||||
@@ -575,26 +536,23 @@ fn get_icon_url(domain: &str) -> Result<IconUrlResult, Error> {
|
|||||||
|
|
||||||
// Set the referer to be used on the final request, some sites check this.
|
// Set the referer to be used on the final request, some sites check this.
|
||||||
// Mostly used to prevent direct linking and other security resons.
|
// Mostly used to prevent direct linking and other security resons.
|
||||||
referer = url.as_str().to_string();
|
referer = url.to_string();
|
||||||
|
|
||||||
// Add the default favicon.ico to the list with the domain the content responded from.
|
// Add the fallback favicon.ico and apple-touch-icon.png to the list with the domain the content responded from.
|
||||||
iconlist.push(Icon::new(35, String::from(url.join("/favicon.ico").unwrap())));
|
iconlist.push(Icon::new(35, String::from(url.join("/favicon.ico").unwrap())));
|
||||||
|
iconlist.push(Icon::new(40, String::from(url.join("/apple-touch-icon.png").unwrap())));
|
||||||
|
|
||||||
// 384KB should be more than enough for the HTML, though as we only really need the HTML header.
|
// 384KB should be more than enough for the HTML, though as we only really need the HTML header.
|
||||||
let mut limited_reader = content.take(384 * 1024);
|
let limited_reader = stream_to_bytes_limit(content, 384 * 1024).await?.to_vec();
|
||||||
|
|
||||||
use html5ever::tendril::TendrilSink;
|
let dom = Tokenizer::new_with_emitter(limited_reader.to_reader(), FaviconEmitter::default()).infallible();
|
||||||
let dom = html5ever::parse_document(markup5ever_rcdom::RcDom::default(), Default::default())
|
get_favicons_node(dom, &mut iconlist, &url);
|
||||||
.from_utf8()
|
|
||||||
.read_from(&mut limited_reader)?;
|
|
||||||
|
|
||||||
let mut base_url: url::Url = url;
|
|
||||||
get_base_href(&dom.document, &mut base_url);
|
|
||||||
get_favicons_node(&dom.document, &mut iconlist, &base_url);
|
|
||||||
} else {
|
} else {
|
||||||
// Add the default favicon.ico to the list with just the given domain
|
// Add the default favicon.ico to the list with just the given domain
|
||||||
iconlist.push(Icon::new(35, format!("{}/favicon.ico", ssldomain)));
|
iconlist.push(Icon::new(35, format!("{ssldomain}/favicon.ico")));
|
||||||
iconlist.push(Icon::new(35, format!("{}/favicon.ico", httpdomain)));
|
iconlist.push(Icon::new(40, format!("{ssldomain}/apple-touch-icon.png")));
|
||||||
|
iconlist.push(Icon::new(35, format!("{httpdomain}/favicon.ico")));
|
||||||
|
iconlist.push(Icon::new(40, format!("{httpdomain}/apple-touch-icon.png")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the iconlist by priority
|
// Sort the iconlist by priority
|
||||||
@@ -607,13 +565,15 @@ fn get_icon_url(domain: &str) -> Result<IconUrlResult, Error> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_page(url: &str) -> Result<Response, Error> {
|
async fn get_page(url: &str) -> Result<Response, Error> {
|
||||||
get_page_with_referer(url, "")
|
get_page_with_referer(url, "").await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_page_with_referer(url: &str, referer: &str) -> Result<Response, Error> {
|
async fn get_page_with_referer(url: &str, referer: &str) -> Result<Response, Error> {
|
||||||
if is_domain_blacklisted(url::Url::parse(url).unwrap().host_str().unwrap_or_default()) {
|
match check_domain_blacklist_reason(url::Url::parse(url).unwrap().host_str().unwrap_or_default()).await {
|
||||||
warn!("Favicon '{}' resolves to a blacklisted domain or IP!", url);
|
Some(DomainBlacklistReason::Regex) => warn!("Favicon '{}' is from a blacklisted domain!", url),
|
||||||
|
Some(DomainBlacklistReason::IP) => warn!("Favicon '{}' is hosted on a non-global IP!", url),
|
||||||
|
None => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut client = CLIENT.get(url);
|
let mut client = CLIENT.get(url);
|
||||||
@@ -621,9 +581,9 @@ fn get_page_with_referer(url: &str, referer: &str) -> Result<Response, Error> {
|
|||||||
client = client.header("Referer", referer)
|
client = client.header("Referer", referer)
|
||||||
}
|
}
|
||||||
|
|
||||||
match client.send() {
|
match client.send().await {
|
||||||
Ok(c) => c.error_for_status().map_err(Into::into),
|
Ok(c) => c.error_for_status().map_err(Into::into),
|
||||||
Err(e) => err_silent!(format!("{}", e)),
|
Err(e) => err_silent!(format!("{e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,7 +599,7 @@ fn get_page_with_referer(url: &str, referer: &str) -> Result<Response, Error> {
|
|||||||
/// priority1 = get_icon_priority("http://example.com/path/to/a/favicon.png", "32x32");
|
/// priority1 = get_icon_priority("http://example.com/path/to/a/favicon.png", "32x32");
|
||||||
/// priority2 = get_icon_priority("https://example.com/path/to/a/favicon.ico", "");
|
/// priority2 = get_icon_priority("https://example.com/path/to/a/favicon.ico", "");
|
||||||
/// ```
|
/// ```
|
||||||
fn get_icon_priority(href: &str, sizes: Option<&str>) -> u8 {
|
fn get_icon_priority(href: &str, sizes: &str) -> u8 {
|
||||||
// Check if there is a dimension set
|
// Check if there is a dimension set
|
||||||
let (width, height) = parse_sizes(sizes);
|
let (width, height) = parse_sizes(sizes);
|
||||||
|
|
||||||
@@ -687,11 +647,11 @@ fn get_icon_priority(href: &str, sizes: Option<&str>) -> u8 {
|
|||||||
/// let (width, height) = parse_sizes("x128x128"); // (128, 128)
|
/// let (width, height) = parse_sizes("x128x128"); // (128, 128)
|
||||||
/// let (width, height) = parse_sizes("32"); // (0, 0)
|
/// let (width, height) = parse_sizes("32"); // (0, 0)
|
||||||
/// ```
|
/// ```
|
||||||
fn parse_sizes(sizes: Option<&str>) -> (u16, u16) {
|
fn parse_sizes(sizes: &str) -> (u16, u16) {
|
||||||
let mut width: u16 = 0;
|
let mut width: u16 = 0;
|
||||||
let mut height: u16 = 0;
|
let mut height: u16 = 0;
|
||||||
|
|
||||||
if let Some(sizes) = sizes {
|
if !sizes.is_empty() {
|
||||||
match ICON_SIZE_REGEX.captures(sizes.trim()) {
|
match ICON_SIZE_REGEX.captures(sizes.trim()) {
|
||||||
None => {}
|
None => {}
|
||||||
Some(dimensions) => {
|
Some(dimensions) => {
|
||||||
@@ -706,14 +666,16 @@ fn parse_sizes(sizes: Option<&str>) -> (u16, u16) {
|
|||||||
(width, height)
|
(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_icon(domain: &str) -> Result<(Vec<u8>, Option<&str>), Error> {
|
async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
|
||||||
if is_domain_blacklisted(domain) {
|
match check_domain_blacklist_reason(domain).await {
|
||||||
err_silent!("Domain is blacklisted", domain)
|
Some(DomainBlacklistReason::Regex) => err_silent!("Domain is blacklisted", domain),
|
||||||
|
Some(DomainBlacklistReason::IP) => err_silent!("Host resolves to a non-global IP", domain),
|
||||||
|
None => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
let icon_result = get_icon_url(domain)?;
|
let icon_result = get_icon_url(domain).await?;
|
||||||
|
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Bytes::new();
|
||||||
let mut icon_type: Option<&str> = None;
|
let mut icon_type: Option<&str> = None;
|
||||||
|
|
||||||
use data_url::DataUrl;
|
use data_url::DataUrl;
|
||||||
@@ -722,8 +684,12 @@ fn download_icon(domain: &str) -> Result<(Vec<u8>, Option<&str>), Error> {
|
|||||||
if icon.href.starts_with("data:image") {
|
if icon.href.starts_with("data:image") {
|
||||||
let datauri = DataUrl::process(&icon.href).unwrap();
|
let datauri = DataUrl::process(&icon.href).unwrap();
|
||||||
// Check if we are able to decode the data uri
|
// Check if we are able to decode the data uri
|
||||||
match datauri.decode_to_vec() {
|
let mut body = BytesMut::new();
|
||||||
Ok((body, _fragment)) => {
|
match datauri.decode::<_, ()>(|bytes| {
|
||||||
|
body.extend_from_slice(bytes);
|
||||||
|
Ok(())
|
||||||
|
}) {
|
||||||
|
Ok(_) => {
|
||||||
// Also check if the size is atleast 67 bytes, which seems to be the smallest png i could create
|
// Also check if the size is atleast 67 bytes, which seems to be the smallest png i could create
|
||||||
if body.len() >= 67 {
|
if body.len() >= 67 {
|
||||||
// Check if the icon type is allowed, else try an icon from the list.
|
// Check if the icon type is allowed, else try an icon from the list.
|
||||||
@@ -733,16 +699,17 @@ fn download_icon(domain: &str) -> Result<(Vec<u8>, Option<&str>), Error> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
info!("Extracted icon from data:image uri for {}", domain);
|
info!("Extracted icon from data:image uri for {}", domain);
|
||||||
buffer = body;
|
buffer = body.freeze();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => debug!("Extracted icon from data:image uri is invalid"),
|
_ => debug!("Extracted icon from data:image uri is invalid"),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match get_page_with_referer(&icon.href, &icon_result.referer) {
|
match get_page_with_referer(&icon.href, &icon_result.referer).await {
|
||||||
Ok(mut res) => {
|
Ok(res) => {
|
||||||
res.copy_to(&mut buffer)?;
|
buffer = stream_to_bytes_limit(res, 5120 * 1024).await?; // 5120KB/5MB for each icon max (Same as icons.bitwarden.net)
|
||||||
|
|
||||||
// Check if the icon type is allowed, else try an icon from the list.
|
// Check if the icon type is allowed, else try an icon from the list.
|
||||||
icon_type = get_icon_type(&buffer);
|
icon_type = get_icon_type(&buffer);
|
||||||
if icon_type.is_none() {
|
if icon_type.is_none() {
|
||||||
@@ -765,13 +732,13 @@ fn download_icon(domain: &str) -> Result<(Vec<u8>, Option<&str>), Error> {
|
|||||||
Ok((buffer, icon_type))
|
Ok((buffer, icon_type))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_icon(path: &str, icon: &[u8]) {
|
async fn save_icon(path: &str, icon: &[u8]) {
|
||||||
match File::create(path) {
|
match File::create(path).await {
|
||||||
Ok(mut f) => {
|
Ok(mut f) => {
|
||||||
f.write_all(icon).expect("Error writing icon file");
|
f.write_all(icon).await.expect("Error writing icon file");
|
||||||
}
|
}
|
||||||
Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => {
|
Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||||
create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache folder");
|
create_dir_all(&CONFIG.icon_cache_folder()).await.expect("Error creating icon cache folder");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Unable to save icon: {:?}", e);
|
warn!("Unable to save icon: {:?}", e);
|
||||||
@@ -791,13 +758,30 @@ fn get_icon_type(bytes: &[u8]) -> Option<&'static str> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Minimize the amount of bytes to be parsed from a reqwest result.
|
||||||
|
/// This prevents very long parsing and memory usage.
|
||||||
|
async fn stream_to_bytes_limit(res: Response, max_size: usize) -> Result<Bytes, reqwest::Error> {
|
||||||
|
let mut stream = res.bytes_stream().take(max_size);
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
let mut size = 0;
|
||||||
|
while let Some(chunk) = stream.next().await {
|
||||||
|
let chunk = &chunk?;
|
||||||
|
size += chunk.len();
|
||||||
|
buf.extend(chunk);
|
||||||
|
if size >= max_size {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(buf.freeze())
|
||||||
|
}
|
||||||
|
|
||||||
/// This is an implementation of the default Cookie Jar from Reqwest and reqwest_cookie_store build by pfernie.
|
/// This is an implementation of the default Cookie Jar from Reqwest and reqwest_cookie_store build by pfernie.
|
||||||
/// The default cookie jar used by Reqwest keeps all the cookies based upon the Max-Age or Expires which could be a long time.
|
/// The default cookie jar used by Reqwest keeps all the cookies based upon the Max-Age or Expires which could be a long time.
|
||||||
/// That could be used for tracking, to prevent this we force the lifespan of the cookies to always be max two minutes.
|
/// That could be used for tracking, to prevent this we force the lifespan of the cookies to always be max two minutes.
|
||||||
/// A Cookie Jar is needed because some sites force a redirect with cookies to verify if a request uses cookies or not.
|
/// A Cookie Jar is needed because some sites force a redirect with cookies to verify if a request uses cookies or not.
|
||||||
use cookie_store::CookieStore;
|
use cookie_store::CookieStore;
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Jar(RwLock<CookieStore>);
|
pub struct Jar(std::sync::RwLock<CookieStore>);
|
||||||
|
|
||||||
impl reqwest::cookie::CookieStore for Jar {
|
impl reqwest::cookie::CookieStore for Jar {
|
||||||
fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &header::HeaderValue>, url: &url::Url) {
|
fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &header::HeaderValue>, url: &url::Url) {
|
||||||
@@ -820,12 +804,10 @@ impl reqwest::cookie::CookieStore for Jar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cookies(&self, url: &url::Url) -> Option<header::HeaderValue> {
|
fn cookies(&self, url: &url::Url) -> Option<header::HeaderValue> {
|
||||||
use bytes::Bytes;
|
|
||||||
|
|
||||||
let cookie_store = self.0.read().unwrap();
|
let cookie_store = self.0.read().unwrap();
|
||||||
let s = cookie_store
|
let s = cookie_store
|
||||||
.get_request_values(url)
|
.get_request_values(url)
|
||||||
.map(|(name, value)| format!("{}={}", name, value))
|
.map(|(name, value)| format!("{name}={value}"))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("; ");
|
.join("; ");
|
||||||
|
|
||||||
@@ -836,3 +818,158 @@ impl reqwest::cookie::CookieStore for Jar {
|
|||||||
header::HeaderValue::from_maybe_shared(Bytes::from(s)).ok()
|
header::HeaderValue::from_maybe_shared(Bytes::from(s)).ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Custom FaviconEmitter for the html5gum parser.
|
||||||
|
/// The FaviconEmitter is using an almost 1:1 copy of the DefaultEmitter with some small changes.
|
||||||
|
/// This prevents emitting tags like comments, doctype and also strings between the tags.
|
||||||
|
/// Therefor parsing the HTML content is faster.
|
||||||
|
use std::collections::{BTreeSet, VecDeque};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum FaviconToken {
|
||||||
|
StartTag(StartTag),
|
||||||
|
EndTag(EndTag),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct FaviconEmitter {
|
||||||
|
current_token: Option<FaviconToken>,
|
||||||
|
last_start_tag: HtmlString,
|
||||||
|
current_attribute: Option<(HtmlString, HtmlString)>,
|
||||||
|
seen_attributes: BTreeSet<HtmlString>,
|
||||||
|
emitted_tokens: VecDeque<FaviconToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FaviconEmitter {
|
||||||
|
fn emit_token(&mut self, token: FaviconToken) {
|
||||||
|
self.emitted_tokens.push_front(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush_current_attribute(&mut self) {
|
||||||
|
if let Some((k, v)) = self.current_attribute.take() {
|
||||||
|
match self.current_token {
|
||||||
|
Some(FaviconToken::StartTag(ref mut tag)) => {
|
||||||
|
tag.attributes.entry(k).and_modify(|_| {}).or_insert(v);
|
||||||
|
}
|
||||||
|
Some(FaviconToken::EndTag(_)) => {
|
||||||
|
self.seen_attributes.insert(k);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
debug_assert!(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Emitter for FaviconEmitter {
|
||||||
|
type Token = FaviconToken;
|
||||||
|
|
||||||
|
fn set_last_start_tag(&mut self, last_start_tag: Option<&[u8]>) {
|
||||||
|
self.last_start_tag.clear();
|
||||||
|
self.last_start_tag.extend(last_start_tag.unwrap_or_default());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_token(&mut self) -> Option<Self::Token> {
|
||||||
|
self.emitted_tokens.pop_back()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_start_tag(&mut self) {
|
||||||
|
self.current_token = Some(FaviconToken::StartTag(StartTag::default()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_end_tag(&mut self) {
|
||||||
|
self.current_token = Some(FaviconToken::EndTag(EndTag::default()));
|
||||||
|
self.seen_attributes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_current_tag(&mut self) -> Option<html5gum::State> {
|
||||||
|
self.flush_current_attribute();
|
||||||
|
let mut token = self.current_token.take().unwrap();
|
||||||
|
let mut emit = false;
|
||||||
|
match token {
|
||||||
|
FaviconToken::EndTag(ref mut tag) => {
|
||||||
|
// Always clean seen attributes
|
||||||
|
self.seen_attributes.clear();
|
||||||
|
|
||||||
|
// Only trigger an emit for the </head> tag.
|
||||||
|
// This is matched, and will break the for-loop.
|
||||||
|
if *tag.name == b"head" {
|
||||||
|
emit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FaviconToken::StartTag(ref mut tag) => {
|
||||||
|
// Only trriger an emit for <link> and <base> tags.
|
||||||
|
// These are the only tags we want to parse.
|
||||||
|
if *tag.name == b"link" || *tag.name == b"base" {
|
||||||
|
self.set_last_start_tag(Some(&tag.name));
|
||||||
|
emit = true;
|
||||||
|
} else {
|
||||||
|
self.set_last_start_tag(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only emit the tags we want to parse.
|
||||||
|
if emit {
|
||||||
|
self.emit_token(token);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_tag_name(&mut self, s: &[u8]) {
|
||||||
|
match self.current_token {
|
||||||
|
Some(
|
||||||
|
FaviconToken::StartTag(StartTag {
|
||||||
|
ref mut name,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
| FaviconToken::EndTag(EndTag {
|
||||||
|
ref mut name,
|
||||||
|
..
|
||||||
|
}),
|
||||||
|
) => {
|
||||||
|
name.extend(s);
|
||||||
|
}
|
||||||
|
_ => debug_assert!(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_attribute(&mut self) {
|
||||||
|
self.flush_current_attribute();
|
||||||
|
self.current_attribute = Some(Default::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_attribute_name(&mut self, s: &[u8]) {
|
||||||
|
self.current_attribute.as_mut().unwrap().0.extend(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_attribute_value(&mut self, s: &[u8]) {
|
||||||
|
self.current_attribute.as_mut().unwrap().1.extend(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_is_appropriate_end_tag_token(&mut self) -> bool {
|
||||||
|
match self.current_token {
|
||||||
|
Some(FaviconToken::EndTag(ref tag)) => !self.last_start_tag.is_empty() && self.last_start_tag == tag.name,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do not want and need these parts of the HTML document
|
||||||
|
// These will be skipped and ignored during the tokenization and iteration.
|
||||||
|
fn emit_current_comment(&mut self) {}
|
||||||
|
fn emit_current_doctype(&mut self) {}
|
||||||
|
fn emit_eof(&mut self) {}
|
||||||
|
fn emit_error(&mut self, _: html5gum::Error) {}
|
||||||
|
fn emit_string(&mut self, _: &[u8]) {}
|
||||||
|
fn init_comment(&mut self) {}
|
||||||
|
fn init_doctype(&mut self) {}
|
||||||
|
fn push_comment(&mut self, _: &[u8]) {}
|
||||||
|
fn push_doctype_name(&mut self, _: &[u8]) {}
|
||||||
|
fn push_doctype_public_identifier(&mut self, _: &[u8]) {}
|
||||||
|
fn push_doctype_system_identifier(&mut self, _: &[u8]) {}
|
||||||
|
fn set_doctype_public_identifier(&mut self, _: &[u8]) {}
|
||||||
|
fn set_doctype_system_identifier(&mut self, _: &[u8]) {}
|
||||||
|
fn set_force_quirks(&mut self) {}
|
||||||
|
fn set_self_closing(&mut self) {}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,35 +1,39 @@
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
|
use rocket::serde::json::Json;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
request::{Form, FormItems, FromForm},
|
form::{Form, FromForm},
|
||||||
Route,
|
Route,
|
||||||
};
|
};
|
||||||
use rocket_contrib::json::Json;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
|
core::accounts::{PreloginData, RegisterData, _prelogin, _register},
|
||||||
|
core::log_user_event,
|
||||||
core::two_factor::{duo, email, email::EmailTokenData, yubikey},
|
core::two_factor::{duo, email, email::EmailTokenData, yubikey},
|
||||||
ApiResult, EmptyResult, JsonResult,
|
ApiResult, EmptyResult, JsonResult, JsonUpcase,
|
||||||
},
|
},
|
||||||
auth::ClientIp,
|
auth::{ClientHeaders, ClientIp},
|
||||||
db::{models::*, DbConn},
|
db::{models::*, DbConn},
|
||||||
error::MapResult,
|
error::MapResult,
|
||||||
mail, util, CONFIG,
|
mail, util, CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![login]
|
routes![login, prelogin, identity_register]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/connect/token", data = "<data>")]
|
#[post("/connect/token", data = "<data>")]
|
||||||
fn login(data: Form<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResult {
|
async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn: DbConn) -> JsonResult {
|
||||||
let data: ConnectData = data.into_inner();
|
let data: ConnectData = data.into_inner();
|
||||||
|
|
||||||
match data.grant_type.as_ref() {
|
let mut user_uuid: Option<String> = None;
|
||||||
|
|
||||||
|
let login_result = match data.grant_type.as_ref() {
|
||||||
"refresh_token" => {
|
"refresh_token" => {
|
||||||
_check_is_some(&data.refresh_token, "refresh_token cannot be blank")?;
|
_check_is_some(&data.refresh_token, "refresh_token cannot be blank")?;
|
||||||
_refresh_login(data, conn)
|
_refresh_login(data, &mut conn).await
|
||||||
}
|
}
|
||||||
"password" => {
|
"password" => {
|
||||||
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
||||||
@@ -41,36 +45,69 @@ fn login(data: Form<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResult {
|
|||||||
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||||
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
_password_login(data, conn, &ip)
|
_password_login(data, &mut user_uuid, &mut conn, &client_header.ip).await
|
||||||
}
|
}
|
||||||
"client_credentials" => {
|
"client_credentials" => {
|
||||||
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
||||||
_check_is_some(&data.client_secret, "client_secret cannot be blank")?;
|
_check_is_some(&data.client_secret, "client_secret cannot be blank")?;
|
||||||
_check_is_some(&data.scope, "scope cannot be blank")?;
|
_check_is_some(&data.scope, "scope cannot be blank")?;
|
||||||
|
|
||||||
_api_key_login(data, conn, &ip)
|
_check_is_some(&data.device_identifier, "device_identifier cannot be blank")?;
|
||||||
|
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||||
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
|
_api_key_login(data, &mut user_uuid, &mut conn, &client_header.ip).await
|
||||||
}
|
}
|
||||||
t => err!("Invalid type", t),
|
t => err!("Invalid type", t),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(user_uuid) = user_uuid {
|
||||||
|
match &login_result {
|
||||||
|
Ok(_) => {
|
||||||
|
log_user_event(
|
||||||
|
EventType::UserLoggedIn as i32,
|
||||||
|
&user_uuid,
|
||||||
|
client_header.device_type,
|
||||||
|
&client_header.ip.ip,
|
||||||
|
&mut conn,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if let Some(ev) = e.get_event() {
|
||||||
|
log_user_event(
|
||||||
|
ev.event as i32,
|
||||||
|
&user_uuid,
|
||||||
|
client_header.device_type,
|
||||||
|
&client_header.ip.ip,
|
||||||
|
&mut conn,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
|
login_result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
|
||||||
// Extract token
|
// Extract token
|
||||||
let token = data.refresh_token.unwrap();
|
let token = data.refresh_token.unwrap();
|
||||||
|
|
||||||
// Get device by refresh token
|
// Get device by refresh token
|
||||||
let mut device = Device::find_by_refresh_token(&token, &conn).map_res("Invalid refresh token")?;
|
let mut device = Device::find_by_refresh_token(&token, conn).await.map_res("Invalid refresh token")?;
|
||||||
|
|
||||||
let scope = "api offline_access";
|
let scope = "api offline_access";
|
||||||
let scope_vec = vec!["api".into(), "offline_access".into()];
|
let scope_vec = vec!["api".into(), "offline_access".into()];
|
||||||
|
|
||||||
// Common
|
// Common
|
||||||
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
|
let user = User::find_by_uuid(&device.user_uuid, conn).await.unwrap();
|
||||||
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &conn);
|
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
||||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
|
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
|
||||||
device.save(&conn)?;
|
device.save(conn).await?;
|
||||||
|
|
||||||
Ok(Json(json!({
|
let result = json!({
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"expires_in": expires_in,
|
"expires_in": expires_in,
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
@@ -80,13 +117,22 @@ fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
|
|||||||
|
|
||||||
"Kdf": user.client_kdf_type,
|
"Kdf": user.client_kdf_type,
|
||||||
"KdfIterations": user.client_kdf_iter,
|
"KdfIterations": user.client_kdf_iter,
|
||||||
|
"KdfMemory": user.client_kdf_memory,
|
||||||
|
"KdfParallelism": user.client_kdf_parallelism,
|
||||||
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
|
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
|
||||||
"scope": scope,
|
"scope": scope,
|
||||||
"unofficialServer": true,
|
"unofficialServer": true,
|
||||||
})))
|
});
|
||||||
|
|
||||||
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult {
|
async fn _password_login(
|
||||||
|
data: ConnectData,
|
||||||
|
user_uuid: &mut Option<String>,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
ip: &ClientIp,
|
||||||
|
) -> JsonResult {
|
||||||
// Validate scope
|
// Validate scope
|
||||||
let scope = data.scope.as_ref().unwrap();
|
let scope = data.scope.as_ref().unwrap();
|
||||||
if scope != "api offline_access" {
|
if scope != "api offline_access" {
|
||||||
@@ -98,21 +144,46 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult
|
|||||||
crate::ratelimit::check_limit_login(&ip.ip)?;
|
crate::ratelimit::check_limit_login(&ip.ip)?;
|
||||||
|
|
||||||
// Get the user
|
// Get the user
|
||||||
let username = data.username.as_ref().unwrap();
|
let username = data.username.as_ref().unwrap().trim();
|
||||||
let user = match User::find_by_mail(username, &conn) {
|
let mut user = match User::find_by_mail(username, conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)),
|
None => err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set the user_uuid here to be passed back used for event logging.
|
||||||
|
*user_uuid = Some(user.uuid.clone());
|
||||||
|
|
||||||
// Check password
|
// Check password
|
||||||
let password = data.password.as_ref().unwrap();
|
let password = data.password.as_ref().unwrap();
|
||||||
if !user.check_valid_password(password) {
|
if !user.check_valid_password(password) {
|
||||||
err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username))
|
err!(
|
||||||
|
"Username or password is incorrect. Try again",
|
||||||
|
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the KDF Iterations
|
||||||
|
if user.password_iterations != CONFIG.password_iterations() {
|
||||||
|
user.password_iterations = CONFIG.password_iterations();
|
||||||
|
user.set_password(password, None, false, None);
|
||||||
|
|
||||||
|
if let Err(e) = user.save(conn).await {
|
||||||
|
error!("Error updating user: {:#?}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user is disabled
|
// Check if the user is disabled
|
||||||
if !user.enabled {
|
if !user.enabled {
|
||||||
err!("This user has been disabled", format!("IP: {}. Username: {}.", ip.ip, username))
|
err!(
|
||||||
|
"This user has been disabled",
|
||||||
|
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Utc::now().naive_utc();
|
let now = Utc::now().naive_utc();
|
||||||
@@ -126,42 +197,52 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult
|
|||||||
if resend_limit == 0 || user.login_verify_count < resend_limit {
|
if resend_limit == 0 || user.login_verify_count < resend_limit {
|
||||||
// We want to send another email verification if we require signups to verify
|
// We want to send another email verification if we require signups to verify
|
||||||
// their email address, and we haven't sent them a reminder in a while...
|
// their email address, and we haven't sent them a reminder in a while...
|
||||||
let mut user = user;
|
|
||||||
user.last_verifying_at = Some(now);
|
user.last_verifying_at = Some(now);
|
||||||
user.login_verify_count += 1;
|
user.login_verify_count += 1;
|
||||||
|
|
||||||
if let Err(e) = user.save(&conn) {
|
if let Err(e) = user.save(conn).await {
|
||||||
error!("Error updating user: {:#?}", e);
|
error!("Error updating user: {:#?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = mail::send_verify_email(&user.email, &user.uuid) {
|
if let Err(e) = mail::send_verify_email(&user.email, &user.uuid).await {
|
||||||
error!("Error auto-sending email verification email: {:#?}", e);
|
error!("Error auto-sending email verification email: {:#?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We still want the login to fail until they actually verified the email address
|
// We still want the login to fail until they actually verified the email address
|
||||||
err!("Please verify your email before trying again.", format!("IP: {}. Username: {}.", ip.ip, username))
|
err!(
|
||||||
|
"Please verify your email before trying again.",
|
||||||
|
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let (mut device, new_device) = get_device(&data, &conn, &user);
|
let (mut device, new_device) = get_device(&data, conn, &user).await;
|
||||||
|
|
||||||
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, ip, &conn)?;
|
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, ip, conn).await?;
|
||||||
|
|
||||||
if CONFIG.mail_enabled() && new_device {
|
if CONFIG.mail_enabled() && new_device {
|
||||||
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name) {
|
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await {
|
||||||
error!("Error sending new device email: {:#?}", e);
|
error!("Error sending new device email: {:#?}", e);
|
||||||
|
|
||||||
if CONFIG.require_device_email() {
|
if CONFIG.require_device_email() {
|
||||||
err!("Could not send login notification email. Please contact your administrator.")
|
err!(
|
||||||
|
"Could not send login notification email. Please contact your administrator.",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common
|
// Common
|
||||||
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &conn);
|
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
||||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
|
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
|
||||||
device.save(&conn)?;
|
device.save(conn).await?;
|
||||||
|
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
@@ -174,6 +255,8 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult
|
|||||||
|
|
||||||
"Kdf": user.client_kdf_type,
|
"Kdf": user.client_kdf_type,
|
||||||
"KdfIterations": user.client_kdf_iter,
|
"KdfIterations": user.client_kdf_iter,
|
||||||
|
"KdfMemory": user.client_kdf_memory,
|
||||||
|
"KdfParallelism": user.client_kdf_parallelism,
|
||||||
"ResetMasterPassword": false,// TODO: Same as above
|
"ResetMasterPassword": false,// TODO: Same as above
|
||||||
"scope": scope,
|
"scope": scope,
|
||||||
"unofficialServer": true,
|
"unofficialServer": true,
|
||||||
@@ -187,7 +270,12 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult
|
|||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _api_key_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult {
|
async fn _api_key_login(
|
||||||
|
data: ConnectData,
|
||||||
|
user_uuid: &mut Option<String>,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
ip: &ClientIp,
|
||||||
|
) -> JsonResult {
|
||||||
// Validate scope
|
// Validate scope
|
||||||
let scope = data.scope.as_ref().unwrap();
|
let scope = data.scope.as_ref().unwrap();
|
||||||
if scope != "api" {
|
if scope != "api" {
|
||||||
@@ -200,49 +288,69 @@ fn _api_key_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult
|
|||||||
|
|
||||||
// Get the user via the client_id
|
// Get the user via the client_id
|
||||||
let client_id = data.client_id.as_ref().unwrap();
|
let client_id = data.client_id.as_ref().unwrap();
|
||||||
let user_uuid = match client_id.strip_prefix("user.") {
|
let client_user_uuid = match client_id.strip_prefix("user.") {
|
||||||
Some(uuid) => uuid,
|
Some(uuid) => uuid,
|
||||||
None => err!("Malformed client_id", format!("IP: {}.", ip.ip)),
|
None => err!("Malformed client_id", format!("IP: {}.", ip.ip)),
|
||||||
};
|
};
|
||||||
let user = match User::find_by_uuid(user_uuid, &conn) {
|
let user = match User::find_by_uuid(client_user_uuid, conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("Invalid client_id", format!("IP: {}.", ip.ip)),
|
None => err!("Invalid client_id", format!("IP: {}.", ip.ip)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set the user_uuid here to be passed back used for event logging.
|
||||||
|
*user_uuid = Some(user.uuid.clone());
|
||||||
|
|
||||||
// Check if the user is disabled
|
// Check if the user is disabled
|
||||||
if !user.enabled {
|
if !user.enabled {
|
||||||
err!("This user has been disabled (API key login)", format!("IP: {}. Username: {}.", ip.ip, user.email))
|
err!(
|
||||||
|
"This user has been disabled (API key login)",
|
||||||
|
format!("IP: {}. Username: {}.", ip.ip, user.email),
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check API key. Note that API key logins bypass 2FA.
|
// Check API key. Note that API key logins bypass 2FA.
|
||||||
let client_secret = data.client_secret.as_ref().unwrap();
|
let client_secret = data.client_secret.as_ref().unwrap();
|
||||||
if !user.check_valid_api_key(client_secret) {
|
if !user.check_valid_api_key(client_secret) {
|
||||||
err!("Incorrect client_secret", format!("IP: {}. Username: {}.", ip.ip, user.email))
|
err!(
|
||||||
|
"Incorrect client_secret",
|
||||||
|
format!("IP: {}. Username: {}.", ip.ip, user.email),
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let (mut device, new_device) = get_device(&data, &conn, &user);
|
let (mut device, new_device) = get_device(&data, conn, &user).await;
|
||||||
|
|
||||||
if CONFIG.mail_enabled() && new_device {
|
if CONFIG.mail_enabled() && new_device {
|
||||||
let now = Utc::now().naive_utc();
|
let now = Utc::now().naive_utc();
|
||||||
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name) {
|
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await {
|
||||||
error!("Error sending new device email: {:#?}", e);
|
error!("Error sending new device email: {:#?}", e);
|
||||||
|
|
||||||
if CONFIG.require_device_email() {
|
if CONFIG.require_device_email() {
|
||||||
err!("Could not send login notification email. Please contact your administrator.")
|
err!(
|
||||||
|
"Could not send login notification email. Please contact your administrator.",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common
|
// Common
|
||||||
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &conn);
|
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
||||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
|
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
|
||||||
device.save(&conn)?;
|
device.save(conn).await?;
|
||||||
|
|
||||||
info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);
|
info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);
|
||||||
|
|
||||||
// Note: No refresh_token is returned. The CLI just repeats the
|
// Note: No refresh_token is returned. The CLI just repeats the
|
||||||
// client_credentials login flow when the existing token expires.
|
// client_credentials login flow when the existing token expires.
|
||||||
Ok(Json(json!({
|
let result = json!({
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"expires_in": expires_in,
|
"expires_in": expires_in,
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
@@ -251,32 +359,28 @@ fn _api_key_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult
|
|||||||
|
|
||||||
"Kdf": user.client_kdf_type,
|
"Kdf": user.client_kdf_type,
|
||||||
"KdfIterations": user.client_kdf_iter,
|
"KdfIterations": user.client_kdf_iter,
|
||||||
|
"KdfMemory": user.client_kdf_memory,
|
||||||
|
"KdfParallelism": user.client_kdf_parallelism,
|
||||||
"ResetMasterPassword": false, // TODO: Same as above
|
"ResetMasterPassword": false, // TODO: Same as above
|
||||||
"scope": scope,
|
"scope": scope,
|
||||||
"unofficialServer": true,
|
"unofficialServer": true,
|
||||||
})))
|
});
|
||||||
|
|
||||||
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves an existing device or creates a new device from ConnectData and the User
|
/// Retrieves an existing device or creates a new device from ConnectData and the User
|
||||||
fn get_device(data: &ConnectData, conn: &DbConn, user: &User) -> (Device, bool) {
|
async fn get_device(data: &ConnectData, conn: &mut DbConn, user: &User) -> (Device, bool) {
|
||||||
// On iOS, device_type sends "iOS", on others it sends a number
|
// On iOS, device_type sends "iOS", on others it sends a number
|
||||||
let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(0);
|
// When unknown or unable to parse, return 14, which is 'Unknown Browser'
|
||||||
|
let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(14);
|
||||||
let device_id = data.device_identifier.clone().expect("No device id provided");
|
let device_id = data.device_identifier.clone().expect("No device id provided");
|
||||||
let device_name = data.device_name.clone().expect("No device name provided");
|
let device_name = data.device_name.clone().expect("No device name provided");
|
||||||
|
|
||||||
let mut new_device = false;
|
let mut new_device = false;
|
||||||
// Find device or create new
|
// Find device or create new
|
||||||
let device = match Device::find_by_uuid(&device_id, conn) {
|
let device = match Device::find_by_uuid_and_user(&device_id, &user.uuid, conn).await {
|
||||||
Some(device) => {
|
Some(device) => device,
|
||||||
// Check if owned device, and recreate if not
|
|
||||||
if device.user_uuid != user.uuid {
|
|
||||||
info!("Device exists but is owned by another user. The old device will be discarded");
|
|
||||||
new_device = true;
|
|
||||||
Device::new(device_id, user.uuid.clone(), device_name, device_type)
|
|
||||||
} else {
|
|
||||||
device
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
None => {
|
||||||
new_device = true;
|
new_device = true;
|
||||||
Device::new(device_id, user.uuid.clone(), device_name, device_type)
|
Device::new(device_id, user.uuid.clone(), device_name, device_type)
|
||||||
@@ -286,28 +390,28 @@ fn get_device(data: &ConnectData, conn: &DbConn, user: &User) -> (Device, bool)
|
|||||||
(device, new_device)
|
(device, new_device)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn twofactor_auth(
|
async fn twofactor_auth(
|
||||||
user_uuid: &str,
|
user_uuid: &str,
|
||||||
data: &ConnectData,
|
data: &ConnectData,
|
||||||
device: &mut Device,
|
device: &mut Device,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
conn: &DbConn,
|
conn: &mut DbConn,
|
||||||
) -> ApiResult<Option<String>> {
|
) -> ApiResult<Option<String>> {
|
||||||
let twofactors = TwoFactor::find_by_user(user_uuid, conn);
|
let twofactors = TwoFactor::find_by_user(user_uuid, conn).await;
|
||||||
|
|
||||||
// No twofactor token if twofactor is disabled
|
// No twofactor token if twofactor is disabled
|
||||||
if twofactors.is_empty() {
|
if twofactors.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
TwoFactorIncomplete::mark_incomplete(user_uuid, &device.uuid, &device.name, ip, conn)?;
|
TwoFactorIncomplete::mark_incomplete(user_uuid, &device.uuid, &device.name, ip, conn).await?;
|
||||||
|
|
||||||
let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect();
|
let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect();
|
||||||
let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, asume the first one
|
let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, asume the first one
|
||||||
|
|
||||||
let twofactor_code = match data.two_factor_token {
|
let twofactor_code = match data.two_factor_token {
|
||||||
Some(ref code) => code,
|
Some(ref code) => code,
|
||||||
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?, "2FA token not provided"),
|
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn).await?, "2FA token not provided"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled);
|
let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled);
|
||||||
@@ -320,16 +424,17 @@ fn twofactor_auth(
|
|||||||
|
|
||||||
match TwoFactorType::from_i32(selected_id) {
|
match TwoFactorType::from_i32(selected_id) {
|
||||||
Some(TwoFactorType::Authenticator) => {
|
Some(TwoFactorType::Authenticator) => {
|
||||||
_tf::authenticator::validate_totp_code_str(user_uuid, twofactor_code, &selected_data?, ip, conn)?
|
_tf::authenticator::validate_totp_code_str(user_uuid, twofactor_code, &selected_data?, ip, conn).await?
|
||||||
}
|
}
|
||||||
Some(TwoFactorType::U2f) => _tf::u2f::validate_u2f_login(user_uuid, twofactor_code, conn)?,
|
Some(TwoFactorType::Webauthn) => {
|
||||||
Some(TwoFactorType::Webauthn) => _tf::webauthn::validate_webauthn_login(user_uuid, twofactor_code, conn)?,
|
_tf::webauthn::validate_webauthn_login(user_uuid, twofactor_code, conn).await?
|
||||||
Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?)?,
|
}
|
||||||
|
Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
|
||||||
Some(TwoFactorType::Duo) => {
|
Some(TwoFactorType::Duo) => {
|
||||||
_tf::duo::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?
|
_tf::duo::validate_duo_login(data.username.as_ref().unwrap().trim(), twofactor_code, conn).await?
|
||||||
}
|
}
|
||||||
Some(TwoFactorType::Email) => {
|
Some(TwoFactorType::Email) => {
|
||||||
_tf::email::validate_email_code_str(user_uuid, twofactor_code, &selected_data?, conn)?
|
_tf::email::validate_email_code_str(user_uuid, twofactor_code, &selected_data?, conn).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(TwoFactorType::Remember) => {
|
Some(TwoFactorType::Remember) => {
|
||||||
@@ -338,14 +443,22 @@ fn twofactor_auth(
|
|||||||
remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time
|
remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?, "2FA Remember token not provided")
|
err_json!(
|
||||||
|
_json_err_twofactor(&twofactor_ids, user_uuid, conn).await?,
|
||||||
|
"2FA Remember token not provided"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => err!("Invalid two factor provider"),
|
_ => err!(
|
||||||
|
"Invalid two factor provider",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn2fa
|
||||||
|
}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
TwoFactorIncomplete::mark_complete(user_uuid, &device.uuid, conn)?;
|
TwoFactorIncomplete::mark_complete(user_uuid, &device.uuid, conn).await?;
|
||||||
|
|
||||||
if !CONFIG.disable_2fa_remember() && remember == 1 {
|
if !CONFIG.disable_2fa_remember() && remember == 1 {
|
||||||
Ok(Some(device.refresh_twofactor_remember()))
|
Ok(Some(device.refresh_twofactor_remember()))
|
||||||
@@ -359,7 +472,7 @@ fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
|
|||||||
tf.map(|t| t.data).map_res("Two factor doesn't exist")
|
tf.map(|t| t.data).map_res("Two factor doesn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
|
async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbConn) -> ApiResult<Value> {
|
||||||
use crate::api::core::two_factor;
|
use crate::api::core::two_factor;
|
||||||
|
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
@@ -375,38 +488,18 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
|||||||
match TwoFactorType::from_i32(*provider) {
|
match TwoFactorType::from_i32(*provider) {
|
||||||
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
|
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
|
||||||
|
|
||||||
Some(TwoFactorType::U2f) if CONFIG.domain_set() => {
|
|
||||||
let request = two_factor::u2f::generate_u2f_login(user_uuid, conn)?;
|
|
||||||
let mut challenge_list = Vec::new();
|
|
||||||
|
|
||||||
for key in request.registered_keys {
|
|
||||||
challenge_list.push(json!({
|
|
||||||
"appId": request.app_id,
|
|
||||||
"challenge": request.challenge,
|
|
||||||
"version": key.version,
|
|
||||||
"keyHandle": key.key_handle,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let challenge_list_str = serde_json::to_string(&challenge_list).unwrap();
|
|
||||||
|
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
|
||||||
"Challenges": challenge_list_str,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => {
|
Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => {
|
||||||
let request = two_factor::webauthn::generate_webauthn_login(user_uuid, conn)?;
|
let request = two_factor::webauthn::generate_webauthn_login(user_uuid, conn).await?;
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = request.0;
|
result["TwoFactorProviders2"][provider.to_string()] = request.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(TwoFactorType::Duo) => {
|
Some(TwoFactorType::Duo) => {
|
||||||
let email = match User::find_by_uuid(user_uuid, conn) {
|
let email = match User::find_by_uuid(user_uuid, conn).await {
|
||||||
Some(u) => u.email,
|
Some(u) => u.email,
|
||||||
None => err!("User does not exist"),
|
None => err!("User does not exist"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (signature, host) = duo::generate_duo_signature(&email, conn)?;
|
let (signature, host) = duo::generate_duo_signature(&email, conn).await?;
|
||||||
|
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||||
"Host": host,
|
"Host": host,
|
||||||
@@ -415,7 +508,7 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
|||||||
}
|
}
|
||||||
|
|
||||||
Some(tf_type @ TwoFactorType::YubiKey) => {
|
Some(tf_type @ TwoFactorType::YubiKey) => {
|
||||||
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn) {
|
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn).await {
|
||||||
Some(tf) => tf,
|
Some(tf) => tf,
|
||||||
None => err!("No YubiKey devices registered"),
|
None => err!("No YubiKey devices registered"),
|
||||||
};
|
};
|
||||||
@@ -430,14 +523,14 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
|||||||
Some(tf_type @ TwoFactorType::Email) => {
|
Some(tf_type @ TwoFactorType::Email) => {
|
||||||
use crate::api::core::two_factor as _tf;
|
use crate::api::core::two_factor as _tf;
|
||||||
|
|
||||||
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn) {
|
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn).await {
|
||||||
Some(tf) => tf,
|
Some(tf) => tf,
|
||||||
None => err!("No twofactor email registered"),
|
None => err!("No twofactor email registered"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send email immediately if email is the only 2FA option
|
// Send email immediately if email is the only 2FA option
|
||||||
if providers.len() == 1 {
|
if providers.len() == 1 {
|
||||||
_tf::email::send_token(user_uuid, conn)?
|
_tf::email::send_token(user_uuid, conn).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
let email_data = EmailTokenData::from_json(&twofactor.data)?;
|
let email_data = EmailTokenData::from_json(&twofactor.data)?;
|
||||||
@@ -453,68 +546,70 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/accounts/prelogin", data = "<data>")]
|
||||||
|
async fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Value> {
|
||||||
|
_prelogin(data, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/accounts/register", data = "<data>")]
|
||||||
|
async fn identity_register(data: JsonUpcase<RegisterData>, conn: DbConn) -> JsonResult {
|
||||||
|
_register(data, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
// https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts
|
// https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts
|
||||||
// https://github.com/bitwarden/mobile/blob/master/src/Core/Models/Request/TokenRequest.cs
|
// https://github.com/bitwarden/mobile/blob/master/src/Core/Models/Request/TokenRequest.cs
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default, FromForm)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct ConnectData {
|
struct ConnectData {
|
||||||
// refresh_token, password, client_credentials (API key)
|
#[field(name = uncased("grant_type"))]
|
||||||
grant_type: String,
|
#[field(name = uncased("granttype"))]
|
||||||
|
grant_type: String, // refresh_token, password, client_credentials (API key)
|
||||||
|
|
||||||
// Needed for grant_type="refresh_token"
|
// Needed for grant_type="refresh_token"
|
||||||
|
#[field(name = uncased("refresh_token"))]
|
||||||
|
#[field(name = uncased("refreshtoken"))]
|
||||||
refresh_token: Option<String>,
|
refresh_token: Option<String>,
|
||||||
|
|
||||||
// Needed for grant_type = "password" | "client_credentials"
|
// Needed for grant_type = "password" | "client_credentials"
|
||||||
|
#[field(name = uncased("client_id"))]
|
||||||
|
#[field(name = uncased("clientid"))]
|
||||||
client_id: Option<String>, // web, cli, desktop, browser, mobile
|
client_id: Option<String>, // web, cli, desktop, browser, mobile
|
||||||
client_secret: Option<String>, // API key login (cli only)
|
#[field(name = uncased("client_secret"))]
|
||||||
|
#[field(name = uncased("clientsecret"))]
|
||||||
|
client_secret: Option<String>,
|
||||||
|
#[field(name = uncased("password"))]
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
|
#[field(name = uncased("scope"))]
|
||||||
scope: Option<String>,
|
scope: Option<String>,
|
||||||
|
#[field(name = uncased("username"))]
|
||||||
username: Option<String>,
|
username: Option<String>,
|
||||||
|
|
||||||
|
#[field(name = uncased("device_identifier"))]
|
||||||
|
#[field(name = uncased("deviceidentifier"))]
|
||||||
device_identifier: Option<String>,
|
device_identifier: Option<String>,
|
||||||
|
#[field(name = uncased("device_name"))]
|
||||||
|
#[field(name = uncased("devicename"))]
|
||||||
device_name: Option<String>,
|
device_name: Option<String>,
|
||||||
|
#[field(name = uncased("device_type"))]
|
||||||
|
#[field(name = uncased("devicetype"))]
|
||||||
device_type: Option<String>,
|
device_type: Option<String>,
|
||||||
device_push_token: Option<String>, // Unused; mobile device push not yet supported.
|
#[allow(unused)]
|
||||||
|
#[field(name = uncased("device_push_token"))]
|
||||||
|
#[field(name = uncased("devicepushtoken"))]
|
||||||
|
_device_push_token: Option<String>, // Unused; mobile device push not yet supported.
|
||||||
|
|
||||||
// Needed for two-factor auth
|
// Needed for two-factor auth
|
||||||
|
#[field(name = uncased("two_factor_provider"))]
|
||||||
|
#[field(name = uncased("twofactorprovider"))]
|
||||||
two_factor_provider: Option<i32>,
|
two_factor_provider: Option<i32>,
|
||||||
|
#[field(name = uncased("two_factor_token"))]
|
||||||
|
#[field(name = uncased("twofactortoken"))]
|
||||||
two_factor_token: Option<String>,
|
two_factor_token: Option<String>,
|
||||||
|
#[field(name = uncased("two_factor_remember"))]
|
||||||
|
#[field(name = uncased("twofactorremember"))]
|
||||||
two_factor_remember: Option<i32>,
|
two_factor_remember: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'f> FromForm<'f> for ConnectData {
|
|
||||||
type Error = String;
|
|
||||||
|
|
||||||
fn from_form(items: &mut FormItems<'f>, _strict: bool) -> Result<Self, Self::Error> {
|
|
||||||
let mut form = Self::default();
|
|
||||||
for item in items {
|
|
||||||
let (key, value) = item.key_value_decoded();
|
|
||||||
let mut normalized_key = key.to_lowercase();
|
|
||||||
normalized_key.retain(|c| c != '_'); // Remove '_'
|
|
||||||
|
|
||||||
match normalized_key.as_ref() {
|
|
||||||
"granttype" => form.grant_type = value,
|
|
||||||
"refreshtoken" => form.refresh_token = Some(value),
|
|
||||||
"clientid" => form.client_id = Some(value),
|
|
||||||
"clientsecret" => form.client_secret = Some(value),
|
|
||||||
"password" => form.password = Some(value),
|
|
||||||
"scope" => form.scope = Some(value),
|
|
||||||
"username" => form.username = Some(value),
|
|
||||||
"deviceidentifier" => form.device_identifier = Some(value),
|
|
||||||
"devicename" => form.device_name = Some(value),
|
|
||||||
"devicetype" => form.device_type = Some(value),
|
|
||||||
"devicepushtoken" => form.device_push_token = Some(value),
|
|
||||||
"twofactorprovider" => form.two_factor_provider = value.parse().ok(),
|
|
||||||
"twofactortoken" => form.two_factor_token = Some(value),
|
|
||||||
"twofactorremember" => form.two_factor_remember = value.parse().ok(),
|
|
||||||
key => warn!("Detected unexpected parameter during login: {}", key),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(form)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
|
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
|
||||||
if value.is_none() {
|
if value.is_none() {
|
||||||
err!(msg)
|
err!(msg)
|
||||||
|
|||||||