Compare commits
1508 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adfc96074f | ||
|
|
7cc7b874d1 | ||
|
|
4d12a5061d | ||
|
|
b274add4da | ||
|
|
fd51c9dc4c | ||
|
|
51f8794aa6 | ||
|
|
3db998f9c4 | ||
|
|
0a2a7087a7 | ||
|
|
8f0eb11ced | ||
|
|
e3e3599095 | ||
|
|
aa74e31453 | ||
|
|
e1f6e729fd | ||
|
|
13d83a6d1c | ||
|
|
6075387654 | ||
|
|
69fad3f55f | ||
|
|
e3864b62a4 | ||
|
|
22176f4e0f | ||
|
|
a89d7ec0ea | ||
|
|
8262049e20 | ||
|
|
c61e1e0a2a | ||
|
|
b376cf6c65 | ||
|
|
16bae25ce6 | ||
|
|
57ba17a12e | ||
|
|
f4d98a4910 | ||
|
|
fa32d78ff1 | ||
|
|
56f22a4479 | ||
|
|
7b88d3a1bc | ||
|
|
c8a39f9544 | ||
|
|
e77d1be53e | ||
|
|
6765bd0624 | ||
|
|
5f7b563a01 | ||
|
|
3885875149 | ||
|
|
cf05d5026f | ||
|
|
3e83a30739 | ||
|
|
49c5f5a8f0 | ||
|
|
72939e0cd7 | ||
|
|
271560894a | ||
|
|
6a591c1bcd | ||
|
|
3fdcfef1b4 | ||
|
|
7b8cfa2062 | ||
|
|
cc5921fd74 | ||
|
|
d027b7f759 | ||
|
|
f1524b0120 | ||
|
|
9985892751 | ||
|
|
cfd60bdd91 | ||
|
|
779f2a86e5 | ||
|
|
f47c4445bd | ||
|
|
1aeb4cc3d5 | ||
|
|
9e0a0205cc | ||
|
|
1058efb17a | ||
|
|
d0f744ebef | ||
|
|
a8c043cb16 | ||
|
|
978e02b5dc | ||
|
|
b39dbfff96 | ||
|
|
bd89cfde79 | ||
|
|
0bd563b2e5 | ||
|
|
22fe915629 | ||
|
|
aa161a5365 | ||
|
|
0557514cb4 | ||
|
|
298203253c | ||
|
|
cbeef2b248 | ||
|
|
e68a74ba48 | ||
|
|
02a0db1408 | ||
|
|
348376c672 | ||
|
|
037b02e268 | ||
|
|
fe534ab4e6 | ||
|
|
6625d54d67 | ||
|
|
ee6d1ed586 | ||
|
|
6de1d88e11 | ||
|
|
de19b6f17b | ||
|
|
226a90be1d | ||
|
|
6cfb6ff06a | ||
|
|
649c3d74b8 | ||
|
|
662ce3b2f5 | ||
|
|
0292bc154d | ||
|
|
ceee83f03a | ||
|
|
144904f0f6 | ||
|
|
963c8f1221 | ||
|
|
6c50c38f83 | ||
|
|
3aac62cc81 | ||
|
|
78a05d39c4 | ||
|
|
79bec3880e | ||
|
|
bdf7bd6309 | ||
|
|
4ce1ba999b | ||
|
|
d01501703b | ||
|
|
6a38a09462 | ||
|
|
78990e354f | ||
|
|
3189ebdfef | ||
|
|
39bf627b0a | ||
|
|
a838c763ea | ||
|
|
0afea63994 | ||
|
|
0df9487527 | ||
|
|
9274ee72ad | ||
|
|
2b6c3debb4 | ||
|
|
8dd6dd4e7f | ||
|
|
5e64c96497 | ||
|
|
54c0b4b8a2 | ||
|
|
31056e12ba | ||
|
|
151c8117a3 | ||
|
|
9b5c17c2db | ||
|
|
23e01b257e | ||
|
|
ecc8c7a86e | ||
|
|
ee4d7b9b69 | ||
|
|
80c03839a4 | ||
|
|
f394cb69ce | ||
|
|
52137ba9e5 | ||
|
|
942b8101cc | ||
|
|
a0a6b33ecd | ||
|
|
d4c5e1b51c | ||
|
|
96923aed75 | ||
|
|
a04f833e3f | ||
|
|
3b52cc9bd4 | ||
|
|
df2e92e451 | ||
|
|
8836fe043b | ||
|
|
7ce6a58099 | ||
|
|
57d6aca716 | ||
|
|
8d99637455 | ||
|
|
abd66780f4 | ||
|
|
a5175a35ec | ||
|
|
d9f945b5df | ||
|
|
0c55e39e8c | ||
|
|
b5443952da | ||
|
|
b9f0ccfaba | ||
|
|
24742325b7 | ||
|
|
c87ebe447f | ||
|
|
462cf16db9 | ||
|
|
edaa4e8754 | ||
|
|
76c596c574 | ||
|
|
b5554f6dcf | ||
|
|
fc65f1afd1 | ||
|
|
b066b6a920 | ||
|
|
bc0e63aac8 | ||
|
|
343ff575e6 | ||
|
|
08c922dca6 | ||
|
|
4dd6519cc6 | ||
|
|
cdffdae289 | ||
|
|
27e3b82223 | ||
|
|
239b31748a | ||
|
|
b465b74326 | ||
|
|
616f262d09 | ||
|
|
8aa0ec17c5 | ||
|
|
a8c5b53a2c | ||
|
|
e96dbd444e | ||
|
|
939e2acb0b | ||
|
|
a04955dc70 | ||
|
|
ba04a22492 | ||
|
|
a281fe129f | ||
|
|
a655cc8d3b | ||
|
|
63b584c83d | ||
|
|
7dffd5f079 | ||
|
|
044e5702df | ||
|
|
3c3b9546d9 | ||
|
|
5bc0e74b53 | ||
|
|
f0d4dddacd | ||
|
|
f4a3f46bcf | ||
|
|
38472e4cd2 | ||
|
|
9db5d1e4f4 | ||
|
|
4cadaf7d49 | ||
|
|
748486160f | ||
|
|
2c0a0b2bc4 | ||
|
|
7e51d4bebb | ||
|
|
c011e67122 | ||
|
|
394b4c403d | ||
|
|
74030aa067 | ||
|
|
07b8c745e6 | ||
|
|
6767bfa2d2 | ||
|
|
607d94fef4 | ||
|
|
83b060ef94 | ||
|
|
cb14cb94ce | ||
|
|
2b9de49fbe | ||
|
|
290f273bdf | ||
|
|
7b43779fb0 | ||
|
|
924c38faa6 | ||
|
|
e4d5f9610e | ||
|
|
04e9cb0ac8 | ||
|
|
da53daff37 | ||
|
|
8c26eff2c1 | ||
|
|
044c265423 | ||
|
|
0053658d5d | ||
|
|
99cf3b378f | ||
|
|
78293eab62 | ||
|
|
1a84be5782 | ||
|
|
178f82b675 | ||
|
|
ec5fbbcd1e | ||
|
|
78164054d4 | ||
|
|
6d5d11d5b4 | ||
|
|
9e3b93d385 | ||
|
|
d77cf93193 | ||
|
|
2cca3f3722 | ||
|
|
36d96a1791 | ||
|
|
82e34a5df2 | ||
|
|
7b83f4b1dc | ||
|
|
7d9910d1ca | ||
|
|
7a63f6da56 | ||
|
|
0043833f36 | ||
|
|
3ad3bccadb | ||
|
|
39e94c890e | ||
|
|
701039454a | ||
|
|
ec77a03d7c | ||
|
|
8dbad84a58 | ||
|
|
1767a37162 | ||
|
|
622c3a067a | ||
|
|
4389548b64 | ||
|
|
8cb0f1e558 | ||
|
|
faafb77c73 | ||
|
|
0ecd1c73c1 | ||
|
|
88bf40f9a6 | ||
|
|
a025163b34 | ||
|
|
07c80462b7 | ||
|
|
79ac2277d4 | ||
|
|
1c27bee9d0 | ||
|
|
56dc58b0b8 | ||
|
|
4018addd79 | ||
|
|
429dfb4314 | ||
|
|
1d7bb0bb2b | ||
|
|
476eb673bb | ||
|
|
71681b710c | ||
|
|
fb02a7da06 | ||
|
|
5fdc341138 | ||
|
|
15de6caf75 | ||
|
|
77bc2d5006 | ||
|
|
972f5fca9d | ||
|
|
083314ee2d | ||
|
|
078ce0e546 | ||
|
|
9beca2c226 | ||
|
|
451f23ae24 | ||
|
|
300ebfa19f | ||
|
|
1ce2846c95 | ||
|
|
7fb8c11a9d | ||
|
|
e7993c2d1b | ||
|
|
3e93f7ff88 | ||
|
|
f0580956db | ||
|
|
65b0bab26d | ||
|
|
a559421293 | ||
|
|
6ef35c26a4 | ||
|
|
8a918324aa | ||
|
|
d30c0c8cbb | ||
|
|
1697c826c0 | ||
|
|
b378b8c8ef | ||
|
|
2c42d7ff81 | ||
|
|
e1f36ee54a | ||
|
|
698f72f828 | ||
|
|
8dd94f5336 | ||
|
|
1dc21b9a21 | ||
|
|
248215cc77 | ||
|
|
a6eee73c11 | ||
|
|
50d6a39312 | ||
|
|
467b6b9bcb | ||
|
|
6e6246797c | ||
|
|
f8e4f747f5 | ||
|
|
9acd49fcc6 | ||
|
|
636d12d43f | ||
|
|
5e9383de95 | ||
|
|
4863af863e | ||
|
|
b6d4c62edd | ||
|
|
bf733f3822 | ||
|
|
bbf4027418 | ||
|
|
dbffc5fc22 | ||
|
|
f4a9420002 | ||
|
|
657854bd29 | ||
|
|
cdc9e7d921 | ||
|
|
b125121ac8 | ||
|
|
0c480dd5ec | ||
|
|
c07b8dcf73 | ||
|
|
912a4b216f | ||
|
|
e7fb205c31 | ||
|
|
a2ba20e12f | ||
|
|
f515dd82fe | ||
|
|
61d3193c41 | ||
|
|
430ae66955 | ||
|
|
3abbbc82b2 | ||
|
|
854181f63e | ||
|
|
df996794ed | ||
|
|
2d94018e3c | ||
|
|
72bb9d0ca1 | ||
|
|
93bd0d65e2 | ||
|
|
495b0f0068 | ||
|
|
3275b6a6d8 | ||
|
|
fed5aa1599 | ||
|
|
6bc4efbac1 | ||
|
|
cc0164a67b | ||
|
|
b968cc25ad | ||
|
|
d116a35a6d | ||
|
|
49f856bdd5 | ||
|
|
0d628f589a | ||
|
|
4387b2149f | ||
|
|
8cc602434e | ||
|
|
6411dc9504 | ||
|
|
32c34b0a11 | ||
|
|
6e8f5e0fc2 | ||
|
|
3ce377dbd1 | ||
|
|
ad502b9f18 | ||
|
|
fde186a5a3 | ||
|
|
0823f623c8 | ||
|
|
6cec113304 | ||
|
|
cd42d77a46 | ||
|
|
35907beaca | ||
|
|
a062a59288 | ||
|
|
842c2decd0 | ||
|
|
d1069ed359 | ||
|
|
6d81a1b1f8 | ||
|
|
b2fe478dae | ||
|
|
02ed6a6e8b | ||
|
|
1b271ab467 | ||
|
|
bacb5f8901 | ||
|
|
ae7371da95 | ||
|
|
60f5eb603b | ||
|
|
420ed00f55 | ||
|
|
b975871e9d | ||
|
|
803ffe2960 | ||
|
|
c96c95924c | ||
|
|
f02786001c | ||
|
|
542b7192c3 | ||
|
|
b31aa10b52 | ||
|
|
1eba59954d | ||
|
|
b71d9f05c5 | ||
|
|
fc5e94d55f | ||
|
|
3875fb3eaa | ||
|
|
b3d4132fe6 | ||
|
|
21f20bb9ea | ||
|
|
df937467a0 | ||
|
|
d1ae271111 | ||
|
|
e7fb3e0e45 | ||
|
|
b7b0271ec7 | ||
|
|
9935b839b7 | ||
|
|
0e11098c31 | ||
|
|
c55e0a069b | ||
|
|
c3da876b04 | ||
|
|
643a9c6c7c | ||
|
|
0c3a94172d | ||
|
|
527eb16700 | ||
|
|
e3aec3f094 | ||
|
|
eac2734df4 | ||
|
|
a411e7c977 | ||
|
|
28c4abe2d0 | ||
|
|
a9ef6ebf5f | ||
|
|
c12415c12d | ||
|
|
1f481e690b | ||
|
|
58aad859e1 | ||
|
|
b9ebfe09ee | ||
|
|
864cf7af99 | ||
|
|
b76f460979 | ||
|
|
cbd2c4682d | ||
|
|
d49bdf7d49 | ||
|
|
559a7278a0 | ||
|
|
4a172fae97 | ||
|
|
fc4263e2f9 | ||
|
|
c1b9b4c81b | ||
|
|
aa9b73522e | ||
|
|
0904f83627 | ||
|
|
253053cc23 | ||
|
|
08a3ff65c7 | ||
|
|
ee8fac8be8 | ||
|
|
9fa49b40b3 | ||
|
|
de13119e02 | ||
|
|
bf9acd7691 | ||
|
|
a5066fecc8 | ||
|
|
6432681440 | ||
|
|
b3b6df9d82 | ||
|
|
fe7be4ef62 | ||
|
|
b4603547f6 | ||
|
|
b33b9315ea | ||
|
|
6ae03fa028 | ||
|
|
e1bb1e0472 | ||
|
|
411670e4f5 | ||
|
|
1c55932f84 | ||
|
|
5a77054d6b | ||
|
|
84c5fd58f9 | ||
|
|
98979911ee | ||
|
|
c0cf7358c7 | ||
|
|
9053e64dff | ||
|
|
57bfe97d08 | ||
|
|
028570279c | ||
|
|
bda1cd1f25 | ||
|
|
7a9b775b09 | ||
|
|
17e791afb9 | ||
|
|
920fc7d937 | ||
|
|
6e314a2fa5 | ||
|
|
dc90db6591 | ||
|
|
beed4895c1 | ||
|
|
629dd669c4 | ||
|
|
fc9319e55b | ||
|
|
58b64a5739 | ||
|
|
d93537261e | ||
|
|
22ec87d00e | ||
|
|
b87b4156e7 | ||
|
|
93f010b880 | ||
|
|
c5d4cdf1bc | ||
|
|
c117601e53 | ||
|
|
f78f838ed9 | ||
|
|
1583b69fb7 | ||
|
|
cde6d1b0e0 | ||
|
|
be60569a14 | ||
|
|
51226a74d0 | ||
|
|
e983473a54 | ||
|
|
211ab3fd9d | ||
|
|
90c8ea7f09 | ||
|
|
fb5193d896 | ||
|
|
f7a7f01d7d | ||
|
|
2c84a52937 | ||
|
|
6020590b2f | ||
|
|
1477def4fe | ||
|
|
8882f1da0e | ||
|
|
056d487f1c | ||
|
|
b8083215b3 | ||
|
|
61c864e748 | ||
|
|
0e0f5030da | ||
|
|
0dacc4d49e | ||
|
|
4d783c5e42 | ||
|
|
75b3a6bea4 | ||
|
|
29507cda7e | ||
|
|
81e0c82fee | ||
|
|
7519fad6dd | ||
|
|
248a59ee8c | ||
|
|
cefb6d3c95 | ||
|
|
543076eaac | ||
|
|
cbf1ddeb4c | ||
|
|
3746adcc13 | ||
|
|
854b984850 | ||
|
|
62fa0e2043 | ||
|
|
4f5b1b0aa7 | ||
|
|
0e362c2106 | ||
|
|
6ac5be32b2 | ||
|
|
6966183bc8 | ||
|
|
60d70e3668 | ||
|
|
5e65f2aced | ||
|
|
1e345364cf | ||
|
|
502a6c462f | ||
|
|
ceafdb9cb4 | ||
|
|
a6d8f6beaa | ||
|
|
fa1f84bd0a | ||
|
|
72a1e5eefb | ||
|
|
f20fa0b1c8 | ||
|
|
07b7af59b6 | ||
|
|
c9ac525358 | ||
|
|
8b1b2b1e2d | ||
|
|
ad0591ec17 | ||
|
|
59c47d98dd | ||
|
|
cb38a545ce | ||
|
|
dd356b6ea9 | ||
|
|
c1d39a910f | ||
|
|
eb3913ba48 | ||
|
|
f7efbc66f7 | ||
|
|
f368d9936a | ||
|
|
5cb4e6f651 | ||
|
|
f7f7b087c4 | ||
|
|
4cceee8936 | ||
|
|
5262c02a28 | ||
|
|
fd81529ddc | ||
|
|
383341ad61 | ||
|
|
66d0182825 | ||
|
|
1fc9a40273 | ||
|
|
1953a98968 | ||
|
|
3b5979d783 | ||
|
|
9aa1f43df7 | ||
|
|
bf89f09238 | ||
|
|
b8e14ee269 | ||
|
|
c700ee491e | ||
|
|
65575751ff | ||
|
|
cc28f88325 | ||
|
|
dd913decc6 | ||
|
|
372852ee86 | ||
|
|
154974c24e | ||
|
|
844041fc32 | ||
|
|
c23aff45c8 | ||
|
|
1b4eacb587 | ||
|
|
f21434a971 | ||
|
|
6c0e7baa87 | ||
|
|
09fe3af6b7 | ||
|
|
50a6c61d0d | ||
|
|
53b5fba532 | ||
|
|
a73b710313 | ||
|
|
404b1ef92e | ||
|
|
c3b7af9c89 | ||
|
|
13d04b15de | ||
|
|
88fdd3a456 | ||
|
|
de4ff1c1f0 | ||
|
|
a6637f18c8 | ||
|
|
d4031ee7b5 | ||
|
|
7bf2b9601f | ||
|
|
6fbb4b568b | ||
|
|
7a14f0c012 | ||
|
|
e8f27228a2 | ||
|
|
8346fe4bd6 | ||
|
|
d1b8d7240e | ||
|
|
274b16a1a6 | ||
|
|
b0d8c332e5 | ||
|
|
ba1888b6c4 | ||
|
|
c0e47bafa4 | ||
|
|
894f6ce131 | ||
|
|
f0b39a6b6b | ||
|
|
24fdf3487b | ||
|
|
900b8d9f06 | ||
|
|
58b45b8ebc | ||
|
|
606a69b92c | ||
|
|
2c8b96c511 | ||
|
|
38e6af1d09 | ||
|
|
32a289d563 | ||
|
|
8d4d6eace0 | ||
|
|
f337a8dce7 | ||
|
|
fba06cf651 | ||
|
|
fecf484eda | ||
|
|
34907856c5 | ||
|
|
4a2915a62b | ||
|
|
8d51fe60f4 | ||
|
|
e96632a6b2 | ||
|
|
5ba7abaa79 | ||
|
|
451238d48b | ||
|
|
8df91922ad | ||
|
|
34d62837fd | ||
|
|
ed281bbe4a | ||
|
|
b218cbf503 | ||
|
|
c141b6d65e | ||
|
|
aacec617a8 | ||
|
|
ed19f3e1ad | ||
|
|
1374cf047e | ||
|
|
3a0e4c4f8c | ||
|
|
b34b05f059 | ||
|
|
579e8e05b7 | ||
|
|
a0463f405d | ||
|
|
ec344654db | ||
|
|
4aab5bc68f | ||
|
|
4c37afb446 | ||
|
|
86115afc6b | ||
|
|
87399d1064 | ||
|
|
a01b855d2f | ||
|
|
be39cb5af3 | ||
|
|
6930fd198c | ||
|
|
62e822df4e | ||
|
|
6466687a27 | ||
|
|
ccaaa21f16 | ||
|
|
0595ec166d | ||
|
|
72d1698242 | ||
|
|
9321e00d0a | ||
|
|
b03a49e4e1 | ||
|
|
aa9f6f02ca | ||
|
|
6a88d26054 | ||
|
|
37db97dba9 | ||
|
|
0ccce2b2bc | ||
|
|
58813eb4e0 | ||
|
|
5992edb3ba | ||
|
|
808e0ce3d5 | ||
|
|
534f3ec627 | ||
|
|
9d37c9f743 | ||
|
|
1bad4c3da6 | ||
|
|
9edb579156 | ||
|
|
d1aee5d431 | ||
|
|
dc8d4b4ca1 | ||
|
|
35ab508109 | ||
|
|
e01030820c | ||
|
|
fab041b364 | ||
|
|
e5da67d1bc | ||
|
|
bbc61930f9 | ||
|
|
40c789e7cb | ||
|
|
6084212bf6 | ||
|
|
944b56751d | ||
|
|
c9e53542e0 | ||
|
|
7001067baa | ||
|
|
aece2251cf | ||
|
|
756f0048f0 | ||
|
|
1cb2fca7a5 | ||
|
|
287af260b7 | ||
|
|
9fd76362dc | ||
|
|
733517fa76 | ||
|
|
d8b3d0715f | ||
|
|
1d951b28aa | ||
|
|
65ab6870e6 | ||
|
|
255e3d14d0 | ||
|
|
25e486ef18 | ||
|
|
92123bd243 | ||
|
|
6d5026d17e | ||
|
|
1e6ca131d9 | ||
|
|
21c6a441aa | ||
|
|
e23c110057 | ||
|
|
de16b73ce8 | ||
|
|
f269d67ec6 | ||
|
|
7f3490974b | ||
|
|
a42eef376d | ||
|
|
bbf115dc71 | ||
|
|
77bcdcfb08 | ||
|
|
d713dc4933 | ||
|
|
0c9a6bf76b | ||
|
|
4c4094420b | ||
|
|
96e55ca79b | ||
|
|
c61c5024d0 | ||
|
|
f61d0a68d5 | ||
|
|
f917f7d167 | ||
|
|
35541ef323 | ||
|
|
1f60d4a808 | ||
|
|
adc199b315 | ||
|
|
f557c4c550 | ||
|
|
0c4a73ff10 | ||
|
|
203e019e82 | ||
|
|
4d2a39bad2 | ||
|
|
db4fae3603 | ||
|
|
871789a236 | ||
|
|
ba6fb53594 | ||
|
|
5d74b21bc4 | ||
|
|
006490c753 | ||
|
|
1614af0128 | ||
|
|
27fd91ed37 | ||
|
|
7df0560104 | ||
|
|
2368199e03 | ||
|
|
bee98e1ba0 | ||
|
|
8abbbb4625 | ||
|
|
ef182fe75e | ||
|
|
613e93fdc6 | ||
|
|
a5e3c89a54 | ||
|
|
7f55b71495 | ||
|
|
d95d59e454 | ||
|
|
08ea069ed4 | ||
|
|
e7a41b4cd9 | ||
|
|
262a601d21 | ||
|
|
00af6b5179 | ||
|
|
5800d01406 | ||
|
|
c803451920 | ||
|
|
95bdc70d1d | ||
|
|
5d10197334 | ||
|
|
e7da6cd651 | ||
|
|
0f35369292 | ||
|
|
579845dfb1 | ||
|
|
6ad5c16d3a | ||
|
|
24176b5c7d | ||
|
|
0b3b5979ba | ||
|
|
dbec9fbb4a | ||
|
|
7cd0bc7cac | ||
|
|
c12a931dbf | ||
|
|
f8af10dd26 | ||
|
|
4c47b9c3e6 | ||
|
|
44f2fc67a3 | ||
|
|
ec1ba16ef7 | ||
|
|
7a5724591a | ||
|
|
9f433bc359 | ||
|
|
1d45a174ac | ||
|
|
80cee32031 | ||
|
|
437cfd0b8e | ||
|
|
dc4dae6ddb | ||
|
|
2f578010a0 | ||
|
|
e7ec3fe61f | ||
|
|
7b389fc323 | ||
|
|
86361b630e | ||
|
|
c57df87bc3 | ||
|
|
cbbf3c5a53 | ||
|
|
8a9fbb461c | ||
|
|
55b25cb003 | ||
|
|
c929a71649 | ||
|
|
8af1bcd35c | ||
|
|
ae2587dcad | ||
|
|
cac8445aa1 | ||
|
|
7eb98035e5 | ||
|
|
7c75c87a0c | ||
|
|
404efd2523 | ||
|
|
fc7d60e7ec | ||
|
|
fbf3afd6fb | ||
|
|
3f8f277841 | ||
|
|
2ff44df636 | ||
|
|
83a612981e | ||
|
|
0ae1ace8fe | ||
|
|
a2745c687e | ||
|
|
e5e053a2ad | ||
|
|
a5f89bb6d7 | ||
|
|
a53b569d0a | ||
|
|
bcbebda39b | ||
|
|
a296850d58 | ||
|
|
b609a4ee74 | ||
|
|
5409a5eaa0 | ||
|
|
6959bc5b02 | ||
|
|
134700b432 | ||
|
|
483d25c3f3 | ||
|
|
dab4eb7664 | ||
|
|
139e90830f | ||
|
|
2e8ad9281d | ||
|
|
6b6cfd10f1 | ||
|
|
5f07d45846 | ||
|
|
2dd92fd940 | ||
|
|
40f64709a6 | ||
|
|
c31b311b4e | ||
|
|
c316532fe9 | ||
|
|
a65d6ba8f1 | ||
|
|
11f5d6aa0d | ||
|
|
9532aa9500 | ||
|
|
ec3deed38e | ||
|
|
38015b4913 | ||
|
|
e5508b5c5d | ||
|
|
9587e4105f | ||
|
|
d2d735c5c0 | ||
|
|
915c10b4b8 | ||
|
|
671530f5b4 | ||
|
|
0c778f57d3 | ||
|
|
43db7729c4 | ||
|
|
48b467a683 | ||
|
|
59b7406dd7 | ||
|
|
9e7a40abc8 | ||
|
|
189331f465 | ||
|
|
e6a2364209 | ||
|
|
2b17aa598f | ||
|
|
f10fdf4610 | ||
|
|
4156fe0666 | ||
|
|
89bef6027c | ||
|
|
333ca0a827 | ||
|
|
de82a056e6 | ||
|
|
a01b1ffe8c | ||
|
|
61718a5915 | ||
|
|
2fed3572b2 | ||
|
|
af9e4fc150 | ||
|
|
f8475af5a6 | ||
|
|
413870e995 | ||
|
|
cdd6f272ed | ||
|
|
5eddd0cd8d | ||
|
|
5cf2b736e1 | ||
|
|
55330960e9 | ||
|
|
a3b88567cc | ||
|
|
73a687376a | ||
|
|
de4c08c2ff | ||
|
|
9396df2e20 | ||
|
|
4143f50004 | ||
|
|
d1511c5eb0 | ||
|
|
c4c6d48abf | ||
|
|
43c5f9094a | ||
|
|
51ab9c59ae | ||
|
|
fc95ab8658 | ||
|
|
bebe860903 | ||
|
|
df4679ea55 | ||
|
|
2263eada70 | ||
|
|
368c9ee3d7 | ||
|
|
3513a01711 | ||
|
|
0af36a5757 | ||
|
|
fdd5a94074 | ||
|
|
17ad2cfd14 | ||
|
|
67f509e2bb | ||
|
|
6102094c9e | ||
|
|
d84062b1b2 | ||
|
|
a878440485 | ||
|
|
cd6e61e93b | ||
|
|
a77b56b522 | ||
|
|
6ed5084691 | ||
|
|
4ac6ecb558 | ||
|
|
41671b4f25 | ||
|
|
024ab1212b | ||
|
|
77f62e11ef | ||
|
|
0960835cd9 | ||
|
|
3746dd47f8 | ||
|
|
ce255c5181 | ||
|
|
7728cc734a | ||
|
|
cc581c6a9e | ||
|
|
f7b142e74a | ||
|
|
1108cee626 | ||
|
|
94fdba5990 | ||
|
|
524258a9ea | ||
|
|
c0cf7a6d6a | ||
|
|
f9916d1cd6 | ||
|
|
52512c0ccc | ||
|
|
989f041658 | ||
|
|
2f81b750a3 | ||
|
|
035a5b88c2 | ||
|
|
7702149962 | ||
|
|
f3e24d62ca | ||
|
|
d663b9f346 | ||
|
|
ae147358b1 | ||
|
|
20bc53119e | ||
|
|
3a3a4b2fea | ||
|
|
497437729b | ||
|
|
1c37fcf398 | ||
|
|
67af66fc55 | ||
|
|
288c843a17 | ||
|
|
9357c2db0e | ||
|
|
139771f4d4 | ||
|
|
b7783aaa1c | ||
|
|
9821beb1de | ||
|
|
d279f722f8 | ||
|
|
9539a8e18a | ||
|
|
f11b9bff17 | ||
|
|
06c282dd9a | ||
|
|
3e9fb853d9 | ||
|
|
cd21ad2085 | ||
|
|
729100ae16 | ||
|
|
bcd29a4232 | ||
|
|
380a9d7faa | ||
|
|
9cd0033504 | ||
|
|
483fe77a35 | ||
|
|
1742303ad7 | ||
|
|
292fb3920f | ||
|
|
9ed8f11b22 | ||
|
|
860d8c6b78 | ||
|
|
99965805a6 | ||
|
|
7036d1328e | ||
|
|
697910c7b2 | ||
|
|
64dc605843 | ||
|
|
75fa88e6e2 | ||
|
|
2857b8c586 | ||
|
|
b1788c29db | ||
|
|
2e1401f013 | ||
|
|
1e1c11b13c | ||
|
|
25c1c854b1 | ||
|
|
5be2cc1965 | ||
|
|
4aa3f40792 | ||
|
|
3f41a82fd3 | ||
|
|
1b641b4222 | ||
|
|
85aabebbb4 | ||
|
|
919232261d | ||
|
|
ddd25a20eb | ||
|
|
84b8f9d6fa | ||
|
|
46af0ff74c | ||
|
|
b3651ed0a3 | ||
|
|
78c4fa393a | ||
|
|
84c4159062 | ||
|
|
ac2888fc4e | ||
|
|
c311847dcf | ||
|
|
cb6cda7265 | ||
|
|
16fd5470db | ||
|
|
ea0cac2a92 | ||
|
|
33b041ef34 | ||
|
|
b692ea693f | ||
|
|
fd39e50c08 | ||
|
|
ad4b9c050a | ||
|
|
1deb6371ed | ||
|
|
3b11556f4b | ||
|
|
25f719b0e2 | ||
|
|
231b63f1b0 | ||
|
|
e73370cc8c | ||
|
|
5d25dd4c06 | ||
|
|
51a8bacc18 | ||
|
|
db07f546a4 | ||
|
|
255cf0bc85 | ||
|
|
733e0b18e2 | ||
|
|
8d90e03992 | ||
|
|
1a1fae9ce3 | ||
|
|
f26786c904 | ||
|
|
e0b6bf5aa6 | ||
|
|
9655fc4490 | ||
|
|
417ea4d481 | ||
|
|
5a59f8e3f4 | ||
|
|
48340d0010 | ||
|
|
251de9fe8a | ||
|
|
c501df927b | ||
|
|
cdb1659506 | ||
|
|
558afe36ad | ||
|
|
712d3870eb | ||
|
|
c71f084531 | ||
|
|
1c58a543b6 | ||
|
|
8e857dc563 | ||
|
|
78d4d4c89e | ||
|
|
aea749d82f | ||
|
|
ce3293b4e2 | ||
|
|
0c12fbdd23 | ||
|
|
118cf97e1d | ||
|
|
abb668633b | ||
|
|
dd2fffd3dc | ||
|
|
64b13e9dc9 | ||
|
|
58d7f1e8ae | ||
|
|
45e4a94416 | ||
|
|
cce054bbe8 | ||
|
|
cf0e326b82 | ||
|
|
e48958f5a0 | ||
|
|
63e2793272 | ||
|
|
532e64b802 | ||
|
|
64b3e965c6 | ||
|
|
9371c027f3 | ||
|
|
a8bc58a420 | ||
|
|
10c56a91da | ||
|
|
403972de39 | ||
|
|
1c0632473a | ||
|
|
ff93109b57 | ||
|
|
b518810106 | ||
|
|
b4d2d65c5c | ||
|
|
beeb188d7e | ||
|
|
2830022ede | ||
|
|
ba4103e03f | ||
|
|
f3d6638384 | ||
|
|
2ad42d660b | ||
|
|
618a00d775 | ||
|
|
d0b65ce297 | ||
|
|
41f640077b | ||
|
|
bfa05616b1 | ||
|
|
f792d7a476 | ||
|
|
9b13cfdbe9 | ||
|
|
02c274e117 | ||
|
|
8993b40730 | ||
|
|
1311b171f9 | ||
|
|
7ecc1022b2 | ||
|
|
a5c14790b3 | ||
|
|
e5f7a03585 | ||
|
|
a024a13f25 | ||
|
|
4d876d0ce8 | ||
|
|
b02c37bf33 | ||
|
|
1d17f11d2f | ||
|
|
1dd9f7f363 | ||
|
|
618d95b76e | ||
|
|
e416abe19b | ||
|
|
296f58f43d | ||
|
|
0b5e3d5a10 | ||
|
|
5d591b18d9 | ||
|
|
e68bc08fed | ||
|
|
df38c84075 | ||
|
|
c509e5db70 | ||
|
|
5a8e029005 | ||
|
|
e3d96b5bb3 | ||
|
|
51afc337ff | ||
|
|
617d41584e | ||
|
|
7a8e2caa50 | ||
|
|
fed58ec29a | ||
|
|
427a7516a3 | ||
|
|
969feb8efa | ||
|
|
d09d6e1e99 | ||
|
|
2d80638090 | ||
|
|
2918d39ab7 | ||
|
|
fb2eb0ebf7 | ||
|
|
e7a36a1ff1 | ||
|
|
41e1b4a5d5 | ||
|
|
6e205fa8ae | ||
|
|
5b3f6ad76e | ||
|
|
8396c3023e | ||
|
|
161d56db36 | ||
|
|
a49a4e5513 | ||
|
|
9741462e7b | ||
|
|
73f09e1af6 | ||
|
|
d41f6e57d2 | ||
|
|
93d041e55b | ||
|
|
94e419e09c | ||
|
|
6c5f6934e7 | ||
|
|
41155b3f97 | ||
|
|
e41c80449d | ||
|
|
6f7a46e528 | ||
|
|
ba48e0c5b8 | ||
|
|
3da636170f | ||
|
|
7687a9e588 | ||
|
|
692dc1a29e | ||
|
|
bb55f9f331 | ||
|
|
dec7b138e9 | ||
|
|
d3b1be80eb | ||
|
|
0339925d15 | ||
|
|
d956ec65a2 | ||
|
|
b02e649405 | ||
|
|
68e98be376 | ||
|
|
c9d174df09 | ||
|
|
80391b867c | ||
|
|
45715293ea | ||
|
|
dd4963e3aa | ||
|
|
c7c7fe194c | ||
|
|
35fdaf1ddd | ||
|
|
2aa5081889 | ||
|
|
9c5d4aaf11 | ||
|
|
04adf25e65 | ||
|
|
8f77261872 | ||
|
|
8b7505c466 | ||
|
|
b420ef3c1f | ||
|
|
e235863b94 | ||
|
|
6b7948b6cd | ||
|
|
87c373b08c | ||
|
|
9844269c1a | ||
|
|
6866b84da8 | ||
|
|
5b19fb3d96 | ||
|
|
fce2a148b8 | ||
|
|
69e1d653ce | ||
|
|
9b88dd6348 | ||
|
|
85c0e5eca2 | ||
|
|
17684f37d9 | ||
|
|
175b87c355 | ||
|
|
5fff9ebfd4 | ||
|
|
795497a60d | ||
|
|
48119acc72 | ||
|
|
ddb23ea1c8 | ||
|
|
6ec7ec3c25 | ||
|
|
39d3690ac0 | ||
|
|
41e0fce068 | ||
|
|
d876bebf28 | ||
|
|
7cb04ce62b | ||
|
|
326d709bf9 | ||
|
|
40dcc9eb33 | ||
|
|
b8f024aa39 | ||
|
|
fbed90224f | ||
|
|
64fe3a1dae | ||
|
|
a160b92529 | ||
|
|
dc3e7f5888 | ||
|
|
30d23d8555 | ||
|
|
42deb992e6 | ||
|
|
6e31a42886 | ||
|
|
f6cab5a65b | ||
|
|
448a80af4a | ||
|
|
6e4b8884e6 | ||
|
|
076e44e39a | ||
|
|
1d23bf3d04 | ||
|
|
85b7f8c5d7 | ||
|
|
e4bf6ffd18 | ||
|
|
1532cc0e70 | ||
|
|
bd63817e37 | ||
|
|
e8ccfeafe1 | ||
|
|
6c892f095d | ||
|
|
e192623c22 | ||
|
|
c20e9adaeb | ||
|
|
fdb6d210d6 | ||
|
|
3473a10159 | ||
|
|
2d8551f0d0 | ||
|
|
38c74bdfa7 | ||
|
|
117da114dc | ||
|
|
d1f67ea7ac | ||
|
|
6f6846ee2a | ||
|
|
6409d36df0 | ||
|
|
10f8aed021 | ||
|
|
5ee9213ad0 | ||
|
|
6be7527424 | ||
|
|
9edeafb0ec | ||
|
|
09b0ea9a30 | ||
|
|
f409049a51 | ||
|
|
94b4725e24 | ||
|
|
2922a35fd2 | ||
|
|
d417874608 | ||
|
|
bc4abe100e | ||
|
|
393f0cd2f4 | ||
|
|
0c822ffa98 | ||
|
|
624d9d9c4a | ||
|
|
0d7fc0904e | ||
|
|
e5cc4a3d3a | ||
|
|
f51763fc88 | ||
|
|
22390a6781 | ||
|
|
3854372f4d | ||
|
|
6d22aa9955 | ||
|
|
9d052703ad | ||
|
|
3bfdbb5ec7 | ||
|
|
9103ea9d70 | ||
|
|
4c99b0d1d9 | ||
|
|
16474cbd81 | ||
|
|
f0c123932d | ||
|
|
0cdff7dc0e | ||
|
|
3c659a29ae | ||
|
|
42beef408c | ||
|
|
c43d84f14b | ||
|
|
394a728b98 | ||
|
|
c741e9ccae | ||
|
|
ab835286b0 | ||
|
|
6485718a97 | ||
|
|
00bcb54b67 | ||
|
|
fef7863810 | ||
|
|
427b9b4892 | ||
|
|
34adc5451d | ||
|
|
224e8d4bba | ||
|
|
07d75e19d5 | ||
|
|
31871f54d4 | ||
|
|
6069991405 | ||
|
|
a017c71d20 | ||
|
|
4001f14953 | ||
|
|
130413cbef | ||
|
|
0622cc658b | ||
|
|
663a5b196d | ||
|
|
6f676f73a4 | ||
|
|
26d7001ae1 | ||
|
|
f79a8e8177 | ||
|
|
0e5147bb1d | ||
|
|
566fb27fc1 | ||
|
|
8c18829089 | ||
|
|
c68c175827 | ||
|
|
415088ae2d | ||
|
|
158926f192 | ||
|
|
f026ffffc8 | ||
|
|
bd0edea3df | ||
|
|
07b4dad4d3 | ||
|
|
0df796bc03 | ||
|
|
cf0212391e | ||
|
|
cb3a695c25 | ||
|
|
3040d468db | ||
|
|
9df300b241 | ||
|
|
870cef7b65 | ||
|
|
d9843d50cd | ||
|
|
9c64c5732b | ||
|
|
275d87f302 | ||
|
|
509f4953bb | ||
|
|
63d1fb2abb | ||
|
|
b3afa34535 | ||
|
|
8203449d92 | ||
|
|
8a96d8d8a5 | ||
|
|
66df609d4a | ||
|
|
fda090f7dd | ||
|
|
74d4c4a3e6 | ||
|
|
c6798a69d9 | ||
|
|
5a484550fb | ||
|
|
b567e4855f | ||
|
|
c2303f78df | ||
|
|
bbe494f85c | ||
|
|
715dbbb92c | ||
|
|
c2455e3f06 | ||
|
|
997052a872 | ||
|
|
3e13e6db98 | ||
|
|
3291b3ca45 | ||
|
|
74ba1c80a9 | ||
|
|
af76280f1d | ||
|
|
9b7fdfd286 | ||
|
|
cefb95dc74 | ||
|
|
f15a7ff5f6 | ||
|
|
6c123ce2b5 | ||
|
|
d7588eaea1 | ||
|
|
8cd756599f | ||
|
|
5b25a6cb53 | ||
|
|
82a0b67a26 | ||
|
|
1fa6b0a353 | ||
|
|
46151a5e55 | ||
|
|
f36c07aa68 | ||
|
|
86797cda20 | ||
|
|
50bc755b44 | ||
|
|
f02461097b | ||
|
|
b36aed8845 | ||
|
|
991cc0953e | ||
|
|
ef4587b596 | ||
|
|
75e2d1d9ce | ||
|
|
75fc68cd18 | ||
|
|
ae34d886a9 | ||
|
|
f2c187bf7c | ||
|
|
243e51fe83 | ||
|
|
d1d3d91fc1 | ||
|
|
4541b4de03 | ||
|
|
291e1fce55 | ||
|
|
bbb4090cd8 | ||
|
|
a30d29b437 | ||
|
|
e6404be02f | ||
|
|
5e10719168 | ||
|
|
68f9019d0e | ||
|
|
d8e7d343ba | ||
|
|
0e5561032c | ||
|
|
fc490a1ca8 | ||
|
|
6e6aab580c | ||
|
|
dc5b1963ae | ||
|
|
564cfa2201 | ||
|
|
398ab028a4 | ||
|
|
1de712c099 | ||
|
|
2d26eb4a70 | ||
|
|
e9cc567977 | ||
|
|
ee82748aeb | ||
|
|
ee3affd140 | ||
|
|
836090a0d5 | ||
|
|
e0e5e42af2 | ||
|
|
49f340b5f8 | ||
|
|
404a10d3c7 | ||
|
|
62e270e95e | ||
|
|
bfbaaf12fb | ||
|
|
0aa9c7b36e | ||
|
|
8540168133 | ||
|
|
02a35fb8d1 | ||
|
|
4647671f07 | ||
|
|
f30450c3c1 | ||
|
|
731501ba27 | ||
|
|
62a8bf05bb | ||
|
|
d8754a2e3e | ||
|
|
64ffa039b4 | ||
|
|
822724a4f1 | ||
|
|
c18c843d03 | ||
|
|
1d362aceaf | ||
|
|
6f5cb4d1a5 | ||
|
|
2cefa0860d | ||
|
|
ea0c83ea74 | ||
|
|
90f64b685e | ||
|
|
62f925d93c | ||
|
|
1985c110b1 | ||
|
|
461bc94a0b | ||
|
|
aaa55a1f4a | ||
|
|
2eecabf5e6 | ||
|
|
fba9bd87df | ||
|
|
bf461b8b27 | ||
|
|
301c4a83b5 | ||
|
|
87468571ae | ||
|
|
7ad2df3e7f | ||
|
|
baef7b5ec0 | ||
|
|
fa8c59360a | ||
|
|
ceeacd2167 | ||
|
|
70214a6578 | ||
|
|
d1a5e5ba57 | ||
|
|
fcd50257ee | ||
|
|
2765fb0c97 | ||
|
|
d22f345d4a | ||
|
|
b658301725 | ||
|
|
1417375d99 | ||
|
|
676420a2b3 | ||
|
|
ffa9436276 | ||
|
|
f6d92d50e4 | ||
|
|
bc1cb820d1 | ||
|
|
8772c158c6 | ||
|
|
d673473d5e | ||
|
|
bb22a1d62a | ||
|
|
f582d83afc | ||
|
|
b12fa5edfd | ||
|
|
65eee7c1d0 | ||
|
|
de4cf3b554 | ||
|
|
cca04dca1c | ||
|
|
bef3897d0a | ||
|
|
842c3dee5f | ||
|
|
d1d39df71e | ||
|
|
2321343d5e | ||
|
|
ce4d9310aa | ||
|
|
58c53cbe0a | ||
|
|
144979b372 | ||
|
|
e29fa04051 | ||
|
|
5ab5232474 | ||
|
|
d7fef8d89e | ||
|
|
3a09361899 | ||
|
|
6ca17a3f9c | ||
|
|
6d40ff7e1b | ||
|
|
0b29eee9ed | ||
|
|
c7fdfdd035 | ||
|
|
d7626e187c | ||
|
|
39453de8fb | ||
|
|
0c38e93b83 | ||
|
|
bf8db812b8 | ||
|
|
0e35da8370 | ||
|
|
b11fa26162 | ||
|
|
e3836538fc | ||
|
|
9301e3b7de | ||
|
|
cf5e5a14b5 | ||
|
|
7f4546e879 | ||
|
|
1a92c59e3f | ||
|
|
ade9731773 | ||
|
|
cc43b3c743 | ||
|
|
b29f6a1640 | ||
|
|
62b8258989 | ||
|
|
75bc568e4b | ||
|
|
b0119a55df | ||
|
|
78983ce76f | ||
|
|
e060e1d97e | ||
|
|
82bdc228b2 | ||
|
|
1fa8311af7 | ||
|
|
93243f2c77 | ||
|
|
3b423826fd | ||
|
|
e44a7c94c6 | ||
|
|
63c0d260ca | ||
|
|
607ca5742b | ||
|
|
86a3072fa9 | ||
|
|
94c60e1837 | ||
|
|
5e4ad55bbd | ||
|
|
e01d8bc10e | ||
|
|
74605adbee | ||
|
|
c6bd1a28d3 | ||
|
|
b4ea4e7499 | ||
|
|
414db326bb | ||
|
|
3186c1a0d0 | ||
|
|
17c70d6c07 | ||
|
|
57ad200288 | ||
|
|
842c11f414 | ||
|
|
d20be7f836 | ||
|
|
8008f226a8 | ||
|
|
fbfc1557c7 | ||
|
|
dcf6a521a6 | ||
|
|
25562bd55b | ||
|
|
bff35bb13a | ||
|
|
b481b35419 | ||
|
|
d96b2e5bd5 | ||
|
|
8e21039ef1 | ||
|
|
5e42f96eaf | ||
|
|
7bc65031c4 | ||
|
|
fd09a4c815 | ||
|
|
5977e9e82f | ||
|
|
f5edca5a31 | ||
|
|
433868ddf5 | ||
|
|
7c26d583a6 | ||
|
|
9c63bad6ee | ||
|
|
4d4a159d24 | ||
|
|
4cb48b86aa | ||
|
|
2c548be5af | ||
|
|
bfaea09c0b | ||
|
|
06bfe52e7a | ||
|
|
0de9ff38f4 | ||
|
|
768181cf8b | ||
|
|
eb3881fc74 | ||
|
|
2c55722e30 | ||
|
|
26d5972ab5 | ||
|
|
20ba19affc | ||
|
|
c86f57862d | ||
|
|
f92f3e3382 | ||
|
|
cd72c535d0 | ||
|
|
439bc1a2a7 | ||
|
|
fb99cf3805 | ||
|
|
a0bf2b49db | ||
|
|
57e995fc71 | ||
|
|
36134f481e | ||
|
|
632c66539e | ||
|
|
645f98284d | ||
|
|
19dd7aad89 | ||
|
|
d82bd31bf1 | ||
|
|
4fa2f16e9a | ||
|
|
208afdfc3a | ||
|
|
69a3ee6c1a | ||
|
|
96d59fb7cc | ||
|
|
3395d1c853 | ||
|
|
e52fb7d8b5 | ||
|
|
be326c37d0 | ||
|
|
1251232e30 | ||
|
|
fa2a212607 | ||
|
|
af3b0cd5f3 | ||
|
|
86fa54d64a | ||
|
|
576abdeec4 | ||
|
|
04da7ec364 | ||
|
|
815648fe32 | ||
|
|
5af76176cd | ||
|
|
b1ed8307dd | ||
|
|
b8122ec2bd | ||
|
|
2b196b89e9 | ||
|
|
5f1c830f47 | ||
|
|
83fc075bc9 | ||
|
|
8dc000c13b | ||
|
|
b130c89297 | ||
|
|
4bc90588fb | ||
|
|
006b3c7da8 | ||
|
|
1f97f39864 | ||
|
|
c15d75e619 | ||
|
|
844162a7ab | ||
|
|
56c4311a6b | ||
|
|
28dcd19dd9 | ||
|
|
78e4e3fd08 | ||
|
|
1cdc719405 | ||
|
|
3cc218b97e | ||
|
|
1f6e2e7f43 | ||
|
|
9f521bbfb4 | ||
|
|
9c19c639dd | ||
|
|
e3a47d980c | ||
|
|
10539929e1 | ||
|
|
3606870565 | ||
|
|
3307d6f282 | ||
|
|
5dc8d1808c | ||
|
|
8d7cddc20a | ||
|
|
35f9743a10 | ||
|
|
f8c397e231 | ||
|
|
24cf6a3ada | ||
|
|
031ee35a00 | ||
|
|
4507ceb36d | ||
|
|
f6116c1624 | ||
|
|
081df4b535 | ||
|
|
e580af8205 | ||
|
|
226e8eeef2 | ||
|
|
00c4ba430d | ||
|
|
951d3bf6dc | ||
|
|
3ae8e14156 | ||
|
|
f170a6ad36 | ||
|
|
25ff4982a0 | ||
|
|
0ac6ceca3f | ||
|
|
81714bbbed | ||
|
|
5b2715ccc0 | ||
|
|
ab411577a2 | ||
|
|
dce96ecc4b | ||
|
|
a232d73cb1 | ||
|
|
c986baf2ae | ||
|
|
df55d5dcde | ||
|
|
ed5cf89776 | ||
|
|
fa3f2283cf | ||
|
|
2988de4025 | ||
|
|
c6f2ddfd7e | ||
|
|
6a7f042d6c | ||
|
|
5fd82ca6e9 | ||
|
|
829404b33c | ||
|
|
610ec0bed4 | ||
|
|
822f063be3 | ||
|
|
32a3094386 | ||
|
|
07ef32bee3 | ||
|
|
df17d31721 | ||
|
|
302c0dd8f1 | ||
|
|
3d70427e2b | ||
|
|
c29ac61ff4 | ||
|
|
c8fdadde39 | ||
|
|
2ffc28a834 | ||
|
|
75b3082172 | ||
|
|
7e8441264f | ||
|
|
b077f2ec22 | ||
|
|
3262b8fd8f | ||
|
|
090b7e5e26 | ||
|
|
7f05c0bf06 | ||
|
|
fbb6c81986 | ||
|
|
9e843f4ba0 | ||
|
|
4e14ec2742 | ||
|
|
69ccf4872e | ||
|
|
1c31aff147 | ||
|
|
608a5c3787 | ||
|
|
297c980a8d | ||
|
|
4091b11f99 | ||
|
|
4718380bd2 | ||
|
|
52ea19809e | ||
|
|
281bd78104 | ||
|
|
d84e744d05 | ||
|
|
1b5bcad3d8 | ||
|
|
bfc4b138ff | ||
|
|
5a0ec11199 | ||
|
|
c52ba1f859 | ||
|
|
d62235ee58 | ||
|
|
e5d2752436 | ||
|
|
cffaee84bb | ||
|
|
96b1d4fe85 | ||
|
|
02acb76ac9 | ||
|
|
038f542895 | ||
|
|
983e175bc9 | ||
|
|
71ac8d7001 | ||
|
|
5258ac3d1a | ||
|
|
27d1627c8f | ||
|
|
780cf7240b | ||
|
|
ce989e39ab | ||
|
|
63d3c7207d | ||
|
|
eb924ec842 | ||
|
|
f826453284 | ||
|
|
1e00111b00 | ||
|
|
06e1592b54 | ||
|
|
41e0c1e39b | ||
|
|
31f63a387e | ||
|
|
4a1ccf19a0 | ||
|
|
d8b387434b | ||
|
|
2e9a42320c | ||
|
|
1d92f90cbe | ||
|
|
df728fc8e6 | ||
|
|
834e3fb996 | ||
|
|
0b7d4a2c35 | ||
|
|
e13626e92b | ||
|
|
0286010053 | ||
|
|
382e315668 | ||
|
|
5f5c00adb5 | ||
|
|
e093efa931 | ||
|
|
c129eae6a7 | ||
|
|
5b6e5786ea | ||
|
|
8a8471e49a | ||
|
|
719866a574 | ||
|
|
95ebc3bedf | ||
|
|
a8747614bf | ||
|
|
c89f5a7003 | ||
|
|
ebcebfbe5f | ||
|
|
c82782fe9f | ||
|
|
e626f59feb | ||
|
|
cc8d5abcd6 | ||
|
|
83a4c351dd | ||
|
|
4c0c46f5a8 | ||
|
|
1842caff0f | ||
|
|
80d3e8cdb8 | ||
|
|
d936d61b20 | ||
|
|
4a10a81374 | ||
|
|
6404a1b984 | ||
|
|
a3dc145738 | ||
|
|
dbd1b8781a | ||
|
|
9e7874cc04 | ||
|
|
d1d3c96777 | ||
|
|
2d975eb6c9 | ||
|
|
be054fe4ce | ||
|
|
f27902ab92 | ||
|
|
7e43719e20 | ||
|
|
f6016c2769 | ||
|
|
0ba60bd674 | ||
|
|
41b34645f9 | ||
|
|
ceff2840d8 | ||
|
|
6541938f16 | ||
|
|
276eff4f15 | ||
|
|
a3c9d0fe59 | ||
|
|
14f032971b | ||
|
|
9b9c54f775 | ||
|
|
a778a1eaf9 | ||
|
|
5f281518fc | ||
|
|
ca7fa30aa8 | ||
|
|
df4c63e2a3 | ||
|
|
51ce548a00 | ||
|
|
7aa8217ee7 | ||
|
|
a926082e4d | ||
|
|
3ba7b34b25 | ||
|
|
d9531f9617 | ||
|
|
18c14cc452 | ||
|
|
1e7a5647dd | ||
|
|
7edab62651 | ||
|
|
9e9321ab00 | ||
|
|
3caa9a8e38 | ||
|
|
02b6add514 | ||
|
|
67574ff92e | ||
|
|
158c231858 | ||
|
|
479fa34542 | ||
|
|
0ba5db58ec | ||
|
|
a15456896b | ||
|
|
e374772fc6 | ||
|
|
257f02c554 | ||
|
|
826cb41392 | ||
|
|
dcfc74ad0b | ||
|
|
5745137a34 | ||
|
|
3adc4cb0d4 | ||
|
|
fb74ac2601 | ||
|
|
d269cfcd31 | ||
|
|
1871f2bf9e | ||
|
|
c386040f99 | ||
|
|
583111fa9b | ||
|
|
6448a8e821 | ||
|
|
2e7d8d3642 | ||
|
|
6527bd02e8 | ||
|
|
0400e0c6d5 | ||
|
|
3a0a2b14d9 | ||
|
|
9997afeedc | ||
|
|
b026baee34 | ||
|
|
5ee3ef4fe4 | ||
|
|
c90094e328 | ||
|
|
ef1dc172fd | ||
|
|
eadc449bb0 | ||
|
|
8c63d6dd4a | ||
|
|
40c3161416 | ||
|
|
ef2d2875b2 | ||
|
|
2310865425 | ||
|
|
4649e1697b | ||
|
|
b9ddadf9ce | ||
|
|
eae9f46ac4 | ||
|
|
68ffd0814b | ||
|
|
9b12f5a41e | ||
|
|
dd781dc6da | ||
|
|
0cfa6f774b | ||
|
|
13a5a7186c | ||
|
|
fc5cf8aeac | ||
|
|
591440880c | ||
|
|
ac64eedc14 | ||
|
|
b04ea490f8 | ||
|
|
5373e1dc19 | ||
|
|
7410fdbcc9 | ||
|
|
3f4b595779 | ||
|
|
3d357c8c22 | ||
|
|
b2f38200f7 | ||
|
|
10b8a93b5e | ||
|
|
19caa72495 | ||
|
|
6cc649da83 |
@@ -1,7 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
target/
|
||||
console
|
||||
!console/
|
||||
portal-ui/node_modules/
|
||||
.git/
|
||||
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: community, triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## NOTE
|
||||
|
||||
Please subscribe to our [paid subscription plans](https://min.io/pricing) for 24x7 support from our Engineering team.
|
||||
|
||||
<!--- Provide a general summary of the issue in the title above -->
|
||||
|
||||
## Expected Behavior
|
||||
<!--- If you're describing a bug, tell us what should happen -->
|
||||
<!--- If you're suggesting a change/improvement, tell us how it should work -->
|
||||
|
||||
## Current Behavior
|
||||
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
|
||||
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
|
||||
|
||||
## Possible Solution
|
||||
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||
<!--- or ideas how to implement the addition or change -->
|
||||
|
||||
## Steps to Reproduce (for bugs)
|
||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Context
|
||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||
|
||||
## Regression
|
||||
<!-- Is this issue a regression? (Yes / No) -->
|
||||
<!-- If Yes, optionally please include the MinIO version or commit id or PR# that caused this regression, if you have these details. -->
|
||||
|
||||
## Your Environment
|
||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||
* MinIO version used (`minio --version`):
|
||||
* Server setup and configuration:
|
||||
* Operating System and version (`uname -a`):
|
||||
34
.github/workflows/compiles.yml
vendored
34
.github/workflows/compiles.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Compiles on Go ${{ matrix.go-version }} and ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make console
|
||||
34
.github/workflows/crosscompile-1.yml
vendored
34
.github/workflows/crosscompile-1.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Cross compile
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make crosscompile arg1="'linux/ppc64le linux/mips64'"
|
||||
34
.github/workflows/crosscompile-2.yml
vendored
34
.github/workflows/crosscompile-2.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Cross compile
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make crosscompile arg1="'linux/arm64 linux/s390x'"
|
||||
34
.github/workflows/crosscompile-3.yml
vendored
34
.github/workflows/crosscompile-3.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Cross compile
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make crosscompile arg1="'darwin/amd64 freebsd/amd64'"
|
||||
34
.github/workflows/crosscompile-4.yml
vendored
34
.github/workflows/crosscompile-4.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Cross compile
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make crosscompile arg1="'windows/amd64 linux/arm'"
|
||||
34
.github/workflows/crosscompile-5.yml
vendored
34
.github/workflows/crosscompile-5.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Cross compile
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make crosscompile arg1="'linux/386 netbsd/amd64'"
|
||||
34
.github/workflows/go-test-pkg.yml
vendored
34
.github/workflows/go-test-pkg.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Test Pkg on Go ${{ matrix.go-version }} and ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make test-pkg
|
||||
34
.github/workflows/go.yml
vendored
34
.github/workflows/go.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Test Restapi on Go ${{ matrix.go-version }} and ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make test
|
||||
38
.github/workflows/integration.yml
vendored
38
.github/workflows/integration.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||
# updated.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
minio-test:
|
||||
name: Integration Tests with Latest Distributed MinIO
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
run: |
|
||||
make test-integration
|
||||
18
.github/workflows/issues.yaml
vendored
Normal file
18
.github/workflows/issues.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# @format
|
||||
|
||||
name: Issue Workflow
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
name: Add issue to project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v0.5.0
|
||||
with:
|
||||
project-url: https://github.com/orgs/miniohq/projects/2
|
||||
github-token: ${{ secrets.BOT_PAT }}
|
||||
1497
.github/workflows/jobs.yaml
vendored
Normal file
1497
.github/workflows/jobs.yaml
vendored
Normal file
File diff suppressed because it is too large
Load Diff
34
.github/workflows/lint.yml
vendored
34
.github/workflows/lint.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Checking Lint
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
make verifiers
|
||||
15
.github/workflows/react.yml
vendored
15
.github/workflows/react.yml
vendored
@@ -1,15 +0,0 @@
|
||||
name: "React Tests"
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
working-directory: ./portal-ui
|
||||
run: yarn
|
||||
- name: Run tests
|
||||
working-directory: ./portal-ui
|
||||
run: yarn test
|
||||
53
.github/workflows/vulncheck.yaml
vendored
Normal file
53
.github/workflows/vulncheck.yaml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
# @format
|
||||
|
||||
name: Vulnerability Check
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
vulncheck:
|
||||
name: Analysis
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22.5
|
||||
check-latest: true
|
||||
- name: Get official govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
shell: bash
|
||||
- name: Run govulncheck
|
||||
run: govulncheck ./...
|
||||
shell: bash
|
||||
|
||||
react-code-known-vulnerabilities:
|
||||
name: "React Code Has No Known Vulnerable Deps"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [ 1.22.5 ]
|
||||
os: [ ubuntu-latest ]
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
- name: Read .nvmrc
|
||||
id: node_version
|
||||
run: echo "$(cat .nvmrc)" && echo "NVMRC=$(cat .nvmrc)" >> $GITHUB_ENV
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NVMRC }}
|
||||
- name: Checks for known security issues with the installed packages
|
||||
working-directory: ./web-app
|
||||
continue-on-error: false
|
||||
run: |
|
||||
yarn npm audit --recursive --environment production --no-deprecations
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,3 +1,13 @@
|
||||
# Playwright Data
|
||||
web-app/storage/
|
||||
web-app/playwright/.auth/admin.json
|
||||
|
||||
# Report from Playwright
|
||||
web-app/playwright-report/
|
||||
|
||||
# Coverage from Playwright
|
||||
web-app/.nyc_output/
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
@@ -19,6 +29,7 @@ vendor/
|
||||
|
||||
# Ignore executables
|
||||
target/
|
||||
!pkg/logger/target/
|
||||
console
|
||||
!console/
|
||||
|
||||
@@ -26,7 +37,7 @@ dist/
|
||||
|
||||
# Ignore node_modules
|
||||
|
||||
portal-ui/node_modules/
|
||||
web-app/node_modules/
|
||||
|
||||
# Ignore tls cert and key
|
||||
private.key
|
||||
|
||||
@@ -5,34 +5,45 @@ linters-settings:
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
goheader:
|
||||
values:
|
||||
regexp:
|
||||
copyright-holder: Copyright \(c\) (20\d\d\-20\d\d)|2021|({{year}})
|
||||
template-path: .license.tmpl
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- typecheck
|
||||
- goimports
|
||||
- misspell
|
||||
- govet
|
||||
- revive
|
||||
- ineffassign
|
||||
- gosimple
|
||||
- deadcode
|
||||
- unparam
|
||||
- gomodguard
|
||||
- gofmt
|
||||
- unused
|
||||
- structcheck
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- gocritic
|
||||
- gofumpt
|
||||
- durationcheck
|
||||
|
||||
service:
|
||||
golangci-lint-version: 1.27.0 # use the fixed version to not introduce new linters unexpectedly
|
||||
golangci-lint-version: 1.43.0 # use the fixed version to not introduce new linters unexpectedly
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
exclude:
|
||||
- should have a package comment
|
||||
# TODO(y4m4): Remove once all exported ident. have comments!
|
||||
- comment on exported function
|
||||
- comment on exported type
|
||||
- should have comment
|
||||
- use leading k in Go names
|
||||
- comment on exported const
|
||||
- should have a package comment
|
||||
# TODO(y4m4): Remove once all exported ident. have comments!
|
||||
- comment on exported function
|
||||
- comment on exported type
|
||||
- should have comment
|
||||
- use leading k in Go names
|
||||
- comment on exported const
|
||||
run:
|
||||
skip-dirs:
|
||||
- pkg/clientgen
|
||||
- pkg/apis/networking.gke.io
|
||||
- api/operations
|
||||
|
||||
195
.goreleaser.yml
195
.goreleaser.yml
@@ -1,195 +0,0 @@
|
||||
# This is an example goreleaser.yaml file with some sane defaults.
|
||||
# Make sure to check the documentation at http://goreleaser.com
|
||||
project_name: console
|
||||
|
||||
release:
|
||||
name_template: "Release version {{.Tag}}"
|
||||
github:
|
||||
owner: minio
|
||||
name: console
|
||||
extra_files:
|
||||
- glob: "*.minisig"
|
||||
|
||||
before:
|
||||
hooks:
|
||||
# you may remove this if you don't use vgo
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
-
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- ppc64le
|
||||
- s390x
|
||||
- arm64
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
|
||||
main: ./cmd/console/
|
||||
|
||||
flags:
|
||||
- -trimpath
|
||||
- --tags=kqueue
|
||||
|
||||
ldflags:
|
||||
- -s -w -X github.com/minio/console/pkg.ReleaseTag={{.Tag}} -X github.com/minio/console/pkg.CommitID={{.FullCommit}} -X github.com/minio/console/pkg.Version={{.Version}} -X github.com/minio/console/pkg.ShortCommitID={{.ShortCommit}} -X github.com/minio/console/pkg.ReleaseTime={{.Date}}
|
||||
|
||||
archives:
|
||||
-
|
||||
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"
|
||||
format: binary
|
||||
replacements:
|
||||
arm: arm
|
||||
|
||||
signs:
|
||||
-
|
||||
signature: "${artifact}.minisig"
|
||||
cmd: "sh"
|
||||
args:
|
||||
- '-c'
|
||||
- 'minisign -s /media/${USER}/minio/minisign.key -Sm ${artifact} < /media/${USER}/minio/minisign-passphrase'
|
||||
artifacts: all
|
||||
|
||||
snapshot:
|
||||
name_template: v0.0.0@{{.ShortCommit}}
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
|
||||
nfpms:
|
||||
-
|
||||
vendor: MinIO, Inc.
|
||||
homepage: https://github.com/minio/console
|
||||
maintainer: MinIO Development <dev@min.io>
|
||||
description: MinIO Console Server
|
||||
license: GNU Affero General Public License v3.0
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
contents:
|
||||
# Basic file that applies to all packagers
|
||||
- src: systemd/console.service
|
||||
dst: /etc/systemd/system/minio-console.service
|
||||
|
||||
dockers:
|
||||
- image_templates:
|
||||
- "minio/console:{{ .Tag }}-amd64"
|
||||
use: buildx
|
||||
goarch: amd64
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
- image_templates:
|
||||
- "minio/console:{{ .Tag }}-ppc64le"
|
||||
use: buildx
|
||||
goarch: ppc64le
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/ppc64le"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
- image_templates:
|
||||
- "minio/console:{{ .Tag }}-s390x"
|
||||
use: buildx
|
||||
goarch: s390x
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/s390x"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
- image_templates:
|
||||
- "minio/console:{{ .Tag }}-arm64"
|
||||
use: buildx
|
||||
goarch: arm64
|
||||
goos: linux
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
- image_templates:
|
||||
- "quay.io/minio/console:{{ .Tag }}-amd64"
|
||||
use: buildx
|
||||
goarch: amd64
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
- image_templates:
|
||||
- "quay.io/minio/console:{{ .Tag }}-ppc64le"
|
||||
use: buildx
|
||||
goarch: ppc64le
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/ppc64le"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
- image_templates:
|
||||
- "quay.io/minio/console:{{ .Tag }}-s390x"
|
||||
use: buildx
|
||||
goarch: s390x
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/s390x"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
- image_templates:
|
||||
- "quay.io/minio/console:{{ .Tag }}-arm64"
|
||||
use: buildx
|
||||
goarch: arm64
|
||||
goos: linux
|
||||
dockerfile: Dockerfile.release
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- CREDITS
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64"
|
||||
- "--build-arg=TAG={{ .Tag }}"
|
||||
docker_manifests:
|
||||
- name_template: minio/console:{{ .Tag }}
|
||||
image_templates:
|
||||
- minio/console:{{ .Tag }}-amd64
|
||||
- minio/console:{{ .Tag }}-arm64
|
||||
- minio/console:{{ .Tag }}-ppc64le
|
||||
- minio/console:{{ .Tag }}-s390x
|
||||
- name_template: quay.io/minio/console:{{ .Tag }}
|
||||
image_templates:
|
||||
- quay.io/minio/console:{{ .Tag }}-amd64
|
||||
- quay.io/minio/console:{{ .Tag }}-arm64
|
||||
- quay.io/minio/console:{{ .Tag }}-ppc64le
|
||||
- quay.io/minio/console:{{ .Tag }}-s390x
|
||||
- name_template: minio/console:latest
|
||||
image_templates:
|
||||
- minio/console:{{ .Tag }}-amd64
|
||||
- minio/console:{{ .Tag }}-arm64
|
||||
- minio/console:{{ .Tag }}-ppc64le
|
||||
- minio/console:{{ .Tag }}-s390x
|
||||
15
.license.tmpl
Normal file
15
.license.tmpl
Normal file
@@ -0,0 +1,15 @@
|
||||
This file is part of MinIO Console Server
|
||||
{{copyright-holder}} MinIO, Inc.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
35
.semgrepignore
Normal file
35
.semgrepignore
Normal file
@@ -0,0 +1,35 @@
|
||||
# Ignore git items
|
||||
.gitignore
|
||||
.git/
|
||||
:include .gitignore
|
||||
|
||||
# Common large paths
|
||||
node_modules/
|
||||
web-app/node_modules/
|
||||
build/
|
||||
dist/
|
||||
.idea/
|
||||
vendor/
|
||||
.env/
|
||||
.venv/
|
||||
.tox/
|
||||
*.min.js
|
||||
|
||||
# Common test paths
|
||||
test/
|
||||
tests/
|
||||
*_test.go
|
||||
|
||||
# Semgrep rules folder
|
||||
.semgrep
|
||||
|
||||
# Semgrep-action log folder
|
||||
.semgrep_logs/
|
||||
|
||||
# Ignore VsCode files
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
*~
|
||||
.eslintcache
|
||||
|
||||
consoleApi.ts
|
||||
389
CHANGELOG.md
Normal file
389
CHANGELOG.md
Normal file
@@ -0,0 +1,389 @@
|
||||
<!-- @format -->
|
||||
|
||||
# Changelog
|
||||
|
||||
## Release v1.7.0
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed directory listing
|
||||
- Fix MinIO videos link
|
||||
|
||||
Additional Changes:
|
||||
|
||||
- Removed deprecated KES functionality
|
||||
|
||||
## Release v1.6.3
|
||||
|
||||
Additional Changes:
|
||||
|
||||
- Updated go.mod version
|
||||
|
||||
## Release v1.6.2
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed minor user session issues
|
||||
- Updated project dependencies
|
||||
|
||||
Additional Changes:
|
||||
|
||||
- Improved Drives List visualization
|
||||
- Improved WS request logic
|
||||
- Updated License page with current MinIO plans.
|
||||
|
||||
## Release v1.6.1
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed objectManager issues under certain conditions
|
||||
- Fixed Security vulnerability in dependencies
|
||||
|
||||
Additional Changes:
|
||||
|
||||
- Improved Share Link behavior
|
||||
|
||||
## Release v1.6.0
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed share link encoding
|
||||
- Fixed Edit Lifecycle Storage Class
|
||||
- Added Tiers Improvements for Bucket Lifecycle management
|
||||
|
||||
Additional Changes:
|
||||
|
||||
- Vulnerability updates
|
||||
- Update Logo logic
|
||||
|
||||
## Release v1.5.0
|
||||
|
||||
Features:
|
||||
|
||||
- Added remove Tier functionality
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed ILM rule tags not being shown
|
||||
- Fixed race condition Object Browser websocket
|
||||
- Fixed Encryption page crashing on empty response
|
||||
- Fixed Replication Delete Marker comparisons
|
||||
|
||||
Additional Changes:
|
||||
|
||||
- Use automatic URI encoding for APIs
|
||||
- Vulnerability updates
|
||||
|
||||
## Release v1.4.0
|
||||
|
||||
Features:
|
||||
|
||||
- Added VersionID support to metadata details
|
||||
- Improved Websockets handlers
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed vulnerabilities and updated dependencies
|
||||
- Fixed an issue with Download URL decoding
|
||||
- Fixed leak in Object Browser Websocket
|
||||
- Minor UX fixes
|
||||
|
||||
## Release v1.3.0
|
||||
|
||||
Features:
|
||||
|
||||
- Adds ExpireDeleteMarker status to BucketLifecycleRule UI
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed vulnerability
|
||||
- Used URL-safe base64 enconding for Share API
|
||||
- Made Prefix field optional when Adding Tier
|
||||
- Added Console user agent in MinIO Admin Client
|
||||
|
||||
## Release v1.2.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated file share logic to work as Proxy
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Updated project dependencies
|
||||
- Fixed Key Permissions UX
|
||||
- Added permissions validation to rewind button
|
||||
- Fixed Health report upload to SUBNET
|
||||
- Misc Cosmetic fixes
|
||||
|
||||
## Release v1.1.1
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed folder download issue
|
||||
|
||||
## Release v1.1.0
|
||||
|
||||
Features:
|
||||
|
||||
- Added Set Expired object all versions selector
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Updated Go Dependencies
|
||||
|
||||
## Release v1.0.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated Preview message alert
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Updated Websocket API
|
||||
- Fixed issues with download manager
|
||||
- Fixed policies issues
|
||||
|
||||
## Release v0.46.0
|
||||
|
||||
Features:
|
||||
|
||||
- Added latest help content to forms
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Disabled Create User button in certain policy cases
|
||||
- Fixed an issue with Logout request
|
||||
- Upgraded project dependencies
|
||||
|
||||
## Release v0.45.0
|
||||
|
||||
Deprecated:
|
||||
|
||||
- Deprecated Heal / Drives page
|
||||
|
||||
Features:
|
||||
|
||||
- Updated tines on menus & pages
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Upgraded project dependencies
|
||||
|
||||
## Release v0.44.0
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Upgraded project dependencies
|
||||
- Fixed events icons not loading in subpaths
|
||||
|
||||
## Release v0.43.1
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Update Share Object UI to reflect maximum expiration time in UI
|
||||
|
||||
## Release v0.43.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated PDF preview method
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed vulnerabilities
|
||||
- Prevented non-necessary metadata calls in object browser
|
||||
|
||||
## Release v0.42.2
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Hidden Prometheus metrics if URL is empty
|
||||
|
||||
## Release v0.42.1
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Reset go version to 1.19
|
||||
|
||||
## Release v0.42.0
|
||||
|
||||
Features:
|
||||
|
||||
- Introducing Dark Mode
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed vulnerabilities
|
||||
- Changes on Upload and Delete object urls
|
||||
- Fixed blocking subpath creation if not enough permissions
|
||||
- Removed share object option at prefix level
|
||||
- Updated allowed actions for a deleted object
|
||||
|
||||
## Release v0.41.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated pages to use mds components
|
||||
- support for resolving IPv4/IPv6
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Remove cache for ClientIP
|
||||
- Fixed override environment variables display in settings page
|
||||
- Fixed daylight savings time support in share modal
|
||||
|
||||
## Release v0.40.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated OpenID page
|
||||
- Added New bucket event types support
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed crash in access keys page
|
||||
- Fixed AuditLog filters issue
|
||||
- Fixed multiple issues with Object Browser
|
||||
|
||||
## Release v0.39.0
|
||||
|
||||
Features:
|
||||
|
||||
- Migrated metrics page to mds
|
||||
- Migrated Register page to mds
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed LDAP configuration page issues
|
||||
- Load available certificates in logout
|
||||
- Updated dependencies & go version
|
||||
- Fixed delete objects functionality
|
||||
|
||||
## Release v0.38.0
|
||||
|
||||
Features:
|
||||
|
||||
- Added extra information to Service Accounts page
|
||||
- Updated Tiers, Site Replication, Speedtest, Heal & Watch pages components
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed IDP expiry time errors
|
||||
- Updated project Dependencies
|
||||
|
||||
## Release v0.37.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated Trace and Logs page components
|
||||
- Updated Prometheus metrics
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Disabled input fields for Subscription features if MinIO is not registered
|
||||
|
||||
## Release v0.36.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated Settings page components
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Show LDAP Enabled value LDAP configuration
|
||||
- Download multiple objects in same path as they were selected
|
||||
|
||||
## Release v0.35.1
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Change timestamp format for zip creation
|
||||
|
||||
## Release v0.35.0
|
||||
|
||||
Features:
|
||||
|
||||
- Add Exclude Folders and Exclude Prefixes during bucket creation
|
||||
- Download multiple selected objects as zip and ignore deleted objects
|
||||
- Updated Call Home, Inspet, Profile and Health components
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Remove extra white spaces for configuration strings
|
||||
- Allow Create New Path in bucket view when having right permissions
|
||||
|
||||
## Release v0.34.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated Buckets components
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed SUBNET Health report upload
|
||||
- Updated Download Handler
|
||||
- Fixes issue with rewind
|
||||
- Avoid 1 hour expiration for IDP credentials
|
||||
|
||||
---
|
||||
|
||||
## Release v0.33.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated OpenID, LDAP components
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed security issues
|
||||
- Fixed navigation issues in Object Browser
|
||||
- Fixed Dashboard metrics
|
||||
|
||||
---
|
||||
|
||||
## Release v0.32.0
|
||||
|
||||
Features:
|
||||
|
||||
- Updated Users and Groups components
|
||||
- Added placeholder image for Help Menu
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed memory leak in WebSocket API for Object Browser
|
||||
|
||||
---
|
||||
|
||||
## Release v0.31.0
|
||||
|
||||
**Breaking Changes:**
|
||||
|
||||
- **Removed support for Standalone Deployments**
|
||||
|
||||
Features:
|
||||
|
||||
- Updated way files are displayed in uploading component
|
||||
- Updated Audit Logs and Policies components
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Fixed Download folders issue in Object Browser
|
||||
- Added missing Notification Events (ILM & REPLICA) in Events Notification Page
|
||||
- Fixed Security Vulnerability for `semver` dependency
|
||||
|
||||
---
|
||||
|
||||
## Release v0.30.0
|
||||
|
||||
Features:
|
||||
|
||||
- Added MinIO Console Help Menu
|
||||
- Updated UI Menu components
|
||||
|
||||
Bug Fix:
|
||||
|
||||
- Disable the Upload button on Object Browser if the user is not allowed
|
||||
- Fixed security vulnerability for `lestrrat-go/jwx` and `fast-xml-parser`
|
||||
- Fixed bug on sub-paths for Object Browser
|
||||
- Reduce the number of calls to `/session` API endpoint to improve performance
|
||||
- Rolled back the previous change for the Share File feature to no longer ask for Service Account access keys
|
||||
@@ -4,56 +4,80 @@ This is a REST portal server created using [go-swagger](https://github.com/go-sw
|
||||
|
||||
The API handlers are created using a YAML definition located in `swagger.YAML`.
|
||||
|
||||
To add new api, the YAML file needs to be updated with all the desired apis using the [Swagger Basic Structure](https://swagger.io/docs/specification/2-0/basic-structure/), this includes paths, parameters, definitions, tags, etc.
|
||||
To add new api, the YAML file needs to be updated with all the desired apis using
|
||||
the [Swagger Basic Structure](https://swagger.io/docs/specification/2-0/basic-structure/), this includes paths,
|
||||
parameters, definitions, tags, etc.
|
||||
|
||||
## Generate server from YAML
|
||||
|
||||
Once the YAML file is ready we can autogenerate the code needed for the new api by just running:
|
||||
|
||||
Validate it:
|
||||
|
||||
```
|
||||
swagger validate ./swagger.yml
|
||||
```
|
||||
|
||||
Update server code:
|
||||
|
||||
```
|
||||
make swagger-gen
|
||||
```
|
||||
|
||||
This will update all the necessary code.
|
||||
|
||||
`./restapi/configure_console.go` is a file that contains the handlers to be used by the application, here is the only place where we need to update our code to support the new apis. This file is not affected when running the swagger generator and it is safe to edit.
|
||||
`./api/configure_console.go` is a file that contains the handlers to be used by the application, here is the only place
|
||||
where we need to update our code to support the new apis. This file is not affected when running the swagger generator
|
||||
and it is safe to edit.
|
||||
|
||||
## Unit Tests
|
||||
`./restapi/handlers_test.go` needs to be updated with the proper tests for the new api.
|
||||
|
||||
`./api/handlers_test.go` needs to be updated with the proper tests for the new api.
|
||||
|
||||
To run tests:
|
||||
|
||||
```
|
||||
go test ./restapi
|
||||
go test ./api
|
||||
```
|
||||
|
||||
## Commit changes
|
||||
After verification, commit your changes. This is a [great post](https://chris.beams.io/posts/git-commit/) on how to write useful commit messages
|
||||
|
||||
After verification, commit your changes. This is a [great post](https://chris.beams.io/posts/git-commit/) on how to
|
||||
write useful commit messages
|
||||
|
||||
```
|
||||
$ git commit -am 'Add some feature'
|
||||
```
|
||||
|
||||
### Push to the branch
|
||||
|
||||
Push your locally committed changes to the remote origin (your fork)
|
||||
|
||||
```
|
||||
$ git push origin my-new-feature
|
||||
```
|
||||
|
||||
### Create a Pull Request
|
||||
Pull requests can be created via GitHub. Refer to [this document](https://help.github.com/articles/creating-a-pull-request/) for detailed steps on how to create a pull request. After a Pull Request gets peer reviewed and approved, it will be merged.
|
||||
|
||||
Pull requests can be created via GitHub. Refer
|
||||
to [this document](https://help.github.com/articles/creating-a-pull-request/) for detailed steps on how to create a pull
|
||||
request. After a Pull Request gets peer reviewed and approved, it will be merged.
|
||||
|
||||
## FAQs
|
||||
|
||||
### How does ``console`` manages dependencies?
|
||||
|
||||
``MinIO`` uses `go mod` to manage its dependencies.
|
||||
|
||||
- Run `go get foo/bar` in the source folder to add the dependency to `go.mod` file.
|
||||
|
||||
To remove a dependency
|
||||
|
||||
- Edit your code and remove the import reference.
|
||||
- Run `go mod tidy` in the source folder to remove dependency from `go.mod` file.
|
||||
|
||||
### What are the coding guidelines for console?
|
||||
``console`` is fully conformant with Golang style. Refer: [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project. If you observe offending code, please feel free to send a pull request or ping us on [Slack](https://slack.min.io).
|
||||
|
||||
``console`` is fully conformant with Golang style.
|
||||
Refer: [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project. If you observe
|
||||
offending code, please feel free to send a pull request or ping us on [Slack](https://slack.min.io).
|
||||
|
||||
@@ -1,3 +1,82 @@
|
||||
# Developing MinIO Console
|
||||
|
||||
The MinIO Console requires the [MinIO Server](https://github.com/minio/minio). For development purposes, you also need
|
||||
to run both the MinIO Console web app and the MinIO Console server.
|
||||
|
||||
## Running MinIO Console server
|
||||
|
||||
Build the server in the main folder by running:
|
||||
|
||||
```
|
||||
make
|
||||
```
|
||||
|
||||
> Note: If it's the first time running the server, you might need to run `go mod tidy` to ensure you have all modules
|
||||
> required.
|
||||
> To start the server run:
|
||||
|
||||
```
|
||||
CONSOLE_ACCESS_KEY=<your-access-key>
|
||||
CONSOLE_SECRET_KEY=<your-secret-key>
|
||||
CONSOLE_MINIO_SERVER=<minio-server-endpoint>
|
||||
CONSOLE_DEV_MODE=on
|
||||
./console server
|
||||
```
|
||||
|
||||
## Running MinIO Console web app
|
||||
|
||||
Refer to `/web-app` [instructions](/web-app/README.md) to run the web app locally.
|
||||
|
||||
# Building with MinIO
|
||||
|
||||
To test console in its shipping format, you need to build it from the MinIO repository, the following step will guide
|
||||
you to do that.
|
||||
|
||||
### 0. Building with UI Changes
|
||||
|
||||
If you are performing changes in the UI components of console and want to test inside the MinIO binary, you need to
|
||||
build assets first.
|
||||
|
||||
In the console folder run
|
||||
|
||||
```shell
|
||||
make assets
|
||||
```
|
||||
|
||||
This will regenerate all the static assets that will be served by MinIO.
|
||||
|
||||
### 1. Clone the `MinIO` repository
|
||||
|
||||
In the parent folder of where you cloned this `console` repository, clone the MinIO Repository
|
||||
|
||||
```shell
|
||||
git clone https://github.com/minio/minio.git
|
||||
```
|
||||
|
||||
### 2. Update `go.mod` to use your local version
|
||||
|
||||
In the MinIO repository open `go.mod` and after the first `require()` directive add a `replace()` directive
|
||||
|
||||
```
|
||||
...
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/minio/console => "../console"
|
||||
)
|
||||
|
||||
require (
|
||||
...
|
||||
```
|
||||
|
||||
### 3. Build `MinIO`
|
||||
|
||||
Still in the MinIO folder, run
|
||||
|
||||
```shell
|
||||
make build
|
||||
```
|
||||
|
||||
# LDAP authentication with Console
|
||||
|
||||
## Setup
|
||||
@@ -15,7 +94,8 @@ $ docker cp console/docs/ldap/billy.ldif my-openldap-container:/container/servic
|
||||
$ docker exec my-openldap-container ldapadd -x -D "cn=admin,dc=example,dc=org" -w admin -f /container/service/slapd/assets/test/billy.ldif -H ldap://localhost
|
||||
```
|
||||
|
||||
Query the ldap server to check the user billy was created correctly and got assigned to the consoleAdmin group, you should get a list
|
||||
Query the ldap server to check the user billy was created correctly and got assigned to the consoleAdmin group, you
|
||||
should get a list
|
||||
containing ldap users and groups.
|
||||
|
||||
```
|
||||
@@ -30,7 +110,7 @@ $ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b uid=bil
|
||||
|
||||
### Change the password for user billy
|
||||
|
||||
Set the new password for `billy` to `minio123` and enter `admin` as the default `LDAP Password`
|
||||
Set the new password for `billy` to `minio123` and enter `admin` as the default `LDAP Password`
|
||||
|
||||
```
|
||||
$ docker exec -it my-openldap-container /bin/bash
|
||||
@@ -41,6 +121,7 @@ Enter LDAP Password:
|
||||
```
|
||||
|
||||
### Add the consoleAdmin policy to user billy on MinIO
|
||||
|
||||
```
|
||||
$ cat > consoleAdmin.json << EOF
|
||||
{
|
||||
@@ -66,8 +147,8 @@ $ cat > consoleAdmin.json << EOF
|
||||
]
|
||||
}
|
||||
EOF
|
||||
$ mc admin policy add myminio consoleAdmin consoleAdmin.json
|
||||
$ mc admin policy set myminio consoleAdmin user="uid=billy,dc=example,dc=org"
|
||||
$ mc admin policy create myminio consoleAdmin consoleAdmin.json
|
||||
$ mc admin policy attach myminio consoleAdmin --user="uid=billy,dc=example,dc=org"
|
||||
```
|
||||
|
||||
## Run MinIO
|
||||
|
||||
42
Dockerfile
42
Dockerfile
@@ -1,42 +0,0 @@
|
||||
FROM node:14 as uilayer
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./portal-ui/package.json ./
|
||||
COPY ./portal-ui/yarn.lock ./
|
||||
RUN yarn install
|
||||
|
||||
COPY ./portal-ui .
|
||||
|
||||
RUN make build-static
|
||||
|
||||
USER node
|
||||
|
||||
FROM golang:1.16 as golayer
|
||||
|
||||
RUN apt-get update -y && apt-get install -y ca-certificates
|
||||
|
||||
ADD go.mod /go/src/github.com/minio/console/go.mod
|
||||
ADD go.sum /go/src/github.com/minio/console/go.sum
|
||||
WORKDIR /go/src/github.com/minio/console/
|
||||
|
||||
# Get dependencies - will also be cached if we won't change mod/sum
|
||||
RUN go mod download
|
||||
|
||||
ADD . /go/src/github.com/minio/console/
|
||||
WORKDIR /go/src/github.com/minio/console/
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
COPY --from=uilayer /app/build /go/src/github.com/minio/console/portal-ui/build
|
||||
RUN go build -ldflags "-w -s" -a -o console ./cmd/console
|
||||
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3
|
||||
MAINTAINER MinIO Development "dev@min.io"
|
||||
EXPOSE 9090
|
||||
|
||||
|
||||
COPY --from=golayer /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=golayer /go/src/github.com/minio/console/console .
|
||||
|
||||
ENTRYPOINT ["/console"]
|
||||
@@ -1,13 +0,0 @@
|
||||
FROM node:14 as uilayer
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./portal-ui/package.json ./
|
||||
COPY ./portal-ui/yarn.lock ./
|
||||
RUN yarn install
|
||||
|
||||
COPY ./portal-ui .
|
||||
|
||||
RUN yarn install && make build-static
|
||||
|
||||
USER node
|
||||
@@ -1,23 +0,0 @@
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
|
||||
|
||||
ARG TAG
|
||||
|
||||
COPY CREDITS /licenses/CREDITS
|
||||
COPY LICENSE /licenses/LICENSE
|
||||
|
||||
LABEL name="MinIO" \
|
||||
vendor="MinIO Inc <dev@min.io>" \
|
||||
maintainer="MinIO Inc <dev@min.io>" \
|
||||
version="${TAG}" \
|
||||
release="${TAG}" \
|
||||
summary="A graphical user interface for MinIO" \
|
||||
description="MinIO object storage is fundamentally different. Designed for performance and the S3 API, it is 100% open-source. MinIO is ideal for large, private cloud environments with stringent security requirements and delivers mission-critical availability across a diverse range of workloads."
|
||||
|
||||
RUN \
|
||||
microdnf update --nodocs && \
|
||||
microdnf install ca-certificates --nodocs
|
||||
|
||||
EXPOSE 9090
|
||||
COPY console /console
|
||||
|
||||
ENTRYPOINT ["/console"]
|
||||
255
Makefile
255
Makefile
@@ -4,6 +4,9 @@ GOPATH := $(shell go env GOPATH)
|
||||
BUILD_VERSION:=$(shell git describe --exact-match --tags $(git log -n1 --pretty='%h') 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
||||
BUILD_TIME:=$(shell date 2>/dev/null)
|
||||
TAG ?= "minio/console:$(BUILD_VERSION)-dev"
|
||||
MINIO_VERSION ?= "quay.io/minio/minio:latest"
|
||||
TARGET_BUCKET ?= "target"
|
||||
NODE_VERSION := $(shell cat .nvmrc)
|
||||
|
||||
default: console
|
||||
|
||||
@@ -12,23 +15,15 @@ console:
|
||||
@echo "Building Console binary to './console'"
|
||||
@(GO111MODULE=on CGO_ENABLED=0 go build -trimpath --tags=kqueue --ldflags "-s -w" -o console ./cmd/console)
|
||||
|
||||
k8sdev:
|
||||
@docker build -t $(TAG) --build-arg build_version=$(BUILD_VERSION) --build-arg build_time='$(BUILD_TIME)' .
|
||||
@kind load docker-image $(TAG)
|
||||
@echo "Done, now restart your console deployment"
|
||||
|
||||
getdeps:
|
||||
@mkdir -p ${GOPATH}/bin
|
||||
@which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.40.1)
|
||||
@echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin
|
||||
|
||||
verifiers: getdeps fmt lint
|
||||
|
||||
fmt:
|
||||
@echo "Running $@ check"
|
||||
@GO111MODULE=on gofmt -d restapi/
|
||||
@GO111MODULE=on gofmt -d pkg/
|
||||
@GO111MODULE=on gofmt -d cmd/
|
||||
@GO111MODULE=on gofmt -d cluster/
|
||||
@(env bash $(PWD)/verify-gofmt.sh)
|
||||
|
||||
crosscompile:
|
||||
@(env bash $(PWD)/cross-compile.sh $(arg1))
|
||||
@@ -38,44 +33,248 @@ lint:
|
||||
@GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean
|
||||
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
|
||||
|
||||
lint-fix: getdeps ## runs golangci-lint suite of linters with automatic fixes
|
||||
@echo "Running $@ check"
|
||||
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml --fix
|
||||
|
||||
install: console
|
||||
@echo "Installing console binary to '$(GOPATH)/bin/console'"
|
||||
@mkdir -p $(GOPATH)/bin && cp -f $(PWD)/console $(GOPATH)/bin/console
|
||||
@echo "Installation successful. To learn more, try \"console --help\"."
|
||||
|
||||
swagger-gen: clean-swagger swagger-console swagger-operator
|
||||
swagger-gen: clean-swagger swagger-console apply-gofmt
|
||||
@echo "Done Generating swagger server code from yaml"
|
||||
|
||||
apply-gofmt:
|
||||
@echo "Applying gofmt to all generated an existing files"
|
||||
@GO111MODULE=on gofmt -w .
|
||||
|
||||
clean-swagger:
|
||||
@echo "cleaning"
|
||||
@rm -rf models
|
||||
@rm -rf restapi/operations
|
||||
@rm -rf operatorapi/operations
|
||||
@rm -rf api/operations
|
||||
|
||||
swagger-console:
|
||||
@echo "Generating swagger server code from yaml"
|
||||
@swagger generate server -A console --main-package=management --server-package=restapi --exclude-main -P models.Principal -f ./swagger-console.yml -r NOTICE
|
||||
@swagger generate server -A console --main-package=management --server-package=api --exclude-main -P models.Principal -f ./swagger.yml -r NOTICE
|
||||
@echo "Generating typescript api"
|
||||
@npx swagger-typescript-api -p ./swagger.yml -o ./web-app/src/api -n consoleApi.ts --custom-config generator.config.js
|
||||
@git restore api/server.go
|
||||
|
||||
swagger-operator:
|
||||
@echo "Generating swagger server code from yaml"
|
||||
@swagger generate server -A operator --main-package=operator --server-package=operatorapi --exclude-main -P models.Principal -f ./swagger-operator.yml -r NOTICE
|
||||
|
||||
assets:
|
||||
@(cd portal-ui; yarn install; make build-static; yarn prettier --write . --loglevel warn; cd ..)
|
||||
@(if [ -f "${NVM_DIR}/nvm.sh" ]; then \. "${NVM_DIR}/nvm.sh" && nvm install && nvm use && npm install -g yarn ; fi &&\
|
||||
cd web-app; corepack enable; yarn install --prefer-offline; make build-static; yarn prettier --write . --loglevel warn; cd ..)
|
||||
|
||||
test-integration:
|
||||
@(docker run -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4} && sleep 5)
|
||||
@(GO111MODULE=on go test -race -v github.com/minio/console/integration/...)
|
||||
@(docker stop pgsqlcontainer || true)
|
||||
@(docker stop minio || true)
|
||||
@(docker stop minio2 || true)
|
||||
@(docker network rm mynet123 || true)
|
||||
@echo "create docker network to communicate containers MinIO & PostgreSQL"
|
||||
@(docker network create --subnet=173.18.0.0/29 mynet123)
|
||||
@echo "docker run with MinIO Version below:"
|
||||
@echo $(MINIO_VERSION)
|
||||
@echo "MinIO 1"
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 --net=mynet123 -d --name minio --rm -p 9000:9000 -p 9091:9091 -e MINIO_KMS_SECRET_KEY=my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw= $(MINIO_VERSION) server /data{1...4} --console-address ':9091' && sleep 5)
|
||||
@echo "MinIO 2"
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 --net=mynet123 -d --name minio2 --rm -p 9001:9001 -p 9092:9092 -e MINIO_KMS_SECRET_KEY=my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw= $(MINIO_VERSION) server /data{1...4} --address ':9001' --console-address ':9092' && sleep 5)
|
||||
@echo "Postgres"
|
||||
@(docker run --net=mynet123 --ip=173.18.0.4 --name pgsqlcontainer --rm -p 5432:5432 -e POSTGRES_PASSWORD=password -d postgres && sleep 5)
|
||||
@echo "execute test and get coverage for test-integration:"
|
||||
@(cd integration && go test -coverpkg=../api -c -tags testrunmain . && mkdir -p coverage && ./integration.test -test.v -test.run "^Test*" -test.coverprofile=coverage/system.out)
|
||||
@(docker stop pgsqlcontainer)
|
||||
@(docker stop minio)
|
||||
@(docker stop minio2)
|
||||
@(docker network rm mynet123)
|
||||
|
||||
test-replication:
|
||||
@(docker stop minio || true)
|
||||
@(docker stop minio1 || true)
|
||||
@(docker stop minio2 || true)
|
||||
@(docker network rm mynet123 || true)
|
||||
@(docker network create mynet123)
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 \
|
||||
--net=mynet123 -d \
|
||||
--name minio \
|
||||
--rm \
|
||||
-p 9000:9000 \
|
||||
-p 6000:6000 \
|
||||
-e MINIO_KMS_SECRET_KEY=my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw= \
|
||||
-e MINIO_ROOT_USER="minioadmin" \
|
||||
-e MINIO_ROOT_PASSWORD="minioadmin" \
|
||||
$(MINIO_VERSION) server /data{1...4} \
|
||||
--address :9000 \
|
||||
--console-address :6000)
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 \
|
||||
--net=mynet123 -d \
|
||||
--name minio1 \
|
||||
--rm \
|
||||
-p 9001:9001 \
|
||||
-p 6001:6001 \
|
||||
-e MINIO_KMS_SECRET_KEY=my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw= \
|
||||
-e MINIO_ROOT_USER="minioadmin" \
|
||||
-e MINIO_ROOT_PASSWORD="minioadmin" \
|
||||
$(MINIO_VERSION) server /data{1...4} \
|
||||
--address :9001 \
|
||||
--console-address :6001)
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 \
|
||||
--net=mynet123 -d \
|
||||
--name minio2 \
|
||||
--rm \
|
||||
-p 9002:9002 \
|
||||
-p 6002:6002 \
|
||||
-e MINIO_KMS_SECRET_KEY=my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw= \
|
||||
-e MINIO_ROOT_USER="minioadmin" \
|
||||
-e MINIO_ROOT_PASSWORD="minioadmin" \
|
||||
$(MINIO_VERSION) server /data{1...4} \
|
||||
--address :9002 \
|
||||
--console-address :6002)
|
||||
@(cd replication && go test -coverpkg=../api -c -tags testrunmain . && mkdir -p coverage && ./replication.test -test.v -test.run "^Test*" -test.coverprofile=coverage/replication.out)
|
||||
@(docker stop minio || true)
|
||||
@(docker stop minio1 || true)
|
||||
@(docker stop minio2 || true)
|
||||
@(docker network rm mynet123 || true)
|
||||
|
||||
test-sso-integration:
|
||||
@echo "create the network in bridge mode to communicate all containers"
|
||||
@(docker network create my-net)
|
||||
@echo "run openldap container using MinIO Image: quay.io/minio/openldap:latest"
|
||||
@(docker run \
|
||||
-e LDAP_ORGANIZATION="MinIO Inc" \
|
||||
-e LDAP_DOMAIN="min.io" \
|
||||
-e LDAP_ADMIN_PASSWORD="admin" \
|
||||
--network my-net \
|
||||
-p 389:389 \
|
||||
-p 636:636 \
|
||||
--name openldap \
|
||||
--detach quay.io/minio/openldap:latest)
|
||||
@echo "Run Dex container using MinIO Image: quay.io/minio/dex:latest"
|
||||
@(docker run \
|
||||
-e DEX_ISSUER=http://dex:5556/dex \
|
||||
-e DEX_CLIENT_REDIRECT_URI=http://127.0.0.1:9090/oauth_callback \
|
||||
-e DEX_LDAP_SERVER=openldap:389 \
|
||||
--network my-net \
|
||||
-p 5556:5556 \
|
||||
--name dex \
|
||||
--detach quay.io/minio/dex:latest)
|
||||
@echo "running minio server"
|
||||
@(docker run \
|
||||
-v /data1 -v /data2 -v /data3 -v /data4 \
|
||||
--network my-net \
|
||||
-d \
|
||||
--name minio \
|
||||
--rm \
|
||||
-p 9000:9000 \
|
||||
-p 9001:9001 \
|
||||
-e MINIO_IDENTITY_OPENID_CLIENT_ID="minio-client-app" \
|
||||
-e MINIO_IDENTITY_OPENID_CLIENT_SECRET="minio-client-app-secret" \
|
||||
-e MINIO_IDENTITY_OPENID_CLAIM_NAME=name \
|
||||
-e MINIO_IDENTITY_OPENID_CONFIG_URL=http://dex:5556/dex/.well-known/openid-configuration \
|
||||
-e MINIO_IDENTITY_OPENID_REDIRECT_URI=http://127.0.0.1:9090/oauth_callback \
|
||||
-e MINIO_ROOT_USER=minio \
|
||||
-e MINIO_ROOT_PASSWORD=minio123 $(MINIO_VERSION) server /data{1...4} --address :9000 --console-address :9001)
|
||||
@echo "run mc commands to set the policy"
|
||||
@(docker run --name minio-client --network my-net -dit --entrypoint=/bin/sh minio/mc)
|
||||
@(docker exec minio-client mc alias set myminio/ http://minio:9000 minio minio123)
|
||||
@echo "adding policy to Dillon Harper to be able to login:"
|
||||
@(cd sso-integration && docker cp allaccess.json minio-client:/ && docker exec minio-client mc admin policy create myminio "Dillon Harper" allaccess.json)
|
||||
@echo "starting bash script"
|
||||
@(env bash $(PWD)/sso-integration/set-sso.sh)
|
||||
@echo "add python module"
|
||||
@(pip3 install bs4)
|
||||
@echo "Executing the test:"
|
||||
@(cd sso-integration && go test -coverpkg=../api -c -tags testrunmain . && mkdir -p coverage && ./sso-integration.test -test.v -test.run "^Test*" -test.coverprofile=coverage/sso-system.out)
|
||||
|
||||
test-permissions-1:
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4})
|
||||
@(env bash $(PWD)/web-app/tests/scripts/permissions.sh "web-app/tests/permissions-1/")
|
||||
@(docker stop minio)
|
||||
|
||||
test:
|
||||
@(GO111MODULE=on go test -race -v github.com/minio/console/restapi/...)
|
||||
test-permissions-2:
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4})
|
||||
@(env bash $(PWD)/web-app/tests/scripts/permissions.sh "web-app/tests/permissions-2/")
|
||||
@(docker stop minio)
|
||||
|
||||
test-permissions-3:
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4})
|
||||
@(env bash $(PWD)/web-app/tests/scripts/permissions.sh "web-app/tests/permissions-3/")
|
||||
@(docker stop minio)
|
||||
|
||||
test-permissions-4:
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4})
|
||||
@(env bash $(PWD)/web-app/tests/scripts/permissions.sh "web-app/tests/permissions-4/")
|
||||
@(docker stop minio)
|
||||
|
||||
test-permissions-5:
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4})
|
||||
@(env bash $(PWD)/web-app/tests/scripts/permissions.sh "web-app/tests/permissions-5/")
|
||||
@(docker stop minio)
|
||||
|
||||
test-permissions-6:
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4})
|
||||
@(env bash $(PWD)/web-app/tests/scripts/permissions.sh "web-app/tests/permissions-6/")
|
||||
@(docker stop minio)
|
||||
|
||||
test-permissions-7:
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4})
|
||||
@(env bash $(PWD)/web-app/tests/scripts/permissions.sh "web-app/tests/permissions-7/")
|
||||
@(docker stop minio)
|
||||
|
||||
test-apply-permissions:
|
||||
@(env bash $(PWD)/web-app/tests/scripts/initialize-env.sh)
|
||||
|
||||
test-start-docker-minio:
|
||||
@(docker run -v /data1 -v /data2 -v /data3 -v /data4 -d --name minio --rm -p 9000:9000 quay.io/minio/minio:latest server /data{1...4})
|
||||
|
||||
initialize-permissions: test-start-docker-minio test-apply-permissions
|
||||
@echo "Done initializing permissions test"
|
||||
|
||||
cleanup-permissions:
|
||||
@(env bash $(PWD)/web-app/tests/scripts/cleanup-env.sh)
|
||||
@(docker stop minio)
|
||||
|
||||
initialize-docker-network:
|
||||
@(docker network create test-network)
|
||||
|
||||
test-start-docker-minio-w-redirect-url: initialize-docker-network
|
||||
@(docker run \
|
||||
-e MINIO_BROWSER_REDIRECT_URL='http://localhost:8000/console/subpath/' \
|
||||
-e MINIO_SERVER_URL='http://localhost:9000' \
|
||||
-v /data1 -v /data2 -v /data3 -v /data4 \
|
||||
-d --network host --name minio --rm\
|
||||
quay.io/minio/minio:latest server /data{1...4})
|
||||
|
||||
test-start-docker-nginx-w-subpath:
|
||||
@(docker run \
|
||||
--network host \
|
||||
-d --rm \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
-v ./web-app/tests/subpath-nginx/nginx.conf:/etc/nginx/nginx.conf \
|
||||
--name test-nginx nginx)
|
||||
|
||||
test-initialize-minio-nginx: test-start-docker-minio-w-redirect-url test-start-docker-nginx-w-subpath
|
||||
|
||||
cleanup-minio-nginx:
|
||||
@(docker stop minio test-nginx & docker network rm test-network)
|
||||
|
||||
# https://stackoverflow.com/questions/19200235/golang-tests-in-sub-directory
|
||||
# Note: go test ./... will run tests on the current folder and all subfolders.
|
||||
# This is needed because tests can be in the folder or sub-folder(s), let's include them all please!.
|
||||
test:
|
||||
@echo "execute test and get coverage"
|
||||
@(cd api && mkdir -p coverage && GO111MODULE=on go test ./... -test.v -coverprofile=coverage/coverage.out)
|
||||
|
||||
|
||||
# https://stackoverflow.com/questions/19200235/golang-tests-in-sub-directory
|
||||
# Note: go test ./... will run tests on the current folder and all subfolders.
|
||||
# This is since tests in pkg folder are in subfolders and were not executed.
|
||||
test-pkg:
|
||||
@(GO111MODULE=on go test -race -v github.com/minio/console/pkg/...)
|
||||
@echo "execute test and get coverage"
|
||||
@(cd pkg && mkdir -p coverage && GO111MODULE=on go test ./... -test.v -coverprofile=coverage/coverage-pkg.out)
|
||||
|
||||
coverage:
|
||||
@(GO111MODULE=on go test -v -coverprofile=coverage.out github.com/minio/console/restapi/... && go tool cover -html=coverage.out && open coverage.html)
|
||||
@(GO111MODULE=on go test -v -coverprofile=coverage.out github.com/minio/console/api/... && go tool cover -html=coverage.out && open coverage.html)
|
||||
|
||||
clean:
|
||||
@echo "Cleaning up all the generated files"
|
||||
@@ -84,4 +283,10 @@ clean:
|
||||
@rm -vf console
|
||||
|
||||
docker:
|
||||
@docker buildx build --output=type=docker --platform linux/amd64 -t $(TAG) --build-arg build_version=$(BUILD_VERSION) --build-arg build_time='$(BUILD_TIME)' .
|
||||
@docker buildx build --output=type=docker --platform linux/amd64 -t $(TAG) --build-arg build_version=$(BUILD_VERSION) --build-arg build_time='$(BUILD_TIME)' --build-arg NODE_VERSION='$(NODE_VERSION)' .
|
||||
|
||||
release: swagger-gen
|
||||
@echo "Generating Release: $(RELEASE)"
|
||||
@make assets
|
||||
@git add -u .
|
||||
@git add web-app/build/
|
||||
|
||||
2
NOTICE
2
NOTICE
@@ -1,5 +1,5 @@
|
||||
This file is part of MinIO Console Server
|
||||
Copyright (c) 2021 MinIO, Inc.
|
||||
Copyright (c) 2023 MinIO, Inc.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
|
||||
136
README.md
136
README.md
@@ -4,57 +4,38 @@
|
||||
|
||||
A graphical user interface for [MinIO](https://github.com/minio/minio)
|
||||
|
||||
| Dashboard | Creating a bucket |
|
||||
| ------------- | ------------- |
|
||||
|  |  |
|
||||
| Object Browser | Dashboard | Creating a bucket |
|
||||
|------------------------------------|-------------------------------|-------------------------------|
|
||||
|  |  |  |
|
||||
|
||||
<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
|
||||
**Table of Contents**
|
||||
|
||||
- [MinIO Console](#minio-console)
|
||||
- [Install](#install)
|
||||
- [Binary Releases](#binary-releases)
|
||||
- [Docker](#docker)
|
||||
- [Build from source](#build-from-source)
|
||||
- [Setup](#setup)
|
||||
- [1. Create a user `console` using `mc`](#1-create-a-user-console-using-mc)
|
||||
- [2. Create a policy for `console` with admin access to all resources (for testing)](#2-create-a-policy-for-console-with-admin-access-to-all-resources-for-testing)
|
||||
- [3. Set the policy for the new `console` user](#3-set-the-policy-for-the-new-console-user)
|
||||
- [Start Console service:](#start-console-service)
|
||||
- [Start Console service with TLS:](#start-console-service-with-tls)
|
||||
- [Connect Console to a Minio using TLS and a self-signed certificate](#connect-console-to-a-minio-using-tls-and-a-self-signed-certificate)
|
||||
- [Install](#install)
|
||||
- [Build from source](#build-from-source)
|
||||
- [Setup](#setup)
|
||||
- [1. Create a user `console` using `mc`](#1-create-a-user-console-using-mc)
|
||||
- [2. Create a policy for `console` with admin access to all resources (for testing)](#2-create-a-policy-for-console-with-admin-access-to-all-resources-for-testing)
|
||||
- [3. Set the policy for the new `console` user](#3-set-the-policy-for-the-new-console-user)
|
||||
- [Start Console service:](#start-console-service)
|
||||
- [Start Console service with TLS:](#start-console-service-with-tls)
|
||||
- [Connect Console to a Minio using TLS and a self-signed certificate](#connect-console-to-a-minio-using-tls-and-a-self-signed-certificate)
|
||||
- [Contribute to console Project](#contribute-to-console-project)
|
||||
|
||||
<!-- markdown-toc end -->
|
||||
|
||||
## Install
|
||||
|
||||
### Binary Releases
|
||||
MinIO Console is a library that provides a management and browser UI overlay for the MinIO Server.
|
||||
The standalone binary installation path has been removed.
|
||||
|
||||
| OS | ARCH | Binary |
|
||||
|:-------:|:-------:|:----------------------------------------------------------------------------------------------------:|
|
||||
| Linux | amd64 | [linux-amd64](https://github.com/minio/console/releases/latest/download/console-linux-amd64) |
|
||||
| Linux | arm64 | [linux-arm64](https://github.com/minio/console/releases/latest/download/console-linux-arm64) |
|
||||
| Linux | ppc64le | [linux-ppc64le](https://github.com/minio/console/releases/latest/download/console-linux-ppc64le) |
|
||||
| Linux | s390x | [linux-s390x](https://github.com/minio/console/releases/latest/download/console-linux-s390x) |
|
||||
| Apple | amd64 | [darwin-amd64](https://github.com/minio/console/releases/latest/download/console-darwin-amd64) |
|
||||
| Windows | amd64 | [windows-amd64](https://github.com/minio/console/releases/latest/download/console-windows-amd64.exe) |
|
||||
|
||||
You can also verify the binary with [minisign](https://jedisct1.github.io/minisign/) by downloading the corresponding [`.minisig`](https://github.com/minio/console/releases/latest) signature file. Then run:
|
||||
```
|
||||
minisign -Vm console-<OS>-<ARCH> -P RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
Pull the latest release via:
|
||||
```
|
||||
docker pull minio/console
|
||||
```
|
||||
In case a Console standalone binary is needed, it can be generated by building this package from source as follows:
|
||||
|
||||
### Build from source
|
||||
|
||||
> You will need a working Go environment. Therefore, please follow [How to install Go](https://golang.org/doc/install).
|
||||
> Minimum version required is go1.17
|
||||
> Minimum version required is go1.22
|
||||
|
||||
```
|
||||
go install github.com/minio/console/cmd/console@latest
|
||||
@@ -103,60 +84,64 @@ EOF
|
||||
```
|
||||
|
||||
```sh
|
||||
mc admin policy add myminio/ consoleAdmin admin.json
|
||||
mc admin policy create myminio/ consoleAdmin admin.json
|
||||
```
|
||||
|
||||
### 3. Set the policy for the new `console` user
|
||||
|
||||
```sh
|
||||
mc admin policy set myminio consoleAdmin user=console
|
||||
mc admin policy attach myminio consoleAdmin --user=console
|
||||
```
|
||||
|
||||
> NOTE: Additionally, you can create policies to limit the privileges for other `console` users, for example, if you want the user to only have access to dashboard, buckets, notifications and watch page, the policy should look like this:
|
||||
> NOTE: Additionally, you can create policies to limit the privileges for other `console` users, for example, if you
|
||||
> want the user to only have access to dashboard, buckets, notifications and watch page, the policy should look like
|
||||
> this:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Action": [
|
||||
"admin:ServerInfo"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Sid": ""
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:ListenBucketNotification",
|
||||
"s3:PutBucketNotification",
|
||||
"s3:GetBucketNotification",
|
||||
"s3:ListMultipartUploadParts",
|
||||
"s3:ListBucketMultipartUploads",
|
||||
"s3:ListBucket",
|
||||
"s3:HeadBucket",
|
||||
"s3:GetObject",
|
||||
"s3:GetBucketLocation",
|
||||
"s3:AbortMultipartUpload",
|
||||
"s3:CreateBucket",
|
||||
"s3:PutObject",
|
||||
"s3:DeleteObject",
|
||||
"s3:DeleteBucket",
|
||||
"s3:PutBucketPolicy",
|
||||
"s3:DeleteBucketPolicy",
|
||||
"s3:GetBucketPolicy"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
],
|
||||
"Sid": ""
|
||||
}
|
||||
]
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"admin:ServerInfo"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Sid": ""
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:ListenBucketNotification",
|
||||
"s3:PutBucketNotification",
|
||||
"s3:GetBucketNotification",
|
||||
"s3:ListMultipartUploadParts",
|
||||
"s3:ListBucketMultipartUploads",
|
||||
"s3:ListBucket",
|
||||
"s3:HeadBucket",
|
||||
"s3:GetObject",
|
||||
"s3:GetBucketLocation",
|
||||
"s3:AbortMultipartUpload",
|
||||
"s3:CreateBucket",
|
||||
"s3:PutObject",
|
||||
"s3:DeleteObject",
|
||||
"s3:DeleteBucket",
|
||||
"s3:PutBucketPolicy",
|
||||
"s3:DeleteBucketPolicy",
|
||||
"s3:GetBucketPolicy"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
],
|
||||
"Sid": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Start Console service:
|
||||
|
||||
Before running console service, following environment settings must be supplied
|
||||
|
||||
```sh
|
||||
# Salt to encrypt JWT payload
|
||||
export CONSOLE_PBKDF_PASSPHRASE=SECRET
|
||||
@@ -169,6 +154,7 @@ export CONSOLE_MINIO_SERVER=http://localhost:9000
|
||||
```
|
||||
|
||||
Now start the console service.
|
||||
|
||||
```
|
||||
./console server
|
||||
2021-01-19 02:36:08.893735 I | 2021/01/19 02:36:08 server.go:129: Serving console at http://localhost:9090
|
||||
@@ -189,6 +175,7 @@ Copy your `public.crt` and `private.key` to `~/.console/certs`, then:
|
||||
For advanced users, `console` has support for multiple certificates to service clients through multiple domains.
|
||||
|
||||
Following tree structure is expected for supporting multiple domains:
|
||||
|
||||
```sh
|
||||
certs/
|
||||
│
|
||||
@@ -219,4 +206,5 @@ export CONSOLE_MINIO_SERVER=https://localhost:9000
|
||||
You can verify that the apis work by doing the request on `localhost:9090/api/v1/...`
|
||||
|
||||
# Contribute to console Project
|
||||
|
||||
Please follow console [Contributor's Guide](https://github.com/minio/console/blob/master/CONTRIBUTING.md)
|
||||
|
||||
@@ -18,9 +18,10 @@ you need access credentials for a successful exploit).
|
||||
|
||||
If you have not received a reply to your email within 48 hours or you have not heard from the security team
|
||||
for the past five days please contact the security team directly:
|
||||
- Primary security coordinator: lenin@min.io
|
||||
- Secondary coordinator: security@min.io
|
||||
- If you receive no response: dev@min.io
|
||||
|
||||
- Primary security coordinator: daniel@min.io
|
||||
- Secondary coordinator: security@min.io
|
||||
- If you receive no response: dev@min.io
|
||||
|
||||
### Disclosure Process
|
||||
|
||||
|
||||
70
api/admin_arns.go
Normal file
70
api/admin_arns.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
systemApi "github.com/minio/console/api/operations/system"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
"github.com/minio/console/models"
|
||||
)
|
||||
|
||||
func registerAdminArnsHandlers(api *operations.ConsoleAPI) {
|
||||
// return a list of arns
|
||||
api.SystemArnListHandler = systemApi.ArnListHandlerFunc(func(params systemApi.ArnListParams, session *models.Principal) middleware.Responder {
|
||||
arnsResp, err := getArnsResponse(session, params)
|
||||
if err != nil {
|
||||
return systemApi.NewArnListDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return systemApi.NewArnListOK().WithPayload(arnsResp)
|
||||
})
|
||||
}
|
||||
|
||||
// getArns invokes admin info and returns a list of arns
|
||||
func getArns(ctx context.Context, client MinioAdmin) (*models.ArnsResponse, error) {
|
||||
serverInfo, err := client.serverInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// build response
|
||||
return &models.ArnsResponse{
|
||||
Arns: serverInfo.SQSARN,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getArnsResponse returns a list of active arns in the instance
|
||||
func getArnsResponse(session *models.Principal, params systemApi.ArnListParams) (*models.ArnsResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
// serialize output
|
||||
arnsList, err := getArns(ctx, adminClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return arnsList, nil
|
||||
}
|
||||
97
api/admin_arns_test.go
Normal file
97
api/admin_arns_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations/system"
|
||||
"github.com/minio/console/models"
|
||||
|
||||
"github.com/go-openapi/loads"
|
||||
"github.com/minio/console/api/operations"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
|
||||
asrt "github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestArnsList(t *testing.T) {
|
||||
assert := asrt.New(t)
|
||||
adminClient := AdminClientMock{}
|
||||
// Test-1 : getArns() returns proper arn list
|
||||
MinioServerInfoMock = func(_ context.Context) (madmin.InfoMessage, error) {
|
||||
return madmin.InfoMessage{
|
||||
SQSARN: []string{"uno"},
|
||||
}, nil
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
arnsList, err := getArns(ctx, adminClient)
|
||||
assert.NotNil(arnsList, "arn list was returned nil")
|
||||
if arnsList != nil {
|
||||
assert.Equal(len(arnsList.Arns), 1, "Incorrect arns count")
|
||||
}
|
||||
assert.Nil(err, "Error should have been nil")
|
||||
|
||||
// Test-2 : getArns(ctx) fails for whatever reason
|
||||
MinioServerInfoMock = func(_ context.Context) (madmin.InfoMessage, error) {
|
||||
return madmin.InfoMessage{}, errors.New("some reason")
|
||||
}
|
||||
|
||||
arnsList, err = getArns(ctx, adminClient)
|
||||
assert.Nil(arnsList, "arn list was not returned nil")
|
||||
assert.NotNil(err, "An error should have been returned")
|
||||
}
|
||||
|
||||
func TestRegisterAdminArnsHandlers(t *testing.T) {
|
||||
assert := asrt.New(t)
|
||||
swaggerSpec, err := loads.Embedded(SwaggerJSON, FlatSwaggerJSON)
|
||||
if err != nil {
|
||||
assert.Fail("Error")
|
||||
}
|
||||
api := operations.NewConsoleAPI(swaggerSpec)
|
||||
api.SystemArnListHandler = nil
|
||||
registerAdminArnsHandlers(api)
|
||||
if api.SystemArnListHandler == nil {
|
||||
assert.Fail("Assignment should happen")
|
||||
} else {
|
||||
fmt.Println("Function got assigned: ", api.SystemArnListHandler)
|
||||
}
|
||||
|
||||
// To test error case in registerAdminArnsHandlers
|
||||
request, _ := http.NewRequest(
|
||||
"GET",
|
||||
"http://localhost:9090/api/v1/buckets/",
|
||||
nil,
|
||||
)
|
||||
ArnListParamsStruct := system.ArnListParams{
|
||||
HTTPRequest: request,
|
||||
}
|
||||
modelsPrincipal := models.Principal{
|
||||
STSAccessKeyID: "accesskey",
|
||||
}
|
||||
var value middleware.Responder = api.SystemArnListHandler.Handle(ArnListParamsStruct, &modelsPrincipal)
|
||||
str := fmt.Sprintf("%#v", value)
|
||||
fmt.Println("value: ", str)
|
||||
assert.Equal(strings.Contains(str, "_statusCode:500"), true)
|
||||
}
|
||||
355
api/admin_client_mock.go
Normal file
355
api/admin_client_mock.go
Normal file
@@ -0,0 +1,355 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
iampolicy "github.com/minio/pkg/v3/policy"
|
||||
)
|
||||
|
||||
type AdminClientMock struct{}
|
||||
|
||||
var (
|
||||
MinioServerInfoMock func(ctx context.Context) (madmin.InfoMessage, error)
|
||||
minioChangePasswordMock func(ctx context.Context, accessKey, secretKey string) error
|
||||
|
||||
minioHelpConfigKVMock func(subSys, key string, envOnly bool) (madmin.Help, error)
|
||||
minioGetConfigKVMock func(key string) ([]byte, error)
|
||||
minioSetConfigKVMock func(kv string) (restart bool, err error)
|
||||
minioDelConfigKVMock func(name string) (err error)
|
||||
minioHelpConfigKVGlobalMock func(envOnly bool) (madmin.Help, error)
|
||||
|
||||
minioGetLogsMock func(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo
|
||||
|
||||
minioListGroupsMock func() ([]string, error)
|
||||
minioUpdateGroupMembersMock func(madmin.GroupAddRemove) error
|
||||
minioGetGroupDescriptionMock func(group string) (*madmin.GroupDesc, error)
|
||||
minioSetGroupStatusMock func(group string, status madmin.GroupStatus) error
|
||||
|
||||
minioHealMock func(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
|
||||
forceStart, forceStop bool) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error)
|
||||
|
||||
minioServerHealthInfoMock func(ctx context.Context, deadline time.Duration) (interface{}, string, error)
|
||||
|
||||
minioListPoliciesMock func() (map[string]*iampolicy.Policy, error)
|
||||
minioGetPolicyMock func(name string) (*iampolicy.Policy, error)
|
||||
minioRemovePolicyMock func(name string) error
|
||||
minioAddPolicyMock func(name string, policy *iampolicy.Policy) error
|
||||
minioSetPolicyMock func(policyName, entityName string, isGroup bool) error
|
||||
|
||||
minioStartProfiling func(profiler madmin.ProfilerType) ([]madmin.StartProfilingResult, error)
|
||||
minioStopProfiling func() (io.ReadCloser, error)
|
||||
|
||||
minioServiceRestartMock func(ctx context.Context) error
|
||||
|
||||
getSiteReplicationInfo func(ctx context.Context) (*madmin.SiteReplicationInfo, error)
|
||||
addSiteReplicationInfo func(ctx context.Context, sites []madmin.PeerSite) (*madmin.ReplicateAddStatus, error)
|
||||
editSiteReplicationInfo func(ctx context.Context, site madmin.PeerInfo) (*madmin.ReplicateEditStatus, error)
|
||||
deleteSiteReplicationInfoMock func(ctx context.Context, removeReq madmin.SRRemoveReq) (*madmin.ReplicateRemoveStatus, error)
|
||||
getSiteReplicationStatus func(ctx context.Context, params madmin.SRStatusOptions) (*madmin.SRStatusInfo, error)
|
||||
|
||||
minioListTiersMock func(ctx context.Context) ([]*madmin.TierConfig, error)
|
||||
minioTierStatsMock func(ctx context.Context) ([]madmin.TierInfo, error)
|
||||
minioAddTiersMock func(ctx context.Context, tier *madmin.TierConfig) error
|
||||
minioRemoveTierMock func(ctx context.Context, tierName string) error
|
||||
minioEditTiersMock func(ctx context.Context, tierName string, creds madmin.TierCreds) error
|
||||
minioVerifyTierStatusMock func(ctx context.Context, tierName string) error
|
||||
|
||||
minioServiceTraceMock func(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo
|
||||
|
||||
minioListUsersMock func() (map[string]madmin.UserInfo, error)
|
||||
minioAddUserMock func(accessKey, secreyKey string) error
|
||||
minioRemoveUserMock func(accessKey string) error
|
||||
minioGetUserInfoMock func(accessKey string) (madmin.UserInfo, error)
|
||||
minioSetUserStatusMock func(accessKey string, status madmin.AccountStatus) error
|
||||
|
||||
minioAccountInfoMock func(ctx context.Context) (madmin.AccountInfo, error)
|
||||
minioAddServiceAccountMock func(ctx context.Context, policy string, user string, accessKey string, secretKey string, description string, name string, expiry *time.Time, status string) (madmin.Credentials, error)
|
||||
minioListServiceAccountsMock func(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error)
|
||||
minioDeleteServiceAccountMock func(ctx context.Context, serviceAccount string) error
|
||||
minioInfoServiceAccountMock func(ctx context.Context, serviceAccount string) (madmin.InfoServiceAccountResp, error)
|
||||
minioUpdateServiceAccountMock func(ctx context.Context, serviceAccount string, opts madmin.UpdateServiceAccountReq) error
|
||||
minioGetLDAPPolicyEntitiesMock func(ctx context.Context, query madmin.PolicyEntitiesQuery) (madmin.PolicyEntitiesResult, error)
|
||||
|
||||
minioListRemoteBucketsMock func(ctx context.Context, bucket, arnType string) (targets []madmin.BucketTarget, err error)
|
||||
minioGetRemoteBucketMock func(ctx context.Context, bucket, arnType string) (targets *madmin.BucketTarget, err error)
|
||||
minioAddRemoteBucketMock func(ctx context.Context, bucket string, target *madmin.BucketTarget) (string, error)
|
||||
)
|
||||
|
||||
func (ac AdminClientMock) serverInfo(ctx context.Context) (madmin.InfoMessage, error) {
|
||||
return MinioServerInfoMock(ctx)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) listRemoteBuckets(ctx context.Context, bucket, arnType string) (targets []madmin.BucketTarget, err error) {
|
||||
return minioListRemoteBucketsMock(ctx, bucket, arnType)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) getRemoteBucket(ctx context.Context, bucket, arnType string) (targets *madmin.BucketTarget, err error) {
|
||||
return minioGetRemoteBucketMock(ctx, bucket, arnType)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) removeRemoteBucket(_ context.Context, _, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) addRemoteBucket(ctx context.Context, bucket string, target *madmin.BucketTarget) (string, error) {
|
||||
return minioAddRemoteBucketMock(ctx, bucket, target)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) changePassword(ctx context.Context, accessKey, secretKey string) error {
|
||||
return minioChangePasswordMock(ctx, accessKey, secretKey)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) speedtest(_ context.Context, _ madmin.SpeedtestOpts) (chan madmin.SpeedTestResult, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) verifyTierStatus(ctx context.Context, tier string) error {
|
||||
return minioVerifyTierStatusMock(ctx, tier)
|
||||
}
|
||||
|
||||
// mock function helpConfigKV()
|
||||
func (ac AdminClientMock) helpConfigKV(_ context.Context, subSys, key string, envOnly bool) (madmin.Help, error) {
|
||||
return minioHelpConfigKVMock(subSys, key, envOnly)
|
||||
}
|
||||
|
||||
// mock function getConfigKV()
|
||||
func (ac AdminClientMock) getConfigKV(_ context.Context, name string) ([]byte, error) {
|
||||
return minioGetConfigKVMock(name)
|
||||
}
|
||||
|
||||
// mock function setConfigKV()
|
||||
func (ac AdminClientMock) setConfigKV(_ context.Context, kv string) (restart bool, err error) {
|
||||
return minioSetConfigKVMock(kv)
|
||||
}
|
||||
|
||||
// mock function helpConfigKV()
|
||||
func (ac AdminClientMock) helpConfigKVGlobal(_ context.Context, envOnly bool) (madmin.Help, error) {
|
||||
return minioHelpConfigKVGlobalMock(envOnly)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) delConfigKV(_ context.Context, name string) (err error) {
|
||||
return minioDelConfigKVMock(name)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
|
||||
return minioGetLogsMock(ctx, node, lineCnt, logKind)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) listGroups(_ context.Context) ([]string, error) {
|
||||
return minioListGroupsMock()
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) updateGroupMembers(_ context.Context, req madmin.GroupAddRemove) error {
|
||||
return minioUpdateGroupMembersMock(req)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) getGroupDescription(_ context.Context, group string) (*madmin.GroupDesc, error) {
|
||||
return minioGetGroupDescriptionMock(group)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) setGroupStatus(_ context.Context, group string, status madmin.GroupStatus) error {
|
||||
return minioSetGroupStatusMock(group, status)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) heal(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
|
||||
forceStart, forceStop bool,
|
||||
) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error) {
|
||||
return minioHealMock(ctx, bucket, prefix, healOpts, clientToken, forceStart, forceStop)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) serverHealthInfo(ctx context.Context, deadline time.Duration) (interface{}, string, error) {
|
||||
return minioServerHealthInfoMock(ctx, deadline)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) addOrUpdateIDPConfig(_ context.Context, _, _, _ string, _ bool) (restart bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) listIDPConfig(_ context.Context, _ string) ([]madmin.IDPListItem, error) {
|
||||
return []madmin.IDPListItem{{Name: "mock"}}, nil
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) deleteIDPConfig(_ context.Context, _, _ string) (restart bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) getIDPConfig(_ context.Context, _, _ string) (c madmin.IDPConfig, err error) {
|
||||
return madmin.IDPConfig{Info: []madmin.IDPCfgInfo{{Key: "mock", Value: "mock"}}}, nil
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) kmsStatus(_ context.Context) (madmin.KMSStatus, error) {
|
||||
return madmin.KMSStatus{Name: "name", DefaultKeyID: "key", Endpoints: map[string]madmin.ItemState{"localhost": madmin.ItemState("online")}}, nil
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) kmsAPIs(_ context.Context) ([]madmin.KMSAPI, error) {
|
||||
return []madmin.KMSAPI{{Method: "GET", Path: "/mock"}}, nil
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) kmsMetrics(_ context.Context) (*madmin.KMSMetrics, error) {
|
||||
return &madmin.KMSMetrics{}, nil
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) kmsVersion(_ context.Context) (*madmin.KMSVersion, error) {
|
||||
return &madmin.KMSVersion{Version: "test-version"}, nil
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) createKey(_ context.Context, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) listKeys(_ context.Context, _ string) ([]madmin.KMSKeyInfo, error) {
|
||||
return []madmin.KMSKeyInfo{{
|
||||
Name: "name",
|
||||
CreatedBy: "by",
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) keyStatus(_ context.Context, _ string) (*madmin.KMSKeyStatus, error) {
|
||||
return &madmin.KMSKeyStatus{KeyID: "key"}, nil
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) listPolicies(_ context.Context) (map[string]*iampolicy.Policy, error) {
|
||||
return minioListPoliciesMock()
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) getPolicy(_ context.Context, name string) (*iampolicy.Policy, error) {
|
||||
return minioGetPolicyMock(name)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) removePolicy(_ context.Context, name string) error {
|
||||
return minioRemovePolicyMock(name)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) addPolicy(_ context.Context, name string, policy *iampolicy.Policy) error {
|
||||
return minioAddPolicyMock(name, policy)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) setPolicy(_ context.Context, policyName, entityName string, isGroup bool) error {
|
||||
return minioSetPolicyMock(policyName, entityName, isGroup)
|
||||
}
|
||||
|
||||
// mock function for startProfiling()
|
||||
func (ac AdminClientMock) startProfiling(_ context.Context, profiler madmin.ProfilerType) ([]madmin.StartProfilingResult, error) {
|
||||
return minioStartProfiling(profiler)
|
||||
}
|
||||
|
||||
// mock function for stopProfiling()
|
||||
func (ac AdminClientMock) stopProfiling(_ context.Context) (io.ReadCloser, error) {
|
||||
return minioStopProfiling()
|
||||
}
|
||||
|
||||
// mock function of serviceRestart()
|
||||
func (ac AdminClientMock) serviceRestart(ctx context.Context) error {
|
||||
return minioServiceRestartMock(ctx)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) getSiteReplicationInfo(ctx context.Context) (*madmin.SiteReplicationInfo, error) {
|
||||
return getSiteReplicationInfo(ctx)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) addSiteReplicationInfo(ctx context.Context, sites []madmin.PeerSite, _ madmin.SRAddOptions) (*madmin.ReplicateAddStatus, error) {
|
||||
return addSiteReplicationInfo(ctx, sites)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) editSiteReplicationInfo(ctx context.Context, site madmin.PeerInfo, _ madmin.SREditOptions) (*madmin.ReplicateEditStatus, error) {
|
||||
return editSiteReplicationInfo(ctx, site)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) deleteSiteReplicationInfo(ctx context.Context, removeReq madmin.SRRemoveReq) (*madmin.ReplicateRemoveStatus, error) {
|
||||
return deleteSiteReplicationInfoMock(ctx, removeReq)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) getSiteReplicationStatus(ctx context.Context, params madmin.SRStatusOptions) (*madmin.SRStatusInfo, error) {
|
||||
return getSiteReplicationStatus(ctx, params)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) listTiers(ctx context.Context) ([]*madmin.TierConfig, error) {
|
||||
return minioListTiersMock(ctx)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) tierStats(ctx context.Context) ([]madmin.TierInfo, error) {
|
||||
return minioTierStatsMock(ctx)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) addTier(ctx context.Context, tier *madmin.TierConfig) error {
|
||||
return minioAddTiersMock(ctx, tier)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) removeTier(ctx context.Context, tierName string) error {
|
||||
return minioRemoveTierMock(ctx, tierName)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) editTierCreds(ctx context.Context, tierName string, creds madmin.TierCreds) error {
|
||||
return minioEditTiersMock(ctx, tierName, creds)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) serviceTrace(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo {
|
||||
return minioServiceTraceMock(ctx, threshold, s3, internal, storage, os, errTrace)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) listUsers(_ context.Context) (map[string]madmin.UserInfo, error) {
|
||||
return minioListUsersMock()
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) addUser(_ context.Context, accessKey, secretKey string) error {
|
||||
return minioAddUserMock(accessKey, secretKey)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) removeUser(_ context.Context, accessKey string) error {
|
||||
return minioRemoveUserMock(accessKey)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) getUserInfo(_ context.Context, accessKey string) (madmin.UserInfo, error) {
|
||||
return minioGetUserInfoMock(accessKey)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) setUserStatus(_ context.Context, accessKey string, status madmin.AccountStatus) error {
|
||||
return minioSetUserStatusMock(accessKey, status)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) AccountInfo(ctx context.Context) (madmin.AccountInfo, error) {
|
||||
return minioAccountInfoMock(ctx)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) addServiceAccount(ctx context.Context, policy string, user string, accessKey string, secretKey string, description string, name string, expiry *time.Time, status string) (madmin.Credentials, error) {
|
||||
return minioAddServiceAccountMock(ctx, policy, user, accessKey, secretKey, description, name, expiry, status)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) listServiceAccounts(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error) {
|
||||
return minioListServiceAccountsMock(ctx, user)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) deleteServiceAccount(ctx context.Context, serviceAccount string) error {
|
||||
return minioDeleteServiceAccountMock(ctx, serviceAccount)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) infoServiceAccount(ctx context.Context, serviceAccount string) (madmin.InfoServiceAccountResp, error) {
|
||||
return minioInfoServiceAccountMock(ctx, serviceAccount)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) updateServiceAccount(ctx context.Context, serviceAccount string, opts madmin.UpdateServiceAccountReq) error {
|
||||
return minioUpdateServiceAccountMock(ctx, serviceAccount, opts)
|
||||
}
|
||||
|
||||
func (ac AdminClientMock) getLDAPPolicyEntities(ctx context.Context, query madmin.PolicyEntitiesQuery) (madmin.PolicyEntitiesResult, error) {
|
||||
return minioGetLDAPPolicyEntitiesMock(ctx, query)
|
||||
}
|
||||
316
api/admin_config.go
Normal file
316
api/admin_config.go
Normal file
@@ -0,0 +1,316 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/minio/console/api/operations"
|
||||
"github.com/minio/console/models"
|
||||
madmin "github.com/minio/madmin-go/v3"
|
||||
|
||||
cfgApi "github.com/minio/console/api/operations/configuration"
|
||||
)
|
||||
|
||||
func registerConfigHandlers(api *operations.ConsoleAPI) {
|
||||
// List Configurations
|
||||
api.ConfigurationListConfigHandler = cfgApi.ListConfigHandlerFunc(func(params cfgApi.ListConfigParams, session *models.Principal) middleware.Responder {
|
||||
configListResp, err := getListConfigResponse(session, params)
|
||||
if err != nil {
|
||||
return cfgApi.NewListConfigDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return cfgApi.NewListConfigOK().WithPayload(configListResp)
|
||||
})
|
||||
// Configuration Info
|
||||
api.ConfigurationConfigInfoHandler = cfgApi.ConfigInfoHandlerFunc(func(params cfgApi.ConfigInfoParams, session *models.Principal) middleware.Responder {
|
||||
config, err := getConfigResponse(session, params)
|
||||
if err != nil {
|
||||
return cfgApi.NewConfigInfoDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return cfgApi.NewConfigInfoOK().WithPayload(config)
|
||||
})
|
||||
// Set Configuration
|
||||
api.ConfigurationSetConfigHandler = cfgApi.SetConfigHandlerFunc(func(params cfgApi.SetConfigParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := setConfigResponse(session, params)
|
||||
if err != nil {
|
||||
return cfgApi.NewSetConfigDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return cfgApi.NewSetConfigOK().WithPayload(resp)
|
||||
})
|
||||
// Reset Configuration
|
||||
api.ConfigurationResetConfigHandler = cfgApi.ResetConfigHandlerFunc(func(params cfgApi.ResetConfigParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := resetConfigResponse(session, params)
|
||||
if err != nil {
|
||||
return cfgApi.NewResetConfigDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return cfgApi.NewResetConfigOK().WithPayload(resp)
|
||||
})
|
||||
// Export Configuration as base64 string.
|
||||
api.ConfigurationExportConfigHandler = cfgApi.ExportConfigHandlerFunc(func(params cfgApi.ExportConfigParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := exportConfigResponse(session, params)
|
||||
if err != nil {
|
||||
return cfgApi.NewExportConfigDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return cfgApi.NewExportConfigOK().WithPayload(resp)
|
||||
})
|
||||
api.ConfigurationPostConfigsImportHandler = cfgApi.PostConfigsImportHandlerFunc(func(params cfgApi.PostConfigsImportParams, session *models.Principal) middleware.Responder {
|
||||
_, err := importConfigResponse(session, params)
|
||||
if err != nil {
|
||||
return cfgApi.NewPostConfigsImportDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return cfgApi.NewPostConfigsImportDefault(200)
|
||||
})
|
||||
}
|
||||
|
||||
// listConfig gets all configurations' names and their descriptions
|
||||
func listConfig(client MinioAdmin) ([]*models.ConfigDescription, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
configKeysHelp, err := client.helpConfigKV(ctx, "", "", false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var configDescs []*models.ConfigDescription
|
||||
for _, c := range configKeysHelp.KeysHelp {
|
||||
desc := &models.ConfigDescription{
|
||||
Key: c.Key,
|
||||
Description: c.Description,
|
||||
}
|
||||
configDescs = append(configDescs, desc)
|
||||
}
|
||||
return configDescs, nil
|
||||
}
|
||||
|
||||
// getListConfigResponse performs listConfig() and serializes it to the handler's output
|
||||
func getListConfigResponse(session *models.Principal, params cfgApi.ListConfigParams) (*models.ListConfigResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
configDescs, err := listConfig(adminClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
listGroupsResponse := &models.ListConfigResponse{
|
||||
Configurations: configDescs,
|
||||
Total: int64(len(configDescs)),
|
||||
}
|
||||
return listGroupsResponse, nil
|
||||
}
|
||||
|
||||
// getConfig gets the key values for a defined configuration.
|
||||
func getConfig(ctx context.Context, client MinioAdmin, name string) ([]*models.Configuration, error) {
|
||||
configBytes, err := client.getConfigKV(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subSysConfigs, err := madmin.ParseServerConfigOutput(string(configBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var configSubSysList []*models.Configuration
|
||||
for _, scfg := range subSysConfigs {
|
||||
if !madmin.SubSystems.Contains(scfg.SubSystem) {
|
||||
return nil, fmt.Errorf("no sub-systems found")
|
||||
}
|
||||
var confkv []*models.ConfigurationKV
|
||||
for _, kv := range scfg.KV {
|
||||
var envOverride *models.EnvOverride
|
||||
|
||||
if kv.EnvOverride != nil {
|
||||
envOverride = &models.EnvOverride{
|
||||
Name: kv.EnvOverride.Name,
|
||||
Value: kv.EnvOverride.Value,
|
||||
}
|
||||
}
|
||||
|
||||
confkv = append(confkv, &models.ConfigurationKV{Key: kv.Key, Value: kv.Value, EnvOverride: envOverride})
|
||||
}
|
||||
if len(confkv) == 0 {
|
||||
continue
|
||||
}
|
||||
var fullConfigName string
|
||||
if scfg.Target == "" {
|
||||
fullConfigName = scfg.SubSystem
|
||||
} else {
|
||||
fullConfigName = scfg.SubSystem + ":" + scfg.Target
|
||||
}
|
||||
configSubSysList = append(configSubSysList, &models.Configuration{KeyValues: confkv, Name: fullConfigName})
|
||||
}
|
||||
return configSubSysList, nil
|
||||
}
|
||||
|
||||
// getConfigResponse performs getConfig() and serializes it to the handler's output
|
||||
func getConfigResponse(session *models.Principal, params cfgApi.ConfigInfoParams) ([]*models.Configuration, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
configurations, err := getConfig(ctx, adminClient, params.Name)
|
||||
if err != nil {
|
||||
errorVal := ErrorWithContext(ctx, err)
|
||||
minioError := madmin.ToErrorResponse(err)
|
||||
if minioError.Code == "XMinioConfigError" {
|
||||
errorVal.Code = 404
|
||||
}
|
||||
return nil, errorVal
|
||||
}
|
||||
return configurations, nil
|
||||
}
|
||||
|
||||
// setConfig sets a configuration with the defined key values
|
||||
func setConfig(ctx context.Context, client MinioAdmin, configName *string, kvs []*models.ConfigurationKV) (restart bool, err error) {
|
||||
config := buildConfig(configName, kvs)
|
||||
restart, err = client.setConfigKV(ctx, *config)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return restart, nil
|
||||
}
|
||||
|
||||
func setConfigWithARNAccountID(ctx context.Context, client MinioAdmin, configName *string, kvs []*models.ConfigurationKV, arnAccountID string) (restart bool, err error) {
|
||||
// if arnAccountID is not empty the configuration will be treated as a notification target
|
||||
// arnAccountID will be used as an identifier for that specific target
|
||||
// docs: https://min.io/docs/minio/linux/administration/monitoring/bucket-notifications.html
|
||||
if arnAccountID != "" {
|
||||
configName = swag.String(fmt.Sprintf("%s:%s", *configName, arnAccountID))
|
||||
}
|
||||
return setConfig(ctx, client, configName, kvs)
|
||||
}
|
||||
|
||||
// buildConfig builds a concatenated string including name and keyvalues
|
||||
// e.g. `region name=us-west-1`
|
||||
func buildConfig(configName *string, kvs []*models.ConfigurationKV) *string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString(*configName)
|
||||
for _, kv := range kvs {
|
||||
key := strings.TrimSpace(kv.Key)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
builder.WriteString(" ")
|
||||
builder.WriteString(key)
|
||||
builder.WriteString("=")
|
||||
// All newlines must be converted to ','
|
||||
builder.WriteString(strings.ReplaceAll(strings.TrimSpace(fmt.Sprintf("\"%s\"", kv.Value)), "\n", ","))
|
||||
}
|
||||
config := builder.String()
|
||||
return &config
|
||||
}
|
||||
|
||||
// setConfigResponse implements setConfig() to be used by handler
|
||||
func setConfigResponse(session *models.Principal, params cfgApi.SetConfigParams) (*models.SetConfigResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
configName := params.Name
|
||||
|
||||
needsRestart, err := setConfigWithARNAccountID(ctx, adminClient, &configName, params.Body.KeyValues, params.Body.ArnResourceID)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.SetConfigResponse{Restart: needsRestart}, nil
|
||||
}
|
||||
|
||||
func resetConfig(ctx context.Context, client MinioAdmin, configName *string) (err error) {
|
||||
err = client.delConfigKV(ctx, *configName)
|
||||
return err
|
||||
}
|
||||
|
||||
// resetConfigResponse implements resetConfig() to be used by handler
|
||||
func resetConfigResponse(session *models.Principal, params cfgApi.ResetConfigParams) (*models.SetConfigResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
err = resetConfig(ctx, adminClient, ¶ms.Name)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
return &models.SetConfigResponse{Restart: true}, nil
|
||||
}
|
||||
|
||||
func exportConfigResponse(session *models.Principal, params cfgApi.ExportConfigParams) (*models.ConfigExportResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
configRes, err := mAdmin.GetConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// may contain sensitive information so unpack only when required.
|
||||
return &models.ConfigExportResponse{
|
||||
Status: "success",
|
||||
Value: base64.StdEncoding.EncodeToString(configRes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func importConfigResponse(session *models.Principal, params cfgApi.PostConfigsImportParams) (*cfgApi.PostConfigsImportDefault, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
file, _, err := params.HTTPRequest.FormFile("file")
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
err = mAdmin.SetConfig(ctx, file)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &cfgApi.PostConfigsImportDefault{}, nil
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package restapi
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -28,52 +28,21 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
NotifyPostgresSubSys = "notify_postgres"
|
||||
PostgresFormat = "format"
|
||||
PostgresConnectionString = "connection_string"
|
||||
PostgresTable = "table"
|
||||
PostgresHost = "host"
|
||||
PostgresPort = "port"
|
||||
PostgresUsername = "username"
|
||||
PostgresPassword = "password"
|
||||
PostgresDatabase = "database"
|
||||
PostgresQueueDir = "queue_dir"
|
||||
PostgresQueueLimit = "queue_limit"
|
||||
PostgresMaxOpenConnections = "max_open_connections"
|
||||
NotifyPostgresSubSys = "notify_postgres"
|
||||
PostgresFormat = "format"
|
||||
PostgresConnectionString = "connection_string"
|
||||
PostgresTable = "table"
|
||||
PostgresQueueDir = "queue_dir"
|
||||
PostgresQueueLimit = "queue_limit"
|
||||
)
|
||||
|
||||
// assigning mock at runtime instead of compile time
|
||||
var minioHelpConfigKVMock func(subSys, key string, envOnly bool) (madmin.Help, error)
|
||||
var minioGetConfigKVMock func(key string) ([]byte, error)
|
||||
var minioSetConfigKVMock func(kv string) (restart bool, err error)
|
||||
var minioDelConfigKVMock func(name string) (err error)
|
||||
|
||||
// mock function helpConfigKV()
|
||||
func (ac adminClientMock) helpConfigKV(ctx context.Context, subSys, key string, envOnly bool) (madmin.Help, error) {
|
||||
return minioHelpConfigKVMock(subSys, key, envOnly)
|
||||
}
|
||||
|
||||
// mock function getConfigKV()
|
||||
func (ac adminClientMock) getConfigKV(ctx context.Context, name string) ([]byte, error) {
|
||||
return minioGetConfigKVMock(name)
|
||||
}
|
||||
|
||||
// mock function setConfigKV()
|
||||
func (ac adminClientMock) setConfigKV(ctx context.Context, kv string) (restart bool, err error) {
|
||||
return minioSetConfigKVMock(kv)
|
||||
}
|
||||
|
||||
func (ac adminClientMock) delConfigKV(ctx context.Context, name string) (err error) {
|
||||
return minioDelConfigKVMock(name)
|
||||
}
|
||||
|
||||
func TestListConfig(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
adminClient := AdminClientMock{}
|
||||
function := "listConfig()"
|
||||
// Test-1 : listConfig() get list of two configurations and ensure is output correctly
|
||||
configListMock := []madmin.HelpKV{
|
||||
@@ -94,7 +63,7 @@ func TestListConfig(t *testing.T) {
|
||||
}
|
||||
expectedKeysDesc := mockConfigList.KeysHelp
|
||||
// mock function response from listConfig()
|
||||
minioHelpConfigKVMock = func(subSys, key string, envOnly bool) (madmin.Help, error) {
|
||||
minioHelpConfigKVMock = func(_, _ string, _ bool) (madmin.Help, error) {
|
||||
return mockConfigList, nil
|
||||
}
|
||||
configList, err := listConfig(adminClient)
|
||||
@@ -111,7 +80,7 @@ func TestListConfig(t *testing.T) {
|
||||
|
||||
// Test-2 : listConfig() Return error and see that the error is handled correctly and returned
|
||||
// mock function response from listConfig()
|
||||
minioHelpConfigKVMock = func(subSys, key string, envOnly bool) (madmin.Help, error) {
|
||||
minioHelpConfigKVMock = func(_, _ string, _ bool) (madmin.Help, error) {
|
||||
return madmin.Help{}, errors.New("error")
|
||||
}
|
||||
_, err = listConfig(adminClient)
|
||||
@@ -122,10 +91,10 @@ func TestListConfig(t *testing.T) {
|
||||
|
||||
func TestSetConfig(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
adminClient := AdminClientMock{}
|
||||
function := "setConfig()"
|
||||
// mock function response from setConfig()
|
||||
minioSetConfigKVMock = func(kv string) (restart bool, err error) {
|
||||
minioSetConfigKVMock = func(_ string) (restart bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
configName := "notify_postgres"
|
||||
@@ -140,7 +109,8 @@ func TestSetConfig(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Test-1 : setConfig() sets a config with two key value pairs
|
||||
restart, err := setConfig(ctx, adminClient, &configName, kvs)
|
||||
if err != nil {
|
||||
@@ -149,7 +119,7 @@ func TestSetConfig(t *testing.T) {
|
||||
assert.Equal(restart, false)
|
||||
|
||||
// Test-2 : setConfig() returns error, handle properly
|
||||
minioSetConfigKVMock = func(kv string) (restart bool, err error) {
|
||||
minioSetConfigKVMock = func(_ string) (restart bool, err error) {
|
||||
return false, errors.New("error")
|
||||
}
|
||||
restart, err = setConfig(ctx, adminClient, &configName, kvs)
|
||||
@@ -159,7 +129,7 @@ func TestSetConfig(t *testing.T) {
|
||||
assert.Equal(restart, false)
|
||||
|
||||
// Test-4 : setConfig() set config, need restart
|
||||
minioSetConfigKVMock = func(kv string) (restart bool, err error) {
|
||||
minioSetConfigKVMock = func(_ string) (restart bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
restart, err = setConfig(ctx, adminClient, &configName, kvs)
|
||||
@@ -167,20 +137,20 @@ func TestSetConfig(t *testing.T) {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
assert.Equal(restart, true)
|
||||
|
||||
}
|
||||
|
||||
func TestDelConfig(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
adminClient := AdminClientMock{}
|
||||
function := "resetConfig()"
|
||||
// mock function response from setConfig()
|
||||
minioDelConfigKVMock = func(name string) (err error) {
|
||||
minioDelConfigKVMock = func(_ string) (err error) {
|
||||
return nil
|
||||
}
|
||||
configName := "region"
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Test-1 : resetConfig() resets a config with the config name
|
||||
err := resetConfig(ctx, adminClient, &configName)
|
||||
if err != nil {
|
||||
@@ -188,7 +158,7 @@ func TestDelConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test-2 : resetConfig() returns error, handle properly
|
||||
minioDelConfigKVMock = func(name string) (err error) {
|
||||
minioDelConfigKVMock = func(_ string) (err error) {
|
||||
return errors.New("error")
|
||||
}
|
||||
|
||||
@@ -250,7 +220,7 @@ func Test_buildConfig(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
if got := buildConfig(tt.args.configName, tt.args.kvs); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("buildConfig() = %s, want %s", *got, *tt.want)
|
||||
}
|
||||
@@ -260,7 +230,7 @@ func Test_buildConfig(t *testing.T) {
|
||||
|
||||
func Test_setConfigWithARN(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := adminClientMock{}
|
||||
client := AdminClientMock{}
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -290,7 +260,7 @@ func Test_setConfigWithARN(t *testing.T) {
|
||||
},
|
||||
arn: "1",
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, nil
|
||||
},
|
||||
wantErr: false,
|
||||
@@ -310,7 +280,7 @@ func Test_setConfigWithARN(t *testing.T) {
|
||||
},
|
||||
arn: "1",
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return true, nil
|
||||
},
|
||||
wantErr: false,
|
||||
@@ -330,7 +300,7 @@ func Test_setConfigWithARN(t *testing.T) {
|
||||
},
|
||||
arn: "",
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, nil
|
||||
},
|
||||
wantErr: false,
|
||||
@@ -350,7 +320,7 @@ func Test_setConfigWithARN(t *testing.T) {
|
||||
},
|
||||
arn: "",
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, errors.New("error")
|
||||
},
|
||||
wantErr: true,
|
||||
@@ -358,7 +328,7 @@ func Test_setConfigWithARN(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
// mock function response from setConfig()
|
||||
minioSetConfigKVMock = tt.mockSetConfig
|
||||
restart, err := setConfigWithARNAccountID(tt.args.ctx, tt.args.client, tt.args.configName, tt.args.kvs, tt.args.arn)
|
||||
@@ -371,7 +341,7 @@ func Test_setConfigWithARN(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_getConfig(t *testing.T) {
|
||||
client := adminClientMock{}
|
||||
client := AdminClientMock{}
|
||||
type args struct {
|
||||
client MinioAdmin
|
||||
name string
|
||||
@@ -380,7 +350,7 @@ func Test_getConfig(t *testing.T) {
|
||||
name string
|
||||
args args
|
||||
mock func()
|
||||
want []*models.ConfigurationKV
|
||||
want []*models.Configuration
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
@@ -391,7 +361,7 @@ func Test_getConfig(t *testing.T) {
|
||||
},
|
||||
mock: func() {
|
||||
// mock function response from getConfig()
|
||||
minioGetConfigKVMock = func(key string) ([]byte, error) {
|
||||
minioGetConfigKVMock = func(_ string) ([]byte, error) {
|
||||
return []byte(`notify_postgres:_ connection_string="host=localhost dbname=minio_events user=postgres password=password port=5432 sslmode=disable" table=bucketevents`), nil
|
||||
}
|
||||
|
||||
@@ -437,18 +407,22 @@ func Test_getConfig(t *testing.T) {
|
||||
KeysHelp: configListMock,
|
||||
}
|
||||
// mock function response from listConfig()
|
||||
minioHelpConfigKVMock = func(subSys, key string, envOnly bool) (madmin.Help, error) {
|
||||
minioHelpConfigKVMock = func(_, _ string, _ bool) (madmin.Help, error) {
|
||||
return mockConfigList, nil
|
||||
}
|
||||
},
|
||||
want: []*models.ConfigurationKV{
|
||||
want: []*models.Configuration{
|
||||
{
|
||||
Key: PostgresConnectionString,
|
||||
Value: "host=localhost dbname=minio_events user=postgres password=password port=5432 sslmode=disable",
|
||||
},
|
||||
{
|
||||
Key: PostgresTable,
|
||||
Value: "bucketevents",
|
||||
KeyValues: []*models.ConfigurationKV{
|
||||
{
|
||||
Key: PostgresConnectionString,
|
||||
Value: "host=localhost dbname=minio_events user=postgres password=password port=5432 sslmode=disable",
|
||||
},
|
||||
{
|
||||
Key: PostgresTable,
|
||||
Value: "bucketevents",
|
||||
},
|
||||
}, Name: "notify_postgres",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
@@ -461,7 +435,7 @@ func Test_getConfig(t *testing.T) {
|
||||
},
|
||||
mock: func() {
|
||||
// mock function response from getConfig()
|
||||
minioGetConfigKVMock = func(key string) ([]byte, error) {
|
||||
minioGetConfigKVMock = func(_ string) ([]byte, error) {
|
||||
return []byte(`notify_postgres:_`), nil
|
||||
}
|
||||
|
||||
@@ -507,12 +481,12 @@ func Test_getConfig(t *testing.T) {
|
||||
KeysHelp: configListMock,
|
||||
}
|
||||
// mock function response from listConfig()
|
||||
minioHelpConfigKVMock = func(subSys, key string, envOnly bool) (madmin.Help, error) {
|
||||
minioHelpConfigKVMock = func(_, _ string, _ bool) (madmin.Help, error) {
|
||||
return mockConfigList, nil
|
||||
}
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "random bytes coming out of getConfigKv",
|
||||
@@ -521,9 +495,8 @@ func Test_getConfig(t *testing.T) {
|
||||
name: "notify_postgres",
|
||||
},
|
||||
mock: func() {
|
||||
|
||||
// mock function response from getConfig()
|
||||
minioGetConfigKVMock = func(key string) ([]byte, error) {
|
||||
minioGetConfigKVMock = func(_ string) ([]byte, error) {
|
||||
x := make(map[string]string)
|
||||
x["x"] = "x"
|
||||
j, _ := json.Marshal(x)
|
||||
@@ -572,7 +545,7 @@ func Test_getConfig(t *testing.T) {
|
||||
KeysHelp: configListMock,
|
||||
}
|
||||
// mock function response from listConfig()
|
||||
minioHelpConfigKVMock = func(subSys, key string, envOnly bool) (madmin.Help, error) {
|
||||
minioHelpConfigKVMock = func(_, _ string, _ bool) (madmin.Help, error) {
|
||||
return mockConfigList, nil
|
||||
}
|
||||
},
|
||||
@@ -586,15 +559,14 @@ func Test_getConfig(t *testing.T) {
|
||||
name: "notify_postgresx",
|
||||
},
|
||||
mock: func() {
|
||||
|
||||
// mock function response from getConfig()
|
||||
minioGetConfigKVMock = func(key string) ([]byte, error) {
|
||||
minioGetConfigKVMock = func(_ string) ([]byte, error) {
|
||||
return nil, errors.New("invalid config")
|
||||
}
|
||||
|
||||
mockConfigList := madmin.Help{}
|
||||
// mock function response from listConfig()
|
||||
minioHelpConfigKVMock = func(subSys, key string, envOnly bool) (madmin.Help, error) {
|
||||
minioHelpConfigKVMock = func(_, _ string, _ bool) (madmin.Help, error) {
|
||||
return mockConfigList, nil
|
||||
}
|
||||
},
|
||||
@@ -609,11 +581,11 @@ func Test_getConfig(t *testing.T) {
|
||||
},
|
||||
mock: func() {
|
||||
// mock function response from getConfig()
|
||||
minioGetConfigKVMock = func(key string) ([]byte, error) {
|
||||
minioGetConfigKVMock = func(_ string) ([]byte, error) {
|
||||
return nil, errors.New("invalid config")
|
||||
}
|
||||
// mock function response from listConfig()
|
||||
minioHelpConfigKVMock = func(subSys, key string, envOnly bool) (madmin.Help, error) {
|
||||
minioHelpConfigKVMock = func(_, _ string, _ bool) (madmin.Help, error) {
|
||||
return madmin.Help{}, errors.New("no help")
|
||||
}
|
||||
},
|
||||
@@ -623,7 +595,7 @@ func Test_getConfig(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt.mock()
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
got, err := getConfig(context.Background(), tt.args.client, tt.args.name)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package restapi
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -22,29 +22,42 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/websocket"
|
||||
)
|
||||
|
||||
const logTimeFormat string = "15:04:05 MST 01/02/2006"
|
||||
|
||||
// startConsoleLog starts log of the servers
|
||||
func startConsoleLog(ctx context.Context, conn WSConn, client MinioAdmin) error {
|
||||
// TODO: accept parameters as variables
|
||||
func startConsoleLog(ctx context.Context, conn WSConn, client MinioAdmin, logRequest LogRequest) error {
|
||||
var node string
|
||||
// name of node, default = "" (all)
|
||||
node := ""
|
||||
if logRequest.node == "all" {
|
||||
node = ""
|
||||
} else {
|
||||
node = logRequest.node
|
||||
}
|
||||
|
||||
trimNode := strings.Split(node, ":")
|
||||
// number of log lines
|
||||
lineCount := 100
|
||||
// type of logs "minio"|"application"|"all" default = "all"
|
||||
logKind := "all"
|
||||
var logKind string
|
||||
if logRequest.logType == "minio" || logRequest.logType == "application" || logRequest.logType == "all" {
|
||||
logKind = logRequest.logType
|
||||
} else {
|
||||
logKind = "all"
|
||||
}
|
||||
|
||||
// Start listening on all Console Log activity.
|
||||
logCh := client.getLogs(ctx, node, lineCount, logKind)
|
||||
logCh := client.getLogs(ctx, trimNode[0], lineCount, logKind)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case logInfo, ok := <-logCh:
|
||||
|
||||
// zero value returned because the channel is closed and empty
|
||||
if !ok {
|
||||
return nil
|
||||
117
api/admin_console_test.go
Normal file
117
api/admin_console_test.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAdminConsoleLog(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := AdminClientMock{}
|
||||
mockWSConn := mockConn{}
|
||||
function := "startConsoleLog(ctx, )"
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
testReceiver := make(chan madmin.LogInfo, 5)
|
||||
textToReceive := "test message"
|
||||
testStreamSize := 5
|
||||
isClosed := false // testReceiver is closed?
|
||||
|
||||
// Test-1: Serve Console with no errors until Console finishes sending
|
||||
// define mock function behavior for minio server Console
|
||||
minioGetLogsMock = func(_ context.Context, _ string, _ int, _ string) <-chan madmin.LogInfo {
|
||||
ch := make(chan madmin.LogInfo)
|
||||
// Only success, start a routine to start reading line by line.
|
||||
go func(ch chan<- madmin.LogInfo) {
|
||||
defer close(ch)
|
||||
lines := make([]int, testStreamSize)
|
||||
// mocking sending 5 lines of info
|
||||
for range lines {
|
||||
info := madmin.LogInfo{
|
||||
ConsoleMsg: textToReceive,
|
||||
}
|
||||
ch <- info
|
||||
}
|
||||
}(ch)
|
||||
return ch
|
||||
}
|
||||
writesCount := 1
|
||||
// mock connection WriteMessage() no error
|
||||
connWriteMessageMock = func(_ int, data []byte) error {
|
||||
// emulate that receiver gets the message written
|
||||
var t madmin.LogInfo
|
||||
_ = json.Unmarshal(data, &t)
|
||||
if writesCount == testStreamSize {
|
||||
if !isClosed {
|
||||
close(testReceiver)
|
||||
isClosed = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
testReceiver <- t
|
||||
writesCount++
|
||||
return nil
|
||||
}
|
||||
if err := startConsoleLog(ctx, mockWSConn, adminClient, LogRequest{node: "", logType: "all"}); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// check that the TestReceiver got the same number of data from Console.
|
||||
for i := range testReceiver {
|
||||
assert.Equal(textToReceive, i.ConsoleMsg)
|
||||
}
|
||||
|
||||
// Test-2: if error happens while writing, return error
|
||||
connWriteMessageMock = func(_ int, _ []byte) error {
|
||||
return fmt.Errorf("error on write")
|
||||
}
|
||||
if err := startConsoleLog(ctx, mockWSConn, adminClient, LogRequest{node: "", logType: "all"}); assert.Error(err) {
|
||||
assert.Equal("error on write", err.Error())
|
||||
}
|
||||
|
||||
// Test-3: error happens on GetLogs Minio, Console should stop
|
||||
// and error shall be returned.
|
||||
minioGetLogsMock = func(_ context.Context, _ string, _ int, _ string) <-chan madmin.LogInfo {
|
||||
ch := make(chan madmin.LogInfo)
|
||||
// Only success, start a routine to start reading line by line.
|
||||
go func(ch chan<- madmin.LogInfo) {
|
||||
defer close(ch)
|
||||
lines := make([]int, 2)
|
||||
// mocking sending 5 lines of info
|
||||
for range lines {
|
||||
info := madmin.LogInfo{
|
||||
ConsoleMsg: textToReceive,
|
||||
}
|
||||
ch <- info
|
||||
}
|
||||
ch <- madmin.LogInfo{Err: fmt.Errorf("error on Console")}
|
||||
}(ch)
|
||||
return ch
|
||||
}
|
||||
connWriteMessageMock = func(_ int, _ []byte) error {
|
||||
return nil
|
||||
}
|
||||
if err := startConsoleLog(ctx, mockWSConn, adminClient, LogRequest{node: "", logType: "all"}); assert.Error(err) {
|
||||
assert.Equal("error on Console", err.Error())
|
||||
}
|
||||
}
|
||||
339
api/admin_groups.go
Normal file
339
api/admin_groups.go
Normal file
@@ -0,0 +1,339 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
|
||||
groupApi "github.com/minio/console/api/operations/group"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
)
|
||||
|
||||
func registerGroupsHandlers(api *operations.ConsoleAPI) {
|
||||
// List Groups
|
||||
api.GroupListGroupsHandler = groupApi.ListGroupsHandlerFunc(func(params groupApi.ListGroupsParams, session *models.Principal) middleware.Responder {
|
||||
listGroupsResponse, err := getListGroupsResponse(session, params)
|
||||
if err != nil {
|
||||
return groupApi.NewListGroupsDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return groupApi.NewListGroupsOK().WithPayload(listGroupsResponse)
|
||||
})
|
||||
// Group Info
|
||||
api.GroupGroupInfoHandler = groupApi.GroupInfoHandlerFunc(func(params groupApi.GroupInfoParams, session *models.Principal) middleware.Responder {
|
||||
groupInfo, err := getGroupInfoResponse(session, params)
|
||||
if err != nil {
|
||||
return groupApi.NewGroupInfoDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return groupApi.NewGroupInfoOK().WithPayload(groupInfo)
|
||||
})
|
||||
// Add Group
|
||||
api.GroupAddGroupHandler = groupApi.AddGroupHandlerFunc(func(params groupApi.AddGroupParams, session *models.Principal) middleware.Responder {
|
||||
if err := getAddGroupResponse(session, params); err != nil {
|
||||
return groupApi.NewAddGroupDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return groupApi.NewAddGroupCreated()
|
||||
})
|
||||
// Remove Group
|
||||
api.GroupRemoveGroupHandler = groupApi.RemoveGroupHandlerFunc(func(params groupApi.RemoveGroupParams, session *models.Principal) middleware.Responder {
|
||||
if err := getRemoveGroupResponse(session, params); err != nil {
|
||||
return groupApi.NewRemoveGroupDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return groupApi.NewRemoveGroupNoContent()
|
||||
})
|
||||
// Update Group
|
||||
api.GroupUpdateGroupHandler = groupApi.UpdateGroupHandlerFunc(func(params groupApi.UpdateGroupParams, session *models.Principal) middleware.Responder {
|
||||
groupUpdateResp, err := getUpdateGroupResponse(session, params)
|
||||
if err != nil {
|
||||
return groupApi.NewUpdateGroupDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return groupApi.NewUpdateGroupOK().WithPayload(groupUpdateResp)
|
||||
})
|
||||
}
|
||||
|
||||
// getListGroupsResponse performs listGroups() and serializes it to the handler's output
|
||||
func getListGroupsResponse(session *models.Principal, params groupApi.ListGroupsParams) (*models.ListGroupsResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
groups, err := adminClient.listGroups(ctx)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// serialize output
|
||||
listGroupsResponse := &models.ListGroupsResponse{
|
||||
Groups: groups,
|
||||
Total: int64(len(groups)),
|
||||
}
|
||||
|
||||
return listGroupsResponse, nil
|
||||
}
|
||||
|
||||
// groupInfo calls MinIO server get Group's info
|
||||
func groupInfo(ctx context.Context, client MinioAdmin, group string) (*madmin.GroupDesc, error) {
|
||||
groupDesc, err := client.getGroupDescription(ctx, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return groupDesc, nil
|
||||
}
|
||||
|
||||
// getGroupInfoResponse performs groupInfo() and serializes it to the handler's output
|
||||
func getGroupInfoResponse(session *models.Principal, params groupApi.GroupInfoParams) (*models.Group, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
groupDesc, err := groupInfo(ctx, adminClient, params.Name)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
groupResponse := &models.Group{
|
||||
Members: groupDesc.Members,
|
||||
Name: groupDesc.Name,
|
||||
Policy: groupDesc.Policy,
|
||||
Status: groupDesc.Status,
|
||||
}
|
||||
|
||||
return groupResponse, nil
|
||||
}
|
||||
|
||||
// addGroupAdd a MinIO group with the defined members
|
||||
func addGroup(ctx context.Context, client MinioAdmin, group string, members []string) error {
|
||||
gAddRemove := madmin.GroupAddRemove{
|
||||
Group: group,
|
||||
Members: members,
|
||||
IsRemove: false,
|
||||
}
|
||||
err := client.updateGroupMembers(ctx, gAddRemove)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAddGroupResponse performs addGroup() and serializes it to the handler's output
|
||||
func getAddGroupResponse(session *models.Principal, params groupApi.AddGroupParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
// AddGroup request needed to proceed
|
||||
if params.Body == nil {
|
||||
return ErrorWithContext(ctx, ErrGroupBodyNotInRequest)
|
||||
}
|
||||
groupRequest := params.Body
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
groupList, _ := adminClient.listGroups(ctx)
|
||||
|
||||
for _, b := range groupList {
|
||||
if b == *groupRequest.Group {
|
||||
return ErrorWithContext(ctx, ErrGroupAlreadyExists)
|
||||
}
|
||||
}
|
||||
|
||||
if err := addGroup(ctx, adminClient, *groupRequest.Group, groupRequest.Members); err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeGroup deletes a minIO group only if it has no members
|
||||
func removeGroup(ctx context.Context, client MinioAdmin, group string) error {
|
||||
gAddRemove := madmin.GroupAddRemove{
|
||||
Group: group,
|
||||
Members: []string{},
|
||||
IsRemove: true,
|
||||
}
|
||||
err := client.updateGroupMembers(ctx, gAddRemove)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRemoveGroupResponse performs removeGroup() and serializes it to the handler's output
|
||||
func getRemoveGroupResponse(session *models.Principal, params groupApi.RemoveGroupParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
if params.Name == "" {
|
||||
return ErrorWithContext(ctx, ErrGroupNameNotInRequest)
|
||||
}
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
// Create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
if err := removeGroup(ctx, adminClient, params.Name); err != nil {
|
||||
minioError := madmin.ToErrorResponse(err)
|
||||
err2 := ErrorWithContext(ctx, err)
|
||||
if minioError.Code == "XMinioAdminNoSuchGroup" {
|
||||
err2.Code = 404
|
||||
}
|
||||
return err2
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateGroup updates a group by adding/removing members and setting the status to the desired one
|
||||
//
|
||||
// isRemove: whether remove members or not
|
||||
func updateGroupMembers(ctx context.Context, client MinioAdmin, group string, members []string, isRemove bool) error {
|
||||
gAddRemove := madmin.GroupAddRemove{
|
||||
Group: group,
|
||||
Members: members,
|
||||
IsRemove: isRemove,
|
||||
}
|
||||
err := client.updateGroupMembers(ctx, gAddRemove)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addOrDeleteMembers updates a group members by adding or deleting them based on the expectedMembers
|
||||
func addOrDeleteMembers(ctx context.Context, client MinioAdmin, group *madmin.GroupDesc, expectedMembers []string) error {
|
||||
// get members to delete/add
|
||||
membersToDelete := DifferenceArrays(group.Members, expectedMembers)
|
||||
membersToAdd := DifferenceArrays(expectedMembers, group.Members)
|
||||
// delete members if any to be deleted
|
||||
if len(membersToDelete) > 0 {
|
||||
err := updateGroupMembers(ctx, client, group.Name, membersToDelete, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// add members if any to be added
|
||||
if len(membersToAdd) > 0 {
|
||||
err := updateGroupMembers(ctx, client, group.Name, membersToAdd, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setGroupStatus(ctx context.Context, client MinioAdmin, group, status string) error {
|
||||
var setStatus madmin.GroupStatus
|
||||
switch status {
|
||||
case "enabled":
|
||||
setStatus = madmin.GroupEnabled
|
||||
case "disabled":
|
||||
setStatus = madmin.GroupDisabled
|
||||
default:
|
||||
return errors.New(500, "status not valid")
|
||||
}
|
||||
return client.setGroupStatus(ctx, group, setStatus)
|
||||
}
|
||||
|
||||
// getUpdateGroupResponse updates a group by adding or removing it's members depending on the request,
|
||||
// also sets the group's status if status in the request is different than the current one.
|
||||
// Then serializes the output to be used by the handler.
|
||||
func getUpdateGroupResponse(session *models.Principal, params groupApi.UpdateGroupParams) (*models.Group, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
if params.Name == "" {
|
||||
return nil, ErrorWithContext(ctx, ErrGroupNameNotInRequest)
|
||||
}
|
||||
if params.Body == nil {
|
||||
return nil, ErrorWithContext(ctx, ErrGroupBodyNotInRequest)
|
||||
}
|
||||
expectedGroupUpdate := params.Body
|
||||
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
groupUpdated, err := groupUpdate(ctx, adminClient, params.Name, expectedGroupUpdate)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
groupResponse := &models.Group{
|
||||
Name: groupUpdated.Name,
|
||||
Members: groupUpdated.Members,
|
||||
Policy: groupUpdated.Policy,
|
||||
Status: groupUpdated.Status,
|
||||
}
|
||||
return groupResponse, nil
|
||||
}
|
||||
|
||||
// groupUpdate updates a group given the expected parameters, compares the expected parameters against the current ones
|
||||
// and updates them accordingly, status is only updated if the expected status is different than the current one.
|
||||
// Then fetches the group again to return the object updated.
|
||||
func groupUpdate(ctx context.Context, client MinioAdmin, groupName string, expectedGroup *models.UpdateGroupRequest) (*madmin.GroupDesc, error) {
|
||||
expectedMembers := expectedGroup.Members
|
||||
expectedStatus := *expectedGroup.Status
|
||||
// get current members and status
|
||||
groupDescription, err := groupInfo(ctx, client, groupName)
|
||||
if err != nil {
|
||||
LogInfo("error getting group info: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
// update group members
|
||||
err = addOrDeleteMembers(ctx, client, groupDescription, expectedMembers)
|
||||
if err != nil {
|
||||
LogInfo("error updating group: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
// update group status only if different from current status
|
||||
if expectedStatus != groupDescription.Status {
|
||||
err = setGroupStatus(ctx, client, groupDescription.Name, expectedStatus)
|
||||
if err != nil {
|
||||
LogInfo("error updating group's status: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// return latest group info to verify that changes were applied correctly
|
||||
groupDescription, err = groupInfo(ctx, client, groupName)
|
||||
if err != nil {
|
||||
LogInfo("error getting group info: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return groupDescription, nil
|
||||
}
|
||||
@@ -14,52 +14,25 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package restapi
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"errors"
|
||||
)
|
||||
|
||||
// assigning mock at runtime instead of compile time
|
||||
var minioListGroupsMock func() ([]string, error)
|
||||
var minioUpdateGroupMembersMock func(madmin.GroupAddRemove) error
|
||||
var minioGetGroupDescriptionMock func(group string) (*madmin.GroupDesc, error)
|
||||
var minioSetGroupStatusMock func(group string, status madmin.GroupStatus) error
|
||||
|
||||
// mock function of listGroups()
|
||||
func (ac adminClientMock) listGroups(ctx context.Context) ([]string, error) {
|
||||
return minioListGroupsMock()
|
||||
}
|
||||
|
||||
// mock function of updateGroupMembers()
|
||||
func (ac adminClientMock) updateGroupMembers(ctx context.Context, req madmin.GroupAddRemove) error {
|
||||
return minioUpdateGroupMembersMock(req)
|
||||
}
|
||||
|
||||
// mock function of getGroupDescription()
|
||||
func (ac adminClientMock) getGroupDescription(ctx context.Context, group string) (*madmin.GroupDesc, error) {
|
||||
return minioGetGroupDescriptionMock(group)
|
||||
}
|
||||
|
||||
// mock function setGroupStatus()
|
||||
func (ac adminClientMock) setGroupStatus(ctx context.Context, group string, status madmin.GroupStatus) error {
|
||||
return minioSetGroupStatusMock(group, status)
|
||||
}
|
||||
|
||||
func TestListGroups(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
ctx := context.Background()
|
||||
|
||||
adminClient := AdminClientMock{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Test-1 : listGroups() Get response from minio client with two Groups and return the same number on listGroups()
|
||||
mockGroupsList := []string{"group1", "group2"}
|
||||
|
||||
@@ -93,9 +66,9 @@ func TestListGroups(t *testing.T) {
|
||||
|
||||
func TestAddGroup(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
ctx := context.Background()
|
||||
|
||||
adminClient := AdminClientMock{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Test-1 : addGroup() add a new group with two members
|
||||
newGroup := "acmeGroup"
|
||||
groupMembers := []string{"user1", "user2"}
|
||||
@@ -120,9 +93,9 @@ func TestAddGroup(t *testing.T) {
|
||||
|
||||
func TestRemoveGroup(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
ctx := context.Background()
|
||||
|
||||
adminClient := AdminClientMock{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Test-1 : removeGroup() remove group assume it has no members
|
||||
groupToRemove := "acmeGroup"
|
||||
// mock function response from updateGroupMembers()
|
||||
@@ -145,9 +118,9 @@ func TestRemoveGroup(t *testing.T) {
|
||||
|
||||
func TestGroupInfo(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
ctx := context.Background()
|
||||
|
||||
adminClient := AdminClientMock{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Test-1 : groupInfo() get group info
|
||||
groupName := "acmeGroup"
|
||||
mockResponse := &madmin.GroupDesc{
|
||||
@@ -157,7 +130,7 @@ func TestGroupInfo(t *testing.T) {
|
||||
Status: "enabled",
|
||||
}
|
||||
// mock function response from updateGroupMembers()
|
||||
minioGetGroupDescriptionMock = func(group string) (*madmin.GroupDesc, error) {
|
||||
minioGetGroupDescriptionMock = func(_ string) (*madmin.GroupDesc, error) {
|
||||
return mockResponse, nil
|
||||
}
|
||||
function := "groupInfo()"
|
||||
@@ -171,7 +144,7 @@ func TestGroupInfo(t *testing.T) {
|
||||
assert.Equal("enabled", info.Status)
|
||||
|
||||
// Test-2 : groupInfo() Return error and see that the error is handled correctly and returned
|
||||
minioGetGroupDescriptionMock = func(group string) (*madmin.GroupDesc, error) {
|
||||
minioGetGroupDescriptionMock = func(_ string) (*madmin.GroupDesc, error) {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
_, err = groupInfo(ctx, adminClient, groupName)
|
||||
@@ -182,9 +155,9 @@ func TestGroupInfo(t *testing.T) {
|
||||
|
||||
func TestUpdateGroup(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
ctx := context.Background()
|
||||
|
||||
adminClient := AdminClientMock{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Test-1 : addOrDeleteMembers() update group members add user3 and delete user2
|
||||
function := "addOrDeleteMembers()"
|
||||
groupName := "acmeGroup"
|
||||
@@ -253,7 +226,7 @@ func TestUpdateGroup(t *testing.T) {
|
||||
// the function twice but the second time returned an error
|
||||
is2ndRunGroupInfo := false
|
||||
// mock function response from updateGroupMembers()
|
||||
minioGetGroupDescriptionMock = func(group string) (*madmin.GroupDesc, error) {
|
||||
minioGetGroupDescriptionMock = func(_ string) (*madmin.GroupDesc, error) {
|
||||
if is2ndRunGroupInfo {
|
||||
return mockResponseAfterUpdate, nil
|
||||
}
|
||||
@@ -263,7 +236,7 @@ func TestUpdateGroup(t *testing.T) {
|
||||
minioUpdateGroupMembersMock = func(madmin.GroupAddRemove) error {
|
||||
return nil
|
||||
}
|
||||
minioSetGroupStatusMock = func(group string, status madmin.GroupStatus) error {
|
||||
minioSetGroupStatusMock = func(_ string, _ madmin.GroupStatus) error {
|
||||
return nil
|
||||
}
|
||||
groupUpdated, err := groupUpdate(ctx, adminClient, groupName, expectedGroupUpdate)
|
||||
@@ -278,14 +251,14 @@ func TestUpdateGroup(t *testing.T) {
|
||||
|
||||
func TestSetGroupStatus(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
adminClient := AdminClientMock{}
|
||||
function := "setGroupStatus()"
|
||||
groupName := "acmeGroup"
|
||||
ctx := context.Background()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Test-1: setGroupStatus() update valid disabled status
|
||||
expectedStatus := "disabled"
|
||||
minioSetGroupStatusMock = func(group string, status madmin.GroupStatus) error {
|
||||
minioSetGroupStatusMock = func(_ string, _ madmin.GroupStatus) error {
|
||||
return nil
|
||||
}
|
||||
if err := setGroupStatus(ctx, adminClient, groupName, expectedStatus); err != nil {
|
||||
@@ -293,7 +266,7 @@ func TestSetGroupStatus(t *testing.T) {
|
||||
}
|
||||
// Test-2: setGroupStatus() update valid enabled status
|
||||
expectedStatus = "enabled"
|
||||
minioSetGroupStatusMock = func(group string, status madmin.GroupStatus) error {
|
||||
minioSetGroupStatusMock = func(_ string, _ madmin.GroupStatus) error {
|
||||
return nil
|
||||
}
|
||||
if err := setGroupStatus(ctx, adminClient, groupName, expectedStatus); err != nil {
|
||||
@@ -301,7 +274,7 @@ func TestSetGroupStatus(t *testing.T) {
|
||||
}
|
||||
// Test-3: setGroupStatus() update invalid status, should send error
|
||||
expectedStatus = "invalid"
|
||||
minioSetGroupStatusMock = func(group string, status madmin.GroupStatus) error {
|
||||
minioSetGroupStatusMock = func(_ string, _ madmin.GroupStatus) error {
|
||||
return nil
|
||||
}
|
||||
if err := setGroupStatus(ctx, adminClient, groupName, expectedStatus); assert.Error(err) {
|
||||
@@ -309,7 +282,7 @@ func TestSetGroupStatus(t *testing.T) {
|
||||
}
|
||||
// Test-4: setGroupStatus() handler error correctly
|
||||
expectedStatus = "enabled"
|
||||
minioSetGroupStatusMock = func(group string, status madmin.GroupStatus) error {
|
||||
minioSetGroupStatusMock = func(_ string, _ madmin.GroupStatus) error {
|
||||
return errors.New("error")
|
||||
}
|
||||
if err := setGroupStatus(ctx, adminClient, groupName, expectedStatus); assert.Error(err) {
|
||||
153
api/admin_health_info.go
Normal file
153
api/admin_health_info.go
Normal file
@@ -0,0 +1,153 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
b64 "encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/pkg/logger"
|
||||
"github.com/minio/console/pkg/utils"
|
||||
|
||||
subnet "github.com/minio/console/pkg/subnet"
|
||||
mc "github.com/minio/mc/cmd"
|
||||
"github.com/minio/websocket"
|
||||
)
|
||||
|
||||
// startHealthInfo starts fetching mc.ServerHealthInfo and
|
||||
// sends messages with the corresponding data on the websocket connection
|
||||
func startHealthInfo(ctx context.Context, conn WSConn, client MinioAdmin, deadline *time.Duration) error {
|
||||
if deadline == nil {
|
||||
return errors.New("duration can't be nil on startHealthInfo")
|
||||
}
|
||||
|
||||
// Fetch info of all servers (cluster or single server)
|
||||
healthInfo, version, err := client.serverHealthInfo(ctx, *deadline)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
compressedDiag, err := mc.TarGZHealthInfo(healthInfo, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encodedDiag := b64.StdEncoding.EncodeToString(compressedDiag)
|
||||
type messageReport struct {
|
||||
Encoded string `json:"encoded"`
|
||||
ServerHealthInfo interface{} `json:"serverHealthInfo"`
|
||||
SubnetResponse string `json:"subnetResponse"`
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, utils.ContextClientIP, conn.remoteAddress())
|
||||
err = sendHealthInfoToSubnet(ctx, compressedDiag, client)
|
||||
report := messageReport{
|
||||
Encoded: encodedDiag,
|
||||
ServerHealthInfo: healthInfo,
|
||||
SubnetResponse: mc.SubnetBaseURL() + "/health",
|
||||
}
|
||||
if err != nil {
|
||||
report.SubnetResponse = fmt.Sprintf("Error: %s", err.Error())
|
||||
}
|
||||
|
||||
message, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send Message through websocket connection
|
||||
return conn.writeMessage(websocket.TextMessage, message)
|
||||
}
|
||||
|
||||
// getHealthInfoOptionsFromReq gets duration for startHealthInfo request
|
||||
// path come as : `/health-info?deadline=2h`
|
||||
func getHealthInfoOptionsFromReq(req *http.Request) (*time.Duration, error) {
|
||||
deadlineDuration, err := time.ParseDuration(req.FormValue("deadline"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &deadlineDuration, nil
|
||||
}
|
||||
|
||||
func updateMcGlobals(subnetTokenConfig subnet.LicenseTokenConfig) error {
|
||||
mc.GlobalDevMode = getConsoleDevMode()
|
||||
if len(subnetTokenConfig.Proxy) > 0 {
|
||||
proxyURL, e := url.Parse(subnetTokenConfig.Proxy)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
mc.GlobalSubnetProxyURL = proxyURL
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendHealthInfoToSubnet(ctx context.Context, compressedHealthInfo []byte, client MinioAdmin) error {
|
||||
filename := fmt.Sprintf("health_%d.json.gz", time.Now().Unix())
|
||||
subnetTokenConfig, e := GetSubnetKeyFromMinIOConfig(ctx, client)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
e = updateMcGlobals(*subnetTokenConfig)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
var apiKey string
|
||||
if len(subnetTokenConfig.APIKey) != 0 {
|
||||
apiKey = subnetTokenConfig.APIKey
|
||||
} else {
|
||||
apiKey, e = subnet.GetSubnetAPIKeyUsingLicense(subnetTokenConfig.License)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
e = os.WriteFile(filename, compressedHealthInfo, 0o666)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
headers := mc.SubnetAPIKeyAuthHeaders(apiKey)
|
||||
resp, e := (&mc.SubnetFileUploader{
|
||||
FilePath: filename,
|
||||
ReqURL: mc.SubnetUploadURL("health"),
|
||||
Headers: headers,
|
||||
DeleteAfterUpload: true,
|
||||
}).UploadFileToSubnet()
|
||||
if e != nil {
|
||||
// file gets deleted only if upload is successful
|
||||
// so we delete explicitly here as we already have the bytes
|
||||
logger.LogIf(ctx, os.Remove(filename))
|
||||
return e
|
||||
}
|
||||
|
||||
type SubnetResponse struct {
|
||||
LicenseV2 string `json:"license_v2,omitempty"`
|
||||
APIKey string `json:"api_key,omitempty"`
|
||||
}
|
||||
|
||||
var subnetResp SubnetResponse
|
||||
e = json.Unmarshal([]byte(resp), &subnetResp)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
147
api/admin_health_info_test.go
Normal file
147
api/admin_health_info_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
madmin "github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
func Test_serverHealthInfo(t *testing.T) {
|
||||
var testReceiver chan madmin.HealthInfo
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
client := AdminClientMock{}
|
||||
mockWSConn := mockConn{}
|
||||
deadlineDuration, _ := time.ParseDuration("1h")
|
||||
|
||||
type args struct {
|
||||
deadline time.Duration
|
||||
wsWriteMock func(messageType int, data []byte) error
|
||||
mockMessages []madmin.HealthInfo
|
||||
}
|
||||
tests := []struct {
|
||||
test string
|
||||
args args
|
||||
wantError error
|
||||
}{
|
||||
{
|
||||
test: "Return simple health info, no errors",
|
||||
args: args{
|
||||
deadline: deadlineDuration,
|
||||
mockMessages: []madmin.HealthInfo{{}, {}},
|
||||
wsWriteMock: func(_ int, data []byte) error {
|
||||
// mock connection WriteMessage() no error
|
||||
// emulate that receiver gets the message written
|
||||
var t madmin.HealthInfo
|
||||
_ = json.Unmarshal(data, &t)
|
||||
testReceiver <- t
|
||||
return nil
|
||||
},
|
||||
},
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
test: "Return simple health info2, no errors",
|
||||
args: args{
|
||||
deadline: deadlineDuration,
|
||||
mockMessages: []madmin.HealthInfo{{}},
|
||||
wsWriteMock: func(_ int, data []byte) error {
|
||||
// mock connection WriteMessage() no error
|
||||
// emulate that receiver gets the message written
|
||||
var t madmin.HealthInfo
|
||||
_ = json.Unmarshal(data, &t)
|
||||
testReceiver <- t
|
||||
return nil
|
||||
},
|
||||
},
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
test: "Handle error on ws write",
|
||||
args: args{
|
||||
deadline: deadlineDuration,
|
||||
mockMessages: []madmin.HealthInfo{{}},
|
||||
wsWriteMock: func(_ int, data []byte) error {
|
||||
// mock connection WriteMessage() no error
|
||||
// emulate that receiver gets the message written
|
||||
var t madmin.HealthInfo
|
||||
_ = json.Unmarshal(data, &t)
|
||||
return errors.New("error on write")
|
||||
},
|
||||
},
|
||||
wantError: errors.New("error on write"),
|
||||
},
|
||||
{
|
||||
test: "Handle error on health function",
|
||||
args: args{
|
||||
deadline: deadlineDuration,
|
||||
mockMessages: []madmin.HealthInfo{
|
||||
{
|
||||
Error: "error on healthInfo",
|
||||
},
|
||||
},
|
||||
wsWriteMock: func(_ int, data []byte) error {
|
||||
// mock connection WriteMessage() no error
|
||||
// emulate that receiver gets the message written
|
||||
var t madmin.HealthInfo
|
||||
_ = json.Unmarshal(data, &t)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
wantError: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.test, func(_ *testing.T) {
|
||||
// make testReceiver channel
|
||||
testReceiver = make(chan madmin.HealthInfo, len(tt.args.mockMessages))
|
||||
// mock function same for all tests, changes mockMessages
|
||||
minioServerHealthInfoMock = func(_ context.Context,
|
||||
_ time.Duration,
|
||||
) (interface{}, string, error) {
|
||||
info := tt.args.mockMessages[0]
|
||||
return info, madmin.HealthInfoVersion, nil
|
||||
}
|
||||
connWriteMessageMock = tt.args.wsWriteMock
|
||||
err := startHealthInfo(ctx, mockWSConn, client, &deadlineDuration)
|
||||
// close test mock channel
|
||||
close(testReceiver)
|
||||
// check that the TestReceiver got the same number of data from Console.
|
||||
index := 0
|
||||
for info := range testReceiver {
|
||||
if !reflect.DeepEqual(info, tt.args.mockMessages[index]) {
|
||||
t.Errorf("startHealthInfo() got: %v, want: %v", info, tt.args.mockMessages[index])
|
||||
return
|
||||
}
|
||||
index++
|
||||
}
|
||||
if !reflect.DeepEqual(err, tt.wantError) {
|
||||
t.Errorf("startHealthInfo() error: %v, wantError: %v", err, tt.wantError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
290
api/admin_idp.go
Normal file
290
api/admin_idp.go
Normal file
@@ -0,0 +1,290 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
"github.com/minio/console/api/operations/idp"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
var errInvalidIDPType = fmt.Errorf("IDP type must be one of %v", madmin.ValidIDPConfigTypes)
|
||||
|
||||
func registerIDPHandlers(api *operations.ConsoleAPI) {
|
||||
api.IdpCreateConfigurationHandler = idp.CreateConfigurationHandlerFunc(func(params idp.CreateConfigurationParams, session *models.Principal) middleware.Responder {
|
||||
response, err := createIDPConfigurationResponse(session, params)
|
||||
if err != nil {
|
||||
return idp.NewCreateConfigurationDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return idp.NewCreateConfigurationCreated().WithPayload(response)
|
||||
})
|
||||
api.IdpUpdateConfigurationHandler = idp.UpdateConfigurationHandlerFunc(func(params idp.UpdateConfigurationParams, session *models.Principal) middleware.Responder {
|
||||
response, err := updateIDPConfigurationResponse(session, params)
|
||||
if err != nil {
|
||||
return idp.NewUpdateConfigurationDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return idp.NewUpdateConfigurationOK().WithPayload(response)
|
||||
})
|
||||
api.IdpListConfigurationsHandler = idp.ListConfigurationsHandlerFunc(func(params idp.ListConfigurationsParams, session *models.Principal) middleware.Responder {
|
||||
response, err := listIDPConfigurationsResponse(session, params)
|
||||
if err != nil {
|
||||
return idp.NewListConfigurationsDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return idp.NewListConfigurationsOK().WithPayload(response)
|
||||
})
|
||||
api.IdpDeleteConfigurationHandler = idp.DeleteConfigurationHandlerFunc(func(params idp.DeleteConfigurationParams, session *models.Principal) middleware.Responder {
|
||||
response, err := deleteIDPConfigurationResponse(session, params)
|
||||
if err != nil {
|
||||
return idp.NewDeleteConfigurationDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return idp.NewDeleteConfigurationOK().WithPayload(response)
|
||||
})
|
||||
api.IdpGetConfigurationHandler = idp.GetConfigurationHandlerFunc(func(params idp.GetConfigurationParams, session *models.Principal) middleware.Responder {
|
||||
response, err := getIDPConfigurationsResponse(session, params)
|
||||
if err != nil {
|
||||
return idp.NewGetConfigurationDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return idp.NewGetConfigurationOK().WithPayload(response)
|
||||
})
|
||||
api.IdpGetLDAPEntitiesHandler = idp.GetLDAPEntitiesHandlerFunc(func(params idp.GetLDAPEntitiesParams, session *models.Principal) middleware.Responder {
|
||||
response, err := getLDAPEntitiesResponse(session, params)
|
||||
if err != nil {
|
||||
return idp.NewGetLDAPEntitiesDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return idp.NewGetLDAPEntitiesOK().WithPayload(response)
|
||||
})
|
||||
}
|
||||
|
||||
func createIDPConfigurationResponse(session *models.Principal, params idp.CreateConfigurationParams) (*models.SetIDPResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
restart, err := createOrUpdateIDPConfig(ctx, params.Type, params.Body.Name, params.Body.Input, false, AdminClient{Client: mAdmin})
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.SetIDPResponse{Restart: restart}, nil
|
||||
}
|
||||
|
||||
func updateIDPConfigurationResponse(session *models.Principal, params idp.UpdateConfigurationParams) (*models.SetIDPResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
restart, err := createOrUpdateIDPConfig(ctx, params.Type, params.Name, params.Body.Input, true, AdminClient{Client: mAdmin})
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.SetIDPResponse{Restart: restart}, nil
|
||||
}
|
||||
|
||||
func createOrUpdateIDPConfig(ctx context.Context, idpType, name, input string, update bool, client MinioAdmin) (bool, error) {
|
||||
if !madmin.ValidIDPConfigTypes.Contains(idpType) {
|
||||
return false, errInvalidIDPType
|
||||
}
|
||||
restart, err := client.addOrUpdateIDPConfig(ctx, idpType, name, input, update)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return restart, nil
|
||||
}
|
||||
|
||||
func listIDPConfigurationsResponse(session *models.Principal, params idp.ListConfigurationsParams) (*models.IdpListConfigurationsResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
results, err := listIDPConfigurations(ctx, params.Type, AdminClient{Client: mAdmin})
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.IdpListConfigurationsResponse{Results: results}, nil
|
||||
}
|
||||
|
||||
func listIDPConfigurations(ctx context.Context, idpType string, client MinioAdmin) ([]*models.IdpServerConfiguration, error) {
|
||||
if !madmin.ValidIDPConfigTypes.Contains(idpType) {
|
||||
return nil, errInvalidIDPType
|
||||
}
|
||||
results, err := client.listIDPConfig(ctx, idpType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseIDPConfigurations(results), nil
|
||||
}
|
||||
|
||||
func parseIDPConfigurations(configs []madmin.IDPListItem) (serverConfigs []*models.IdpServerConfiguration) {
|
||||
for _, c := range configs {
|
||||
serverConfigs = append(serverConfigs, &models.IdpServerConfiguration{
|
||||
Name: c.Name,
|
||||
Enabled: c.Enabled,
|
||||
Type: c.Type,
|
||||
})
|
||||
}
|
||||
return serverConfigs
|
||||
}
|
||||
|
||||
func deleteIDPConfigurationResponse(session *models.Principal, params idp.DeleteConfigurationParams) (*models.SetIDPResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
restart, err := deleteIDPConfig(ctx, params.Type, params.Name, AdminClient{Client: mAdmin})
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.SetIDPResponse{Restart: restart}, nil
|
||||
}
|
||||
|
||||
func deleteIDPConfig(ctx context.Context, idpType, name string, client MinioAdmin) (bool, error) {
|
||||
if !madmin.ValidIDPConfigTypes.Contains(idpType) {
|
||||
return false, errInvalidIDPType
|
||||
}
|
||||
restart, err := client.deleteIDPConfig(ctx, idpType, name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return restart, nil
|
||||
}
|
||||
|
||||
func getIDPConfigurationsResponse(session *models.Principal, params idp.GetConfigurationParams) (*models.IdpServerConfiguration, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
result, err := getIDPConfiguration(ctx, params.Type, params.Name, AdminClient{Client: mAdmin})
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getIDPConfiguration(ctx context.Context, idpType, name string, client MinioAdmin) (*models.IdpServerConfiguration, error) {
|
||||
if !madmin.ValidIDPConfigTypes.Contains(idpType) {
|
||||
return nil, errInvalidIDPType
|
||||
}
|
||||
config, err := client.getIDPConfig(ctx, idpType, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.IdpServerConfiguration{
|
||||
Name: config.Name,
|
||||
Type: config.Type,
|
||||
Info: parseIDPConfigurationsInfo(config.Info),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseIDPConfigurationsInfo(infoList []madmin.IDPCfgInfo) (results []*models.IdpServerConfigurationInfo) {
|
||||
for _, info := range infoList {
|
||||
results = append(results, &models.IdpServerConfigurationInfo{
|
||||
Key: info.Key,
|
||||
Value: info.Value,
|
||||
IsCfg: info.IsCfg,
|
||||
IsEnv: info.IsEnv,
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func getLDAPEntitiesResponse(session *models.Principal, params idp.GetLDAPEntitiesParams) (*models.LdapEntities, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
result, err := getEntitiesResult(ctx, AdminClient{Client: mAdmin}, params.Body.Users, params.Body.Groups, params.Body.Policies)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getEntitiesResult(ctx context.Context, client MinioAdmin, users, groups, policies []string) (*models.LdapEntities, error) {
|
||||
entities, err := client.getLDAPPolicyEntities(ctx, madmin.PolicyEntitiesQuery{
|
||||
Users: users,
|
||||
Groups: groups,
|
||||
Policy: policies,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result models.LdapEntities
|
||||
|
||||
var usersEntity []*models.LdapUserPolicyEntity
|
||||
var groupsEntity []*models.LdapGroupPolicyEntity
|
||||
var policiesEntity []*models.LdapPolicyEntity
|
||||
|
||||
result.Timestamp = entities.Timestamp.Format(time.RFC3339)
|
||||
|
||||
for _, userMapping := range entities.UserMappings {
|
||||
mapItem := models.LdapUserPolicyEntity{
|
||||
User: userMapping.User,
|
||||
Policies: userMapping.Policies,
|
||||
}
|
||||
|
||||
usersEntity = append(usersEntity, &mapItem)
|
||||
}
|
||||
|
||||
result.Users = usersEntity
|
||||
|
||||
for _, groupsMapping := range entities.GroupMappings {
|
||||
mapItem := models.LdapGroupPolicyEntity{
|
||||
Group: groupsMapping.Group,
|
||||
Policies: groupsMapping.Policies,
|
||||
}
|
||||
|
||||
groupsEntity = append(groupsEntity, &mapItem)
|
||||
}
|
||||
|
||||
result.Groups = groupsEntity
|
||||
|
||||
for _, policyMapping := range entities.PolicyMappings {
|
||||
mapItem := models.LdapPolicyEntity{
|
||||
Policy: policyMapping.Policy,
|
||||
Users: policyMapping.Users,
|
||||
Groups: policyMapping.Groups,
|
||||
}
|
||||
|
||||
policiesEntity = append(policiesEntity, &mapItem)
|
||||
}
|
||||
|
||||
result.Policies = policiesEntity
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
319
api/admin_idp_test.go
Normal file
319
api/admin_idp_test.go
Normal file
@@ -0,0 +1,319 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
|
||||
"github.com/minio/console/api/operations"
|
||||
"github.com/minio/console/api/operations/idp"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type IDPTestSuite struct {
|
||||
suite.Suite
|
||||
assert *assert.Assertions
|
||||
currentServer string
|
||||
isServerSet bool
|
||||
server *httptest.Server
|
||||
adminClient AdminClientMock
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) SetupSuite() {
|
||||
suite.assert = assert.New(suite.T())
|
||||
suite.adminClient = AdminClientMock{}
|
||||
minioServiceRestartMock = func(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) SetupTest() {
|
||||
suite.server = httptest.NewServer(http.HandlerFunc(suite.serverHandler))
|
||||
suite.currentServer, suite.isServerSet = os.LookupEnv(ConsoleMinIOServer)
|
||||
os.Setenv(ConsoleMinIOServer, suite.server.URL)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) serverHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(400)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TearDownSuite() {
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TearDownTest() {
|
||||
if suite.isServerSet {
|
||||
os.Setenv(ConsoleMinIOServer, suite.currentServer)
|
||||
} else {
|
||||
os.Unsetenv(ConsoleMinIOServer)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestRegisterIDPHandlers() {
|
||||
api := &operations.ConsoleAPI{}
|
||||
suite.assertHandlersAreNil(api)
|
||||
registerIDPHandlers(api)
|
||||
suite.assertHandlersAreNotNil(api)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) assertHandlersAreNil(api *operations.ConsoleAPI) {
|
||||
suite.assert.Nil(api.IdpCreateConfigurationHandler)
|
||||
suite.assert.Nil(api.IdpListConfigurationsHandler)
|
||||
suite.assert.Nil(api.IdpUpdateConfigurationHandler)
|
||||
suite.assert.Nil(api.IdpGetConfigurationHandler)
|
||||
suite.assert.Nil(api.IdpGetConfigurationHandler)
|
||||
suite.assert.Nil(api.IdpDeleteConfigurationHandler)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) assertHandlersAreNotNil(api *operations.ConsoleAPI) {
|
||||
suite.assert.NotNil(api.IdpCreateConfigurationHandler)
|
||||
suite.assert.NotNil(api.IdpListConfigurationsHandler)
|
||||
suite.assert.NotNil(api.IdpUpdateConfigurationHandler)
|
||||
suite.assert.NotNil(api.IdpGetConfigurationHandler)
|
||||
suite.assert.NotNil(api.IdpGetConfigurationHandler)
|
||||
suite.assert.NotNil(api.IdpDeleteConfigurationHandler)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestCreateIDPConfigurationHandlerWithError() {
|
||||
params, api := suite.initCreateIDPConfigurationRequest()
|
||||
response := api.IdpCreateConfigurationHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*idp.CreateConfigurationDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) initCreateIDPConfigurationRequest() (params idp.CreateConfigurationParams, api operations.ConsoleAPI) {
|
||||
registerIDPHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
params.Body = &models.IdpServerConfiguration{}
|
||||
params.Type = "ldap"
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestCreateIDPConfigurationWithoutError() {
|
||||
ctx := context.Background()
|
||||
_, err := createOrUpdateIDPConfig(ctx, "ldap", "", "", false, suite.adminClient)
|
||||
suite.assert.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestCreateIDPConfigurationWithWrongType() {
|
||||
ctx := context.Background()
|
||||
_, err := createOrUpdateIDPConfig(ctx, "", "", "", false, suite.adminClient)
|
||||
suite.assert.NotNil(err)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestUpdateIDPConfigurationHandlerWithError() {
|
||||
params, api := suite.initUpdateIDPConfigurationRequest()
|
||||
response := api.IdpUpdateConfigurationHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*idp.UpdateConfigurationDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) initUpdateIDPConfigurationRequest() (params idp.UpdateConfigurationParams, api operations.ConsoleAPI) {
|
||||
registerIDPHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
params.Body = &models.IdpServerConfiguration{}
|
||||
params.Type = "ldap"
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestUpdateIDPConfigurationWithoutError() {
|
||||
ctx := context.Background()
|
||||
_, err := createOrUpdateIDPConfig(ctx, "ldap", "", "", true, suite.adminClient)
|
||||
suite.assert.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestUpdateIDPConfigurationWithWrongType() {
|
||||
ctx := context.Background()
|
||||
_, err := createOrUpdateIDPConfig(ctx, "", "", "", true, suite.adminClient)
|
||||
suite.assert.NotNil(err)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestListIDPConfigurationHandlerWithError() {
|
||||
params, api := suite.initListIDPConfigurationsRequest()
|
||||
response := api.IdpListConfigurationsHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*idp.ListConfigurationsDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) initListIDPConfigurationsRequest() (params idp.ListConfigurationsParams, api operations.ConsoleAPI) {
|
||||
registerIDPHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
params.Type = "ldap"
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestListIDPConfigurationsWithoutError() {
|
||||
ctx := context.Background()
|
||||
res, err := listIDPConfigurations(ctx, "ldap", suite.adminClient)
|
||||
suite.assert.NotNil(res)
|
||||
suite.assert.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestListIDPConfigurationsWithWrongType() {
|
||||
ctx := context.Background()
|
||||
res, err := listIDPConfigurations(ctx, "", suite.adminClient)
|
||||
suite.assert.Nil(res)
|
||||
suite.assert.NotNil(err)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestDeleteIDPConfigurationHandlerWithError() {
|
||||
params, api := suite.initDeleteIDPConfigurationRequest()
|
||||
response := api.IdpDeleteConfigurationHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*idp.DeleteConfigurationDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) initDeleteIDPConfigurationRequest() (params idp.DeleteConfigurationParams, api operations.ConsoleAPI) {
|
||||
registerIDPHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
params.Type = "ldap"
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestDeleteIDPConfigurationWithoutError() {
|
||||
ctx := context.Background()
|
||||
_, err := deleteIDPConfig(ctx, "ldap", "", suite.adminClient)
|
||||
suite.assert.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestDeleteIDPConfigurationWithWrongType() {
|
||||
ctx := context.Background()
|
||||
_, err := deleteIDPConfig(ctx, "", "", suite.adminClient)
|
||||
suite.assert.NotNil(err)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestGetIDPConfigurationHandlerWithError() {
|
||||
params, api := suite.initGetIDPConfigurationRequest()
|
||||
response := api.IdpGetConfigurationHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*idp.GetConfigurationDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) initGetIDPConfigurationRequest() (params idp.GetConfigurationParams, api operations.ConsoleAPI) {
|
||||
registerIDPHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
params.Type = "ldap"
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestGetIDPConfigurationWithoutError() {
|
||||
ctx := context.Background()
|
||||
res, err := getIDPConfiguration(ctx, "ldap", "", suite.adminClient)
|
||||
suite.assert.NotNil(res)
|
||||
suite.assert.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *IDPTestSuite) TestGetIDPConfigurationWithWrongType() {
|
||||
ctx := context.Background()
|
||||
res, err := getIDPConfiguration(ctx, "", "", suite.adminClient)
|
||||
suite.assert.Nil(res)
|
||||
suite.assert.NotNil(err)
|
||||
}
|
||||
|
||||
func TestIDP(t *testing.T) {
|
||||
suite.Run(t, new(IDPTestSuite))
|
||||
}
|
||||
|
||||
func TestGetEntitiesResult(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// mock minIO client
|
||||
client := AdminClientMock{}
|
||||
function := "getEntitiesResult()"
|
||||
|
||||
usersList := []string{"user1", "user2", "user3"}
|
||||
policiesList := []string{"policy1", "policy2", "policy3"}
|
||||
groupsList := []string{"group1", "group3", "group5"}
|
||||
|
||||
policyMap := []madmin.PolicyEntities{
|
||||
{Policy: "testPolicy0", Groups: groupsList, Users: usersList},
|
||||
{Policy: "testPolicy1", Groups: groupsList, Users: usersList},
|
||||
}
|
||||
|
||||
usersMap := []madmin.UserPolicyEntities{
|
||||
{User: "testUser0", Policies: policiesList},
|
||||
{User: "testUser1", Policies: policiesList},
|
||||
}
|
||||
|
||||
groupsMap := []madmin.GroupPolicyEntities{
|
||||
{Group: "group0", Policies: policiesList},
|
||||
{Group: "group1", Policies: policiesList},
|
||||
}
|
||||
|
||||
// Test-1: getEntitiesResult list all information provided
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
mockResponse := madmin.PolicyEntitiesResult{
|
||||
PolicyMappings: policyMap,
|
||||
GroupMappings: groupsMap,
|
||||
UserMappings: usersMap,
|
||||
}
|
||||
minioGetLDAPPolicyEntitiesMock = func(_ context.Context, _ madmin.PolicyEntitiesQuery) (madmin.PolicyEntitiesResult, error) {
|
||||
return mockResponse, nil
|
||||
}
|
||||
|
||||
entities, err := getEntitiesResult(ctx, client, usersList, groupsList, policiesList)
|
||||
if err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
|
||||
for i, groupIt := range entities.Groups {
|
||||
assert.Equal(fmt.Sprintf("group%d", i), groupIt.Group)
|
||||
|
||||
for i, polItm := range groupIt.Policies {
|
||||
assert.Equal(policiesList[i], polItm)
|
||||
}
|
||||
}
|
||||
|
||||
for i, usrIt := range entities.Users {
|
||||
assert.Equal(fmt.Sprintf("testUser%d", i), usrIt.User)
|
||||
|
||||
for i, polItm := range usrIt.Policies {
|
||||
assert.Equal(policiesList[i], polItm)
|
||||
}
|
||||
}
|
||||
|
||||
for i, policyIt := range entities.Policies {
|
||||
assert.Equal(fmt.Sprintf("testPolicy%d", i), policyIt.Policy)
|
||||
|
||||
for i, userItm := range policyIt.Users {
|
||||
assert.Equal(usersList[i], userItm)
|
||||
}
|
||||
|
||||
for i, grItm := range policyIt.Groups {
|
||||
assert.Equal(groupsList[i], grItm)
|
||||
}
|
||||
}
|
||||
|
||||
// Test-2: getEntitiesResult error is returned from getLDAPPolicyEntities()
|
||||
minioGetLDAPPolicyEntitiesMock = func(_ context.Context, _ madmin.PolicyEntitiesQuery) (madmin.PolicyEntitiesResult, error) {
|
||||
return madmin.PolicyEntitiesResult{}, errors.New("error")
|
||||
}
|
||||
|
||||
_, err = getEntitiesResult(ctx, client, usersList, groupsList, policiesList)
|
||||
if assert.Error(err) {
|
||||
assert.Equal("error", err.Error())
|
||||
}
|
||||
}
|
||||
1192
api/admin_info.go
Normal file
1192
api/admin_info.go
Normal file
File diff suppressed because it is too large
Load Diff
152
api/admin_info_test.go
Normal file
152
api/admin_info_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
|
||||
"github.com/minio/console/api/operations"
|
||||
systemApi "github.com/minio/console/api/operations/system"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type AdminInfoTestSuite struct {
|
||||
suite.Suite
|
||||
assert *assert.Assertions
|
||||
currentServer string
|
||||
isServerSet bool
|
||||
isPrometheusRequest bool
|
||||
server *httptest.Server
|
||||
adminClient AdminClientMock
|
||||
}
|
||||
|
||||
func (suite *AdminInfoTestSuite) SetupSuite() {
|
||||
suite.assert = assert.New(suite.T())
|
||||
suite.adminClient = AdminClientMock{}
|
||||
MinioServerInfoMock = func(_ context.Context) (madmin.InfoMessage, error) {
|
||||
return madmin.InfoMessage{
|
||||
Servers: []madmin.ServerProperties{{
|
||||
Disks: []madmin.Disk{{}},
|
||||
}},
|
||||
Backend: madmin.ErasureBackend{Type: "mock"},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *AdminInfoTestSuite) SetupTest() {
|
||||
suite.server = httptest.NewServer(http.HandlerFunc(suite.serverHandler))
|
||||
suite.currentServer, suite.isServerSet = os.LookupEnv(ConsoleMinIOServer)
|
||||
os.Setenv(ConsoleMinIOServer, suite.server.URL)
|
||||
}
|
||||
|
||||
func (suite *AdminInfoTestSuite) serverHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
if suite.isPrometheusRequest {
|
||||
w.WriteHeader(200)
|
||||
} else {
|
||||
w.WriteHeader(400)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *AdminInfoTestSuite) TearDownSuite() {
|
||||
}
|
||||
|
||||
func (suite *AdminInfoTestSuite) TearDownTest() {
|
||||
if suite.isServerSet {
|
||||
os.Setenv(ConsoleMinIOServer, suite.currentServer)
|
||||
} else {
|
||||
os.Unsetenv(ConsoleMinIOServer)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *AdminInfoTestSuite) TestRegisterAdminInfoHandlers() {
|
||||
api := &operations.ConsoleAPI{}
|
||||
suite.assertHandlersAreNil(api)
|
||||
registerAdminInfoHandlers(api)
|
||||
suite.assertHandlersAreNotNil(api)
|
||||
}
|
||||
|
||||
func (suite *AdminInfoTestSuite) assertHandlersAreNil(api *operations.ConsoleAPI) {
|
||||
suite.assert.Nil(api.SystemAdminInfoHandler)
|
||||
suite.assert.Nil(api.SystemDashboardWidgetDetailsHandler)
|
||||
}
|
||||
|
||||
func (suite *AdminInfoTestSuite) assertHandlersAreNotNil(api *operations.ConsoleAPI) {
|
||||
suite.assert.NotNil(api.SystemAdminInfoHandler)
|
||||
suite.assert.NotNil(api.SystemDashboardWidgetDetailsHandler)
|
||||
}
|
||||
|
||||
func (suite *AdminInfoTestSuite) TestSystemAdminInfoHandlerWithError() {
|
||||
params, api := suite.initSystemAdminInfoRequest()
|
||||
response := api.SystemAdminInfoHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*systemApi.AdminInfoDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *AdminInfoTestSuite) initSystemAdminInfoRequest() (params systemApi.AdminInfoParams, api operations.ConsoleAPI) {
|
||||
registerAdminInfoHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
defaultOnly := false
|
||||
params.DefaultOnly = &defaultOnly
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *AdminInfoTestSuite) TestSystemDashboardWidgetDetailsHandlerWithError() {
|
||||
params, api := suite.initSystemDashboardWidgetDetailsRequest()
|
||||
response := api.SystemDashboardWidgetDetailsHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*systemApi.DashboardWidgetDetailsDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *AdminInfoTestSuite) initSystemDashboardWidgetDetailsRequest() (params systemApi.DashboardWidgetDetailsParams, api operations.ConsoleAPI) {
|
||||
registerAdminInfoHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *AdminInfoTestSuite) TestGetUsageWidgetsForDeploymentWithoutError() {
|
||||
ctx := context.WithValue(context.Background(), utils.ContextClientIP, "127.0.0.1")
|
||||
suite.isPrometheusRequest = true
|
||||
res, err := getUsageWidgetsForDeployment(ctx, suite.server.URL, suite.adminClient)
|
||||
suite.assert.Nil(err)
|
||||
suite.assert.NotNil(res)
|
||||
suite.isPrometheusRequest = false
|
||||
}
|
||||
|
||||
func (suite *AdminInfoTestSuite) TestGetWidgetDetailsWithoutError() {
|
||||
ctx := context.WithValue(context.Background(), utils.ContextClientIP, "127.0.0.1")
|
||||
suite.isPrometheusRequest = true
|
||||
var step int32 = 1
|
||||
var start int64
|
||||
var end int64 = 1
|
||||
res, err := getWidgetDetails(ctx, suite.server.URL, "mock", 1, &step, &start, &end)
|
||||
suite.assert.Nil(err)
|
||||
suite.assert.NotNil(res)
|
||||
suite.isPrometheusRequest = false
|
||||
}
|
||||
|
||||
func TestAdminInfo(t *testing.T) {
|
||||
suite.Run(t, new(AdminInfoTestSuite))
|
||||
}
|
||||
117
api/admin_inspect.go
Normal file
117
api/admin_inspect.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
inspectApi "github.com/minio/console/api/operations/inspect"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/secure-io/sio-go"
|
||||
)
|
||||
|
||||
func registerInspectHandler(api *operations.ConsoleAPI) {
|
||||
api.InspectInspectHandler = inspectApi.InspectHandlerFunc(func(params inspectApi.InspectParams, principal *models.Principal) middleware.Responder {
|
||||
k, r, err := getInspectResult(principal, ¶ms)
|
||||
if err != nil {
|
||||
return inspectApi.NewInspectDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
|
||||
return middleware.ResponderFunc(processInspectResponse(¶ms, k, r))
|
||||
})
|
||||
}
|
||||
|
||||
func getInspectResult(session *models.Principal, params *inspectApi.InspectParams) ([]byte, io.ReadCloser, *CodedAPIError) {
|
||||
ctx := params.HTTPRequest.Context()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
cfg := madmin.InspectOptions{
|
||||
File: params.File,
|
||||
Volume: params.Volume,
|
||||
}
|
||||
|
||||
// TODO: Remove encryption option and always encrypt.
|
||||
// Maybe also add public key field.
|
||||
if params.Encrypt != nil && *params.Encrypt {
|
||||
cfg.PublicKey, _ = base64.StdEncoding.DecodeString("MIIBCgKCAQEAs/128UFS9A8YSJY1XqYKt06dLVQQCGDee69T+0Tip/1jGAB4z0/3QMpH0MiS8Wjs4BRWV51qvkfAHzwwdU7y6jxU05ctb/H/WzRj3FYdhhHKdzear9TLJftlTs+xwj2XaADjbLXCV1jGLS889A7f7z5DgABlVZMQd9BjVAR8ED3xRJ2/ZCNuQVJ+A8r7TYPGMY3wWvhhPgPk3Lx4WDZxDiDNlFs4GQSaESSsiVTb9vyGe/94CsCTM6Cw9QG6ifHKCa/rFszPYdKCabAfHcS3eTr0GM+TThSsxO7KfuscbmLJkfQev1srfL2Ii2RbnysqIJVWKEwdW05ID8ryPkuTuwIDAQAB")
|
||||
}
|
||||
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
k, r, err := adminClient.inspect(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return k, r, nil
|
||||
}
|
||||
|
||||
// borrowed from mc cli
|
||||
func decryptInspectV1(key [32]byte, r io.Reader) io.ReadCloser {
|
||||
stream, err := sio.AES_256_GCM.Stream(key[:])
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
nonce := make([]byte, stream.NonceSize())
|
||||
return io.NopCloser(stream.DecryptReader(r, nonce, nil))
|
||||
}
|
||||
|
||||
func processInspectResponse(params *inspectApi.InspectParams, k []byte, r io.ReadCloser) func(w http.ResponseWriter, _ runtime.Producer) {
|
||||
isEnc := params.Encrypt != nil && *params.Encrypt
|
||||
return func(w http.ResponseWriter, _ runtime.Producer) {
|
||||
ext := "enc"
|
||||
if len(k) == 32 && !isEnc {
|
||||
ext = "zip"
|
||||
r = decryptInspectV1(*(*[32]byte)(k), r)
|
||||
}
|
||||
fileName := fmt.Sprintf("inspect-%s-%s.%s", params.Volume, params.File, ext)
|
||||
fileName = strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
case r >= 'A' && r <= 'Z':
|
||||
return r
|
||||
case r >= 'a' && r <= 'z':
|
||||
return r
|
||||
case r >= '0' && r <= '9':
|
||||
return r
|
||||
default:
|
||||
if strings.ContainsAny(string(r), "-+._") {
|
||||
return r
|
||||
}
|
||||
return '_'
|
||||
}
|
||||
}, fileName)
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
|
||||
|
||||
_, err := io.Copy(w, r)
|
||||
if err != nil {
|
||||
LogError("unable to write all the data: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
291
api/admin_kms.go
Normal file
291
api/admin_kms.go
Normal file
@@ -0,0 +1,291 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
kmsAPI "github.com/minio/console/api/operations/k_m_s"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
func registerKMSHandlers(api *operations.ConsoleAPI) {
|
||||
registerKMSStatusHandlers(api)
|
||||
registerKMSKeyHandlers(api)
|
||||
}
|
||||
|
||||
func registerKMSStatusHandlers(api *operations.ConsoleAPI) {
|
||||
api.KmsKMSStatusHandler = kmsAPI.KMSStatusHandlerFunc(func(params kmsAPI.KMSStatusParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := GetKMSStatusResponse(session, params)
|
||||
if err != nil {
|
||||
return kmsAPI.NewKMSStatusDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return kmsAPI.NewKMSStatusOK().WithPayload(resp)
|
||||
})
|
||||
|
||||
api.KmsKMSMetricsHandler = kmsAPI.KMSMetricsHandlerFunc(func(params kmsAPI.KMSMetricsParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := GetKMSMetricsResponse(session, params)
|
||||
if err != nil {
|
||||
return kmsAPI.NewKMSMetricsDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return kmsAPI.NewKMSMetricsOK().WithPayload(resp)
|
||||
})
|
||||
|
||||
api.KmsKMSAPIsHandler = kmsAPI.KMSAPIsHandlerFunc(func(params kmsAPI.KMSAPIsParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := GetKMSAPIsResponse(session, params)
|
||||
if err != nil {
|
||||
return kmsAPI.NewKMSAPIsDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return kmsAPI.NewKMSAPIsOK().WithPayload(resp)
|
||||
})
|
||||
|
||||
api.KmsKMSVersionHandler = kmsAPI.KMSVersionHandlerFunc(func(params kmsAPI.KMSVersionParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := GetKMSVersionResponse(session, params)
|
||||
if err != nil {
|
||||
return kmsAPI.NewKMSVersionDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return kmsAPI.NewKMSVersionOK().WithPayload(resp)
|
||||
})
|
||||
}
|
||||
|
||||
func GetKMSStatusResponse(session *models.Principal, params kmsAPI.KMSStatusParams) (*models.KmsStatusResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return kmsStatus(ctx, AdminClient{Client: mAdmin})
|
||||
}
|
||||
|
||||
func kmsStatus(ctx context.Context, minioClient MinioAdmin) (*models.KmsStatusResponse, *CodedAPIError) {
|
||||
st, err := minioClient.kmsStatus(ctx)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.KmsStatusResponse{
|
||||
DefaultKeyID: st.DefaultKeyID,
|
||||
Name: st.Name,
|
||||
Endpoints: parseStatusEndpoints(st.Endpoints),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseStatusEndpoints(endpoints map[string]madmin.ItemState) (kmsEndpoints []*models.KmsEndpoint) {
|
||||
for key, value := range endpoints {
|
||||
kmsEndpoints = append(kmsEndpoints, &models.KmsEndpoint{URL: key, Status: string(value)})
|
||||
}
|
||||
return kmsEndpoints
|
||||
}
|
||||
|
||||
func GetKMSMetricsResponse(session *models.Principal, params kmsAPI.KMSMetricsParams) (*models.KmsMetricsResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return kmsMetrics(ctx, AdminClient{Client: mAdmin})
|
||||
}
|
||||
|
||||
func kmsMetrics(ctx context.Context, minioClient MinioAdmin) (*models.KmsMetricsResponse, *CodedAPIError) {
|
||||
metrics, err := minioClient.kmsMetrics(ctx)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.KmsMetricsResponse{
|
||||
RequestOK: &metrics.RequestOK,
|
||||
RequestErr: &metrics.RequestErr,
|
||||
RequestFail: &metrics.RequestFail,
|
||||
RequestActive: &metrics.RequestActive,
|
||||
AuditEvents: &metrics.AuditEvents,
|
||||
ErrorEvents: &metrics.ErrorEvents,
|
||||
LatencyHistogram: parseHistogram(metrics.LatencyHistogram),
|
||||
Uptime: &metrics.UpTime,
|
||||
Cpus: &metrics.CPUs,
|
||||
UsableCPUs: &metrics.UsableCPUs,
|
||||
Threads: &metrics.Threads,
|
||||
HeapAlloc: &metrics.HeapAlloc,
|
||||
HeapObjects: metrics.HeapObjects,
|
||||
StackAlloc: &metrics.StackAlloc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseHistogram(histogram map[int64]int64) (records []*models.KmsLatencyHistogram) {
|
||||
for duration, total := range histogram {
|
||||
records = append(records, &models.KmsLatencyHistogram{Duration: duration, Total: total})
|
||||
}
|
||||
cp := func(i, j int) bool {
|
||||
return records[i].Duration < records[j].Duration
|
||||
}
|
||||
sort.Slice(records, cp)
|
||||
return records
|
||||
}
|
||||
|
||||
func GetKMSAPIsResponse(session *models.Principal, params kmsAPI.KMSAPIsParams) (*models.KmsAPIsResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return kmsAPIs(ctx, AdminClient{Client: mAdmin})
|
||||
}
|
||||
|
||||
func kmsAPIs(ctx context.Context, minioClient MinioAdmin) (*models.KmsAPIsResponse, *CodedAPIError) {
|
||||
apis, err := minioClient.kmsAPIs(ctx)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.KmsAPIsResponse{
|
||||
Results: parseApis(apis),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseApis(apis []madmin.KMSAPI) (data []*models.KmsAPI) {
|
||||
for _, api := range apis {
|
||||
data = append(data, &models.KmsAPI{
|
||||
Method: api.Method,
|
||||
Path: api.Path,
|
||||
MaxBody: api.MaxBody,
|
||||
Timeout: api.Timeout,
|
||||
})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func GetKMSVersionResponse(session *models.Principal, params kmsAPI.KMSVersionParams) (*models.KmsVersionResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return kmsVersion(ctx, AdminClient{Client: mAdmin})
|
||||
}
|
||||
|
||||
func kmsVersion(ctx context.Context, minioClient MinioAdmin) (*models.KmsVersionResponse, *CodedAPIError) {
|
||||
version, err := minioClient.kmsVersion(ctx)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.KmsVersionResponse{
|
||||
Version: version.Version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func registerKMSKeyHandlers(api *operations.ConsoleAPI) {
|
||||
api.KmsKMSCreateKeyHandler = kmsAPI.KMSCreateKeyHandlerFunc(func(params kmsAPI.KMSCreateKeyParams, session *models.Principal) middleware.Responder {
|
||||
err := GetKMSCreateKeyResponse(session, params)
|
||||
if err != nil {
|
||||
return kmsAPI.NewKMSCreateKeyDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return kmsAPI.NewKMSCreateKeyCreated()
|
||||
})
|
||||
|
||||
api.KmsKMSListKeysHandler = kmsAPI.KMSListKeysHandlerFunc(func(params kmsAPI.KMSListKeysParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := GetKMSListKeysResponse(session, params)
|
||||
if err != nil {
|
||||
return kmsAPI.NewKMSListKeysDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return kmsAPI.NewKMSListKeysOK().WithPayload(resp)
|
||||
})
|
||||
|
||||
api.KmsKMSKeyStatusHandler = kmsAPI.KMSKeyStatusHandlerFunc(func(params kmsAPI.KMSKeyStatusParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := GetKMSKeyStatusResponse(session, params)
|
||||
if err != nil {
|
||||
return kmsAPI.NewKMSKeyStatusDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return kmsAPI.NewKMSKeyStatusOK().WithPayload(resp)
|
||||
})
|
||||
}
|
||||
|
||||
func GetKMSCreateKeyResponse(session *models.Principal, params kmsAPI.KMSCreateKeyParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return createKey(ctx, *params.Body.Key, AdminClient{Client: mAdmin})
|
||||
}
|
||||
|
||||
func createKey(ctx context.Context, key string, minioClient MinioAdmin) *CodedAPIError {
|
||||
if err := minioClient.createKey(ctx, key); err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetKMSListKeysResponse(session *models.Principal, params kmsAPI.KMSListKeysParams) (*models.KmsListKeysResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
pattern := ""
|
||||
if params.Pattern != nil {
|
||||
pattern = *params.Pattern
|
||||
}
|
||||
return listKeys(ctx, pattern, AdminClient{Client: mAdmin})
|
||||
}
|
||||
|
||||
func listKeys(ctx context.Context, pattern string, minioClient MinioAdmin) (*models.KmsListKeysResponse, *CodedAPIError) {
|
||||
results, err := minioClient.listKeys(ctx, pattern)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.KmsListKeysResponse{Results: parseKeys(results)}, nil
|
||||
}
|
||||
|
||||
func parseKeys(results []madmin.KMSKeyInfo) (data []*models.KmsKeyInfo) {
|
||||
for _, key := range results {
|
||||
data = append(data, &models.KmsKeyInfo{
|
||||
CreatedAt: key.CreatedAt,
|
||||
CreatedBy: key.CreatedBy,
|
||||
Name: key.Name,
|
||||
})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func GetKMSKeyStatusResponse(session *models.Principal, params kmsAPI.KMSKeyStatusParams) (*models.KmsKeyStatusResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return keyStatus(ctx, params.Name, AdminClient{Client: mAdmin})
|
||||
}
|
||||
|
||||
func keyStatus(ctx context.Context, key string, minioClient MinioAdmin) (*models.KmsKeyStatusResponse, *CodedAPIError) {
|
||||
ks, err := minioClient.keyStatus(ctx, key)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.KmsKeyStatusResponse{
|
||||
KeyID: ks.KeyID,
|
||||
EncryptionErr: ks.EncryptionErr,
|
||||
DecryptionErr: ks.DecryptionErr,
|
||||
}, nil
|
||||
}
|
||||
238
api/admin_kms_test.go
Normal file
238
api/admin_kms_test.go
Normal file
@@ -0,0 +1,238 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/console/api/operations"
|
||||
kmsAPI "github.com/minio/console/api/operations/k_m_s"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type KMSTestSuite struct {
|
||||
suite.Suite
|
||||
assert *assert.Assertions
|
||||
currentServer string
|
||||
isServerSet bool
|
||||
server *httptest.Server
|
||||
adminClient AdminClientMock
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) SetupSuite() {
|
||||
suite.assert = assert.New(suite.T())
|
||||
suite.adminClient = AdminClientMock{}
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) SetupTest() {
|
||||
suite.server = httptest.NewServer(http.HandlerFunc(suite.serverHandler))
|
||||
suite.currentServer, suite.isServerSet = os.LookupEnv(ConsoleMinIOServer)
|
||||
os.Setenv(ConsoleMinIOServer, suite.server.URL)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) serverHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(400)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TearDownSuite() {
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TearDownTest() {
|
||||
if suite.isServerSet {
|
||||
os.Setenv(ConsoleMinIOServer, suite.currentServer)
|
||||
} else {
|
||||
os.Unsetenv(ConsoleMinIOServer)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestRegisterKMSHandlers() {
|
||||
api := &operations.ConsoleAPI{}
|
||||
suite.assertHandlersAreNil(api)
|
||||
registerKMSHandlers(api)
|
||||
suite.assertHandlersAreNotNil(api)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) assertHandlersAreNil(api *operations.ConsoleAPI) {
|
||||
suite.assert.Nil(api.KmsKMSStatusHandler)
|
||||
suite.assert.Nil(api.KmsKMSMetricsHandler)
|
||||
suite.assert.Nil(api.KmsKMSAPIsHandler)
|
||||
suite.assert.Nil(api.KmsKMSVersionHandler)
|
||||
suite.assert.Nil(api.KmsKMSCreateKeyHandler)
|
||||
suite.assert.Nil(api.KmsKMSListKeysHandler)
|
||||
suite.assert.Nil(api.KmsKMSKeyStatusHandler)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) assertHandlersAreNotNil(api *operations.ConsoleAPI) {
|
||||
suite.assert.NotNil(api.KmsKMSStatusHandler)
|
||||
suite.assert.NotNil(api.KmsKMSMetricsHandler)
|
||||
suite.assert.NotNil(api.KmsKMSAPIsHandler)
|
||||
suite.assert.NotNil(api.KmsKMSVersionHandler)
|
||||
suite.assert.NotNil(api.KmsKMSCreateKeyHandler)
|
||||
suite.assert.NotNil(api.KmsKMSListKeysHandler)
|
||||
suite.assert.NotNil(api.KmsKMSKeyStatusHandler)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestKMSStatusHandlerWithError() {
|
||||
params, api := suite.initKMSStatusRequest()
|
||||
response := api.KmsKMSStatusHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*kmsAPI.KMSStatusDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) initKMSStatusRequest() (params kmsAPI.KMSStatusParams, api operations.ConsoleAPI) {
|
||||
registerKMSHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestKMSStatusWithoutError() {
|
||||
ctx := context.Background()
|
||||
res, err := kmsStatus(ctx, suite.adminClient)
|
||||
suite.assert.NotNil(res)
|
||||
suite.assert.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestKMSMetricsHandlerWithError() {
|
||||
params, api := suite.initKMSMetricsRequest()
|
||||
response := api.KmsKMSMetricsHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*kmsAPI.KMSMetricsDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) initKMSMetricsRequest() (params kmsAPI.KMSMetricsParams, api operations.ConsoleAPI) {
|
||||
registerKMSHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestKMSMetricsWithoutError() {
|
||||
ctx := context.Background()
|
||||
res, err := kmsMetrics(ctx, suite.adminClient)
|
||||
suite.assert.NotNil(res)
|
||||
suite.assert.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestKMSAPIsHandlerWithError() {
|
||||
params, api := suite.initKMSAPIsRequest()
|
||||
response := api.KmsKMSAPIsHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*kmsAPI.KMSAPIsDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) initKMSAPIsRequest() (params kmsAPI.KMSAPIsParams, api operations.ConsoleAPI) {
|
||||
registerKMSHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestKMSAPIsWithoutError() {
|
||||
ctx := context.Background()
|
||||
res, err := kmsAPIs(ctx, suite.adminClient)
|
||||
suite.assert.NotNil(res)
|
||||
suite.assert.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestKMSVersionHandlerWithError() {
|
||||
params, api := suite.initKMSVersionRequest()
|
||||
response := api.KmsKMSVersionHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*kmsAPI.KMSVersionDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) initKMSVersionRequest() (params kmsAPI.KMSVersionParams, api operations.ConsoleAPI) {
|
||||
registerKMSHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestKMSVersionWithoutError() {
|
||||
ctx := context.Background()
|
||||
res, err := kmsVersion(ctx, suite.adminClient)
|
||||
suite.assert.NotNil(res)
|
||||
suite.assert.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestKMSCreateKeyHandlerWithError() {
|
||||
params, api := suite.initKMSCreateKeyRequest()
|
||||
response := api.KmsKMSCreateKeyHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*kmsAPI.KMSCreateKeyDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) initKMSCreateKeyRequest() (params kmsAPI.KMSCreateKeyParams, api operations.ConsoleAPI) {
|
||||
registerKMSHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
key := "key"
|
||||
params.Body = &models.KmsCreateKeyRequest{Key: &key}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestKMSCreateKeyWithoutError() {
|
||||
ctx := context.Background()
|
||||
err := createKey(ctx, "key", suite.adminClient)
|
||||
suite.assert.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestKMSListKeysHandlerWithError() {
|
||||
params, api := suite.initKMSListKeysRequest()
|
||||
response := api.KmsKMSListKeysHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*kmsAPI.KMSListKeysDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) initKMSListKeysRequest() (params kmsAPI.KMSListKeysParams, api operations.ConsoleAPI) {
|
||||
registerKMSHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestKMSListKeysWithoutError() {
|
||||
ctx := context.Background()
|
||||
res, err := listKeys(ctx, "", suite.adminClient)
|
||||
suite.assert.NotNil(res)
|
||||
suite.assert.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestKMSKeyStatusHandlerWithError() {
|
||||
params, api := suite.initKMSKeyStatusRequest()
|
||||
response := api.KmsKMSKeyStatusHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*kmsAPI.KMSKeyStatusDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) initKMSKeyStatusRequest() (params kmsAPI.KMSKeyStatusParams, api operations.ConsoleAPI) {
|
||||
registerKMSHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *KMSTestSuite) TestKMSKeyStatusWithoutError() {
|
||||
ctx := context.Background()
|
||||
res, err := keyStatus(ctx, "key", suite.adminClient)
|
||||
suite.assert.NotNil(res)
|
||||
suite.assert.Nil(err)
|
||||
}
|
||||
|
||||
func TestKMS(t *testing.T) {
|
||||
suite.Run(t, new(KMSTestSuite))
|
||||
}
|
||||
55
api/admin_nodes.go
Normal file
55
api/admin_nodes.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
systemApi "github.com/minio/console/api/operations/system"
|
||||
"github.com/minio/console/models"
|
||||
)
|
||||
|
||||
func registerNodesHandler(api *operations.ConsoleAPI) {
|
||||
api.SystemListNodesHandler = systemApi.ListNodesHandlerFunc(func(params systemApi.ListNodesParams, session *models.Principal) middleware.Responder {
|
||||
listNodesResponse, err := getListNodesResponse(session, params)
|
||||
if err != nil {
|
||||
return systemApi.NewListNodesDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return systemApi.NewListNodesOK().WithPayload(listNodesResponse)
|
||||
})
|
||||
}
|
||||
|
||||
// getListNodesResponse returns a list of available node endpoints .
|
||||
func getListNodesResponse(session *models.Principal, params systemApi.ListNodesParams) ([]string, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
var nodeList []string
|
||||
|
||||
adminResources, _ := mAdmin.ServerInfo(ctx)
|
||||
|
||||
for _, n := range adminResources.Servers {
|
||||
nodeList = append(nodeList, n.Endpoint)
|
||||
}
|
||||
|
||||
return nodeList, nil
|
||||
}
|
||||
162
api/admin_notification_endpoints.go
Normal file
162
api/admin_notification_endpoints.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
configurationApi "github.com/minio/console/api/operations/configuration"
|
||||
"github.com/minio/console/models"
|
||||
)
|
||||
|
||||
func registerAdminNotificationEndpointsHandlers(api *operations.ConsoleAPI) {
|
||||
// return a list of notification endpoints
|
||||
api.ConfigurationNotificationEndpointListHandler = configurationApi.NotificationEndpointListHandlerFunc(func(params configurationApi.NotificationEndpointListParams, session *models.Principal) middleware.Responder {
|
||||
notifEndpoints, err := getNotificationEndpointsResponse(session, params)
|
||||
if err != nil {
|
||||
return configurationApi.NewNotificationEndpointListDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return configurationApi.NewNotificationEndpointListOK().WithPayload(notifEndpoints)
|
||||
})
|
||||
// add a new notification endpoints
|
||||
api.ConfigurationAddNotificationEndpointHandler = configurationApi.AddNotificationEndpointHandlerFunc(func(params configurationApi.AddNotificationEndpointParams, session *models.Principal) middleware.Responder {
|
||||
notifEndpoints, err := getAddNotificationEndpointResponse(session, params)
|
||||
if err != nil {
|
||||
return configurationApi.NewAddNotificationEndpointDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return configurationApi.NewAddNotificationEndpointCreated().WithPayload(notifEndpoints)
|
||||
})
|
||||
}
|
||||
|
||||
// getNotificationEndpoints invokes admin info and returns a list of notification endpoints
|
||||
func getNotificationEndpoints(ctx context.Context, client MinioAdmin) (*models.NotifEndpointResponse, error) {
|
||||
serverInfo, err := client.serverInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var listEndpoints []*models.NotificationEndpointItem
|
||||
for i := range serverInfo.Services.Notifications {
|
||||
for service, endpointStatus := range serverInfo.Services.Notifications[i] {
|
||||
for j := range endpointStatus {
|
||||
for account, status := range endpointStatus[j] {
|
||||
listEndpoints = append(listEndpoints, &models.NotificationEndpointItem{
|
||||
Service: models.NofiticationService(service),
|
||||
AccountID: account,
|
||||
Status: status.Status,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// build response
|
||||
return &models.NotifEndpointResponse{
|
||||
NotificationEndpoints: listEndpoints,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getNotificationEndpointsResponse returns a list of notification endpoints in the instance
|
||||
func getNotificationEndpointsResponse(session *models.Principal, params configurationApi.NotificationEndpointListParams) (*models.NotifEndpointResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
// serialize output
|
||||
notfEndpointResp, err := getNotificationEndpoints(ctx, adminClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return notfEndpointResp, nil
|
||||
}
|
||||
|
||||
func addNotificationEndpoint(ctx context.Context, client MinioAdmin, params *configurationApi.AddNotificationEndpointParams) (*models.SetNotificationEndpointResponse, error) {
|
||||
configs := []*models.ConfigurationKV{}
|
||||
var configName string
|
||||
|
||||
// we have different add validations for each service
|
||||
switch *params.Body.Service {
|
||||
case models.NofiticationServiceAmqp:
|
||||
configName = "notify_amqp"
|
||||
case models.NofiticationServiceMqtt:
|
||||
configName = "notify_mqtt"
|
||||
case models.NofiticationServiceElasticsearch:
|
||||
configName = "notify_elasticsearch"
|
||||
case models.NofiticationServiceRedis:
|
||||
configName = "notify_redis"
|
||||
case models.NofiticationServiceNats:
|
||||
configName = "notify_nats"
|
||||
case models.NofiticationServicePostgres:
|
||||
configName = "notify_postgres"
|
||||
case models.NofiticationServiceMysql:
|
||||
configName = "notify_mysql"
|
||||
case models.NofiticationServiceKafka:
|
||||
configName = "notify_kafka"
|
||||
case models.NofiticationServiceWebhook:
|
||||
configName = "notify_webhook"
|
||||
case models.NofiticationServiceNsq:
|
||||
configName = "notify_nsq"
|
||||
default:
|
||||
return nil, errors.New("provided service is not supported")
|
||||
}
|
||||
|
||||
// set all the config values if found on the param.Body.Properties
|
||||
for k, val := range params.Body.Properties {
|
||||
configs = append(configs, &models.ConfigurationKV{
|
||||
Key: k,
|
||||
Value: val,
|
||||
})
|
||||
}
|
||||
|
||||
needsRestart, err := setConfigWithARNAccountID(ctx, client, &configName, configs, *params.Body.AccountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.SetNotificationEndpointResponse{
|
||||
AccountID: params.Body.AccountID,
|
||||
Properties: params.Body.Properties,
|
||||
Service: params.Body.Service,
|
||||
Restart: needsRestart,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getNotificationEndpointsResponse returns a list of notification endpoints in the instance
|
||||
func getAddNotificationEndpointResponse(session *models.Principal, params configurationApi.AddNotificationEndpointParams) (*models.SetNotificationEndpointResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
// serialize output
|
||||
notfEndpointResp, err := addNotificationEndpoint(ctx, adminClient, ¶ms)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return notfEndpointResp, nil
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package restapi
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -24,17 +24,17 @@ import (
|
||||
|
||||
"github.com/go-openapi/swag"
|
||||
|
||||
cfgApi "github.com/minio/console/api/operations/configuration"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/restapi/operations/admin_api"
|
||||
)
|
||||
|
||||
func Test_addNotificationEndpoint(t *testing.T) {
|
||||
client := adminClientMock{}
|
||||
client := AdminClientMock{}
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
client MinioAdmin
|
||||
params *admin_api.AddNotificationEndpointParams
|
||||
params *cfgApi.AddNotificationEndpointParams
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -48,7 +48,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
params: &admin_api.AddNotificationEndpointParams{
|
||||
params: &cfgApi.AddNotificationEndpointParams{
|
||||
HTTPRequest: nil,
|
||||
Body: &models.NotificationEndpoint{
|
||||
AccountID: swag.String("1"),
|
||||
@@ -61,7 +61,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, nil
|
||||
},
|
||||
want: &models.SetNotificationEndpointResponse{
|
||||
@@ -81,7 +81,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
params: &admin_api.AddNotificationEndpointParams{
|
||||
params: &cfgApi.AddNotificationEndpointParams{
|
||||
HTTPRequest: nil,
|
||||
Body: &models.NotificationEndpoint{
|
||||
AccountID: swag.String("1"),
|
||||
@@ -94,7 +94,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, errors.New("error")
|
||||
},
|
||||
want: nil,
|
||||
@@ -105,7 +105,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
params: &admin_api.AddNotificationEndpointParams{
|
||||
params: &cfgApi.AddNotificationEndpointParams{
|
||||
HTTPRequest: nil,
|
||||
Body: &models.NotificationEndpoint{
|
||||
AccountID: swag.String("1"),
|
||||
@@ -118,7 +118,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, nil
|
||||
},
|
||||
want: &models.SetNotificationEndpointResponse{
|
||||
@@ -138,7 +138,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
params: &admin_api.AddNotificationEndpointParams{
|
||||
params: &cfgApi.AddNotificationEndpointParams{
|
||||
HTTPRequest: nil,
|
||||
Body: &models.NotificationEndpoint{
|
||||
AccountID: swag.String("1"),
|
||||
@@ -149,7 +149,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, nil
|
||||
},
|
||||
want: &models.SetNotificationEndpointResponse{
|
||||
@@ -167,7 +167,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
params: &admin_api.AddNotificationEndpointParams{
|
||||
params: &cfgApi.AddNotificationEndpointParams{
|
||||
HTTPRequest: nil,
|
||||
Body: &models.NotificationEndpoint{
|
||||
AccountID: swag.String("1"),
|
||||
@@ -178,7 +178,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, nil
|
||||
},
|
||||
want: &models.SetNotificationEndpointResponse{
|
||||
@@ -196,7 +196,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
params: &admin_api.AddNotificationEndpointParams{
|
||||
params: &cfgApi.AddNotificationEndpointParams{
|
||||
HTTPRequest: nil,
|
||||
Body: &models.NotificationEndpoint{
|
||||
AccountID: swag.String("1"),
|
||||
@@ -208,7 +208,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, nil
|
||||
},
|
||||
want: &models.SetNotificationEndpointResponse{
|
||||
@@ -227,7 +227,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
params: &admin_api.AddNotificationEndpointParams{
|
||||
params: &cfgApi.AddNotificationEndpointParams{
|
||||
HTTPRequest: nil,
|
||||
Body: &models.NotificationEndpoint{
|
||||
AccountID: swag.String("1"),
|
||||
@@ -240,7 +240,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, nil
|
||||
},
|
||||
want: &models.SetNotificationEndpointResponse{
|
||||
@@ -260,7 +260,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
params: &admin_api.AddNotificationEndpointParams{
|
||||
params: &cfgApi.AddNotificationEndpointParams{
|
||||
HTTPRequest: nil,
|
||||
Body: &models.NotificationEndpoint{
|
||||
AccountID: swag.String("1"),
|
||||
@@ -273,7 +273,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, nil
|
||||
},
|
||||
want: &models.SetNotificationEndpointResponse{
|
||||
@@ -293,7 +293,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
params: &admin_api.AddNotificationEndpointParams{
|
||||
params: &cfgApi.AddNotificationEndpointParams{
|
||||
HTTPRequest: nil,
|
||||
Body: &models.NotificationEndpoint{
|
||||
AccountID: swag.String("1"),
|
||||
@@ -305,7 +305,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, nil
|
||||
},
|
||||
want: &models.SetNotificationEndpointResponse{
|
||||
@@ -324,7 +324,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
params: &admin_api.AddNotificationEndpointParams{
|
||||
params: &cfgApi.AddNotificationEndpointParams{
|
||||
HTTPRequest: nil,
|
||||
Body: &models.NotificationEndpoint{
|
||||
AccountID: swag.String("1"),
|
||||
@@ -335,7 +335,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, nil
|
||||
},
|
||||
want: &models.SetNotificationEndpointResponse{
|
||||
@@ -353,7 +353,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
params: &admin_api.AddNotificationEndpointParams{
|
||||
params: &cfgApi.AddNotificationEndpointParams{
|
||||
HTTPRequest: nil,
|
||||
Body: &models.NotificationEndpoint{
|
||||
AccountID: swag.String("1"),
|
||||
@@ -365,7 +365,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, nil
|
||||
},
|
||||
want: &models.SetNotificationEndpointResponse{
|
||||
@@ -384,7 +384,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
params: &admin_api.AddNotificationEndpointParams{
|
||||
params: &cfgApi.AddNotificationEndpointParams{
|
||||
HTTPRequest: nil,
|
||||
Body: &models.NotificationEndpoint{
|
||||
AccountID: swag.String("1"),
|
||||
@@ -397,7 +397,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return false, errors.New("invalid config")
|
||||
},
|
||||
want: nil,
|
||||
@@ -408,7 +408,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
params: &admin_api.AddNotificationEndpointParams{
|
||||
params: &cfgApi.AddNotificationEndpointParams{
|
||||
HTTPRequest: nil,
|
||||
Body: &models.NotificationEndpoint{
|
||||
AccountID: swag.String("1"),
|
||||
@@ -421,7 +421,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetConfig: func(kv string) (restart bool, err error) {
|
||||
mockSetConfig: func(_ string) (restart bool, err error) {
|
||||
return true, nil
|
||||
},
|
||||
want: &models.SetNotificationEndpointResponse{
|
||||
@@ -438,7 +438,7 @@ func Test_addNotificationEndpoint(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
// mock function response from setConfig()
|
||||
minioSetConfigKVMock = tt.mockSetConfig
|
||||
got, err := addNotificationEndpoint(tt.args.ctx, tt.args.client, tt.args.params)
|
||||
90
api/admin_objects.go
Normal file
90
api/admin_objects.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/minio/mc/cmd"
|
||||
"github.com/minio/minio-go/v7"
|
||||
)
|
||||
|
||||
type objectsListOpts struct {
|
||||
BucketName string
|
||||
Prefix string
|
||||
Date time.Time
|
||||
}
|
||||
|
||||
type ObjectsRequest struct {
|
||||
Mode string `json:"mode,omitempty"`
|
||||
BucketName string `json:"bucket_name"`
|
||||
Prefix string `json:"prefix"`
|
||||
Date string `json:"date"`
|
||||
RequestID int64 `json:"request_id"`
|
||||
}
|
||||
|
||||
type WSResponse struct {
|
||||
RequestID int64 `json:"request_id,omitempty"`
|
||||
Error *CodedAPIError `json:"error,omitempty"`
|
||||
RequestEnd bool `json:"request_end,omitempty"`
|
||||
Prefix string `json:"prefix,omitempty"`
|
||||
BucketName string `json:"bucketName,omitempty"`
|
||||
Data []ObjectResponse `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type ObjectResponse struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
LastModified string `json:"last_modified,omitempty"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
VersionID string `json:"version_id,omitempty"`
|
||||
DeleteMarker bool `json:"delete_flag,omitempty"`
|
||||
IsLatest bool `json:"is_latest,omitempty"`
|
||||
}
|
||||
|
||||
func getObjectsOptionsFromReq(request ObjectsRequest) (*objectsListOpts, error) {
|
||||
pOptions := objectsListOpts{
|
||||
BucketName: request.BucketName,
|
||||
Prefix: request.Prefix,
|
||||
}
|
||||
|
||||
if request.Mode == "rewind" {
|
||||
parsedDate, errDate := time.Parse(time.RFC3339, request.Date)
|
||||
|
||||
if errDate != nil {
|
||||
return nil, errDate
|
||||
}
|
||||
|
||||
pOptions.Date = parsedDate
|
||||
}
|
||||
|
||||
return &pOptions, nil
|
||||
}
|
||||
|
||||
func startObjectsListing(ctx context.Context, client MinioClient, objOpts *objectsListOpts) <-chan minio.ObjectInfo {
|
||||
opts := minio.ListObjectsOptions{
|
||||
Prefix: objOpts.Prefix,
|
||||
}
|
||||
|
||||
return client.listObjects(ctx, objOpts.BucketName, opts)
|
||||
}
|
||||
|
||||
func startRewindListing(ctx context.Context, client MCClient, objOpts *objectsListOpts) <-chan *cmd.ClientContent {
|
||||
lsRewind := client.list(ctx, cmd.ListOptions{TimeRef: objOpts.Date, WithDeleteMarkers: true})
|
||||
|
||||
return lsRewind
|
||||
}
|
||||
236
api/admin_objects_test.go
Normal file
236
api/admin_objects_test.go
Normal file
@@ -0,0 +1,236 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
mc "github.com/minio/mc/cmd"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWSRewindObjects(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := s3ClientMock{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
testOptions objectsListOpts
|
||||
testMessages []*mc.ClientContent
|
||||
}{
|
||||
{
|
||||
name: "Get list with multiple elements",
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/",
|
||||
Date: time.Now(),
|
||||
},
|
||||
testMessages: []*mc.ClientContent{
|
||||
{
|
||||
BucketName: "buckettest",
|
||||
URL: mc.ClientURL{Path: "/file1.txt"},
|
||||
},
|
||||
{
|
||||
BucketName: "buckettest",
|
||||
URL: mc.ClientURL{Path: "/file2.txt"},
|
||||
},
|
||||
{
|
||||
BucketName: "buckettest",
|
||||
URL: mc.ClientURL{Path: "/path1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Empty list of elements",
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "emptybucket",
|
||||
Prefix: "/",
|
||||
Date: time.Now(),
|
||||
},
|
||||
testMessages: []*mc.ClientContent{},
|
||||
},
|
||||
{
|
||||
name: "Get list with one element",
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/",
|
||||
Date: time.Now(),
|
||||
},
|
||||
testMessages: []*mc.ClientContent{
|
||||
{
|
||||
BucketName: "buckettestsingle",
|
||||
URL: mc.ClientURL{Path: "/file12.txt"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Get data from subpaths",
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/path1/path2",
|
||||
Date: time.Now(),
|
||||
},
|
||||
testMessages: []*mc.ClientContent{
|
||||
{
|
||||
BucketName: "buckettestsingle",
|
||||
URL: mc.ClientURL{Path: "/path1/path2/file12.txt"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
mcListMock = func(_ context.Context, _ mc.ListOptions) <-chan *mc.ClientContent {
|
||||
ch := make(chan *mc.ClientContent)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for _, m := range tt.testMessages {
|
||||
ch <- m
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
rewindList := startRewindListing(ctx, client, &tt.testOptions)
|
||||
|
||||
// check that the rewindList got the same number of data from Console.
|
||||
|
||||
totalItems := 0
|
||||
for data := range rewindList {
|
||||
// Compare elements as we are defining the channel responses
|
||||
assert.Equal(tt.testMessages[totalItems].URL.Path, data.URL.Path)
|
||||
totalItems++
|
||||
}
|
||||
assert.Equal(len(tt.testMessages), totalItems)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWSListObjects(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := minioClientMock{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
wantErr bool
|
||||
testOptions objectsListOpts
|
||||
testMessages []minio.ObjectInfo
|
||||
}{
|
||||
{
|
||||
name: "Get list with multiple elements",
|
||||
wantErr: false,
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/",
|
||||
},
|
||||
testMessages: []minio.ObjectInfo{
|
||||
{
|
||||
Key: "/file1.txt",
|
||||
Size: 500,
|
||||
IsLatest: true,
|
||||
LastModified: time.Now(),
|
||||
},
|
||||
{
|
||||
Key: "/file2.txt",
|
||||
Size: 500,
|
||||
IsLatest: true,
|
||||
LastModified: time.Now(),
|
||||
},
|
||||
{
|
||||
Key: "/path1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Empty list of elements",
|
||||
wantErr: false,
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "emptybucket",
|
||||
Prefix: "/",
|
||||
},
|
||||
testMessages: []minio.ObjectInfo{},
|
||||
},
|
||||
{
|
||||
name: "Get list with one element",
|
||||
wantErr: false,
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/",
|
||||
},
|
||||
testMessages: []minio.ObjectInfo{
|
||||
{
|
||||
Key: "/file2.txt",
|
||||
Size: 500,
|
||||
IsLatest: true,
|
||||
LastModified: time.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Get data from subpaths",
|
||||
wantErr: false,
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/path1/path2",
|
||||
},
|
||||
testMessages: []minio.ObjectInfo{
|
||||
{
|
||||
Key: "/path1/path2/file1.txt",
|
||||
Size: 500,
|
||||
IsLatest: true,
|
||||
LastModified: time.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
minioListObjectsMock = func(_ context.Context, _ string, _ minio.ListObjectsOptions) <-chan minio.ObjectInfo {
|
||||
ch := make(chan minio.ObjectInfo)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for _, m := range tt.testMessages {
|
||||
ch <- m
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
objectsListing := startObjectsListing(ctx, client, &tt.testOptions)
|
||||
|
||||
// check that the TestReceiver got the same number of data from Console
|
||||
totalItems := 0
|
||||
for data := range objectsListing {
|
||||
// Compare elements as we are defining the channel responses
|
||||
assert.Equal(tt.testMessages[totalItems].Key, data.Key)
|
||||
totalItems++
|
||||
}
|
||||
assert.Equal(len(tt.testMessages), totalItems)
|
||||
})
|
||||
}
|
||||
}
|
||||
698
api/admin_policies.go
Normal file
698
api/admin_policies.go
Normal file
@@ -0,0 +1,698 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
bucketApi "github.com/minio/console/api/operations/bucket"
|
||||
policyApi "github.com/minio/console/api/operations/policy"
|
||||
s3 "github.com/minio/minio-go/v7"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
"github.com/minio/console/models"
|
||||
iampolicy "github.com/minio/pkg/v3/policy"
|
||||
|
||||
policies "github.com/minio/console/api/policy"
|
||||
)
|
||||
|
||||
func registersPoliciesHandler(api *operations.ConsoleAPI) {
|
||||
// List Policies
|
||||
api.PolicyListPoliciesHandler = policyApi.ListPoliciesHandlerFunc(func(params policyApi.ListPoliciesParams, session *models.Principal) middleware.Responder {
|
||||
listPoliciesResponse, err := getListPoliciesResponse(session, params)
|
||||
if err != nil {
|
||||
return policyApi.NewListPoliciesDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return policyApi.NewListPoliciesOK().WithPayload(listPoliciesResponse)
|
||||
})
|
||||
// Policy Info
|
||||
api.PolicyPolicyInfoHandler = policyApi.PolicyInfoHandlerFunc(func(params policyApi.PolicyInfoParams, session *models.Principal) middleware.Responder {
|
||||
policyInfo, err := getPolicyInfoResponse(session, params)
|
||||
if err != nil {
|
||||
return policyApi.NewPolicyInfoDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return policyApi.NewPolicyInfoOK().WithPayload(policyInfo)
|
||||
})
|
||||
// Add Policy
|
||||
api.PolicyAddPolicyHandler = policyApi.AddPolicyHandlerFunc(func(params policyApi.AddPolicyParams, session *models.Principal) middleware.Responder {
|
||||
policyResponse, err := getAddPolicyResponse(session, params)
|
||||
if err != nil {
|
||||
return policyApi.NewAddPolicyDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return policyApi.NewAddPolicyCreated().WithPayload(policyResponse)
|
||||
})
|
||||
// Remove Policy
|
||||
api.PolicyRemovePolicyHandler = policyApi.RemovePolicyHandlerFunc(func(params policyApi.RemovePolicyParams, session *models.Principal) middleware.Responder {
|
||||
if err := getRemovePolicyResponse(session, params); err != nil {
|
||||
return policyApi.NewRemovePolicyDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return policyApi.NewRemovePolicyNoContent()
|
||||
})
|
||||
// Set Policy
|
||||
api.PolicySetPolicyHandler = policyApi.SetPolicyHandlerFunc(func(params policyApi.SetPolicyParams, session *models.Principal) middleware.Responder {
|
||||
if err := getSetPolicyResponse(session, params); err != nil {
|
||||
return policyApi.NewSetPolicyDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return policyApi.NewSetPolicyNoContent()
|
||||
})
|
||||
// Set Policy Multiple User/Groups
|
||||
api.PolicySetPolicyMultipleHandler = policyApi.SetPolicyMultipleHandlerFunc(func(params policyApi.SetPolicyMultipleParams, session *models.Principal) middleware.Responder {
|
||||
if err := getSetPolicyMultipleResponse(session, params); err != nil {
|
||||
return policyApi.NewSetPolicyMultipleDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return policyApi.NewSetPolicyMultipleNoContent()
|
||||
})
|
||||
api.BucketListPoliciesWithBucketHandler = bucketApi.ListPoliciesWithBucketHandlerFunc(func(params bucketApi.ListPoliciesWithBucketParams, session *models.Principal) middleware.Responder {
|
||||
policyResponse, err := getListPoliciesWithBucketResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewListPoliciesWithBucketDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewListPoliciesWithBucketOK().WithPayload(policyResponse)
|
||||
})
|
||||
api.BucketListAccessRulesWithBucketHandler = bucketApi.ListAccessRulesWithBucketHandlerFunc(func(params bucketApi.ListAccessRulesWithBucketParams, session *models.Principal) middleware.Responder {
|
||||
policyResponse, err := getListAccessRulesWithBucketResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewListAccessRulesWithBucketDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewListAccessRulesWithBucketOK().WithPayload(policyResponse)
|
||||
})
|
||||
api.BucketSetAccessRuleWithBucketHandler = bucketApi.SetAccessRuleWithBucketHandlerFunc(func(params bucketApi.SetAccessRuleWithBucketParams, session *models.Principal) middleware.Responder {
|
||||
policyResponse, err := getSetAccessRuleWithBucketResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewSetAccessRuleWithBucketDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewSetAccessRuleWithBucketOK().WithPayload(policyResponse)
|
||||
})
|
||||
api.BucketDeleteAccessRuleWithBucketHandler = bucketApi.DeleteAccessRuleWithBucketHandlerFunc(func(params bucketApi.DeleteAccessRuleWithBucketParams, session *models.Principal) middleware.Responder {
|
||||
policyResponse, err := getDeleteAccessRuleWithBucketResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewDeleteAccessRuleWithBucketDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewDeleteAccessRuleWithBucketOK().WithPayload(policyResponse)
|
||||
})
|
||||
api.PolicyListUsersForPolicyHandler = policyApi.ListUsersForPolicyHandlerFunc(func(params policyApi.ListUsersForPolicyParams, session *models.Principal) middleware.Responder {
|
||||
policyUsersResponse, err := getListUsersForPolicyResponse(session, params)
|
||||
if err != nil {
|
||||
return policyApi.NewListUsersForPolicyDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return policyApi.NewListUsersForPolicyOK().WithPayload(policyUsersResponse)
|
||||
})
|
||||
api.PolicyListGroupsForPolicyHandler = policyApi.ListGroupsForPolicyHandlerFunc(func(params policyApi.ListGroupsForPolicyParams, session *models.Principal) middleware.Responder {
|
||||
policyGroupsResponse, err := getListGroupsForPolicyResponse(session, params)
|
||||
if err != nil {
|
||||
return policyApi.NewListGroupsForPolicyDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return policyApi.NewListGroupsForPolicyOK().WithPayload(policyGroupsResponse)
|
||||
})
|
||||
// Gets policies for currently logged in user
|
||||
api.PolicyGetUserPolicyHandler = policyApi.GetUserPolicyHandlerFunc(func(params policyApi.GetUserPolicyParams, session *models.Principal) middleware.Responder {
|
||||
userPolicyResponse, err := getUserPolicyResponse(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return policyApi.NewGetUserPolicyDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return policyApi.NewGetUserPolicyOK().WithPayload(userPolicyResponse)
|
||||
})
|
||||
// Gets policies for specified user
|
||||
api.PolicyGetSAUserPolicyHandler = policyApi.GetSAUserPolicyHandlerFunc(func(params policyApi.GetSAUserPolicyParams, session *models.Principal) middleware.Responder {
|
||||
userPolicyResponse, err := getSAUserPolicyResponse(session, params)
|
||||
if err != nil {
|
||||
return policyApi.NewGetSAUserPolicyDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return policyApi.NewGetSAUserPolicyOK().WithPayload(userPolicyResponse)
|
||||
})
|
||||
}
|
||||
|
||||
func getListAccessRulesWithBucketResponse(session *models.Principal, params bucketApi.ListAccessRulesWithBucketParams) (*models.ListAccessRulesResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
bucket := params.Bucket
|
||||
client, err := newS3BucketClient(session, bucket, "", getClientIP(params.HTTPRequest))
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
accessRules, _ := client.GetAccessRules(ctx)
|
||||
var accessRuleList []*models.AccessRule
|
||||
for k, v := range accessRules {
|
||||
accessRuleList = append(accessRuleList, &models.AccessRule{Prefix: k[len(bucket)+1 : len(k)-1], Access: v})
|
||||
}
|
||||
return &models.ListAccessRulesResponse{AccessRules: accessRuleList}, nil
|
||||
}
|
||||
|
||||
func getSetAccessRuleWithBucketResponse(session *models.Principal, params bucketApi.SetAccessRuleWithBucketParams) (bool, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
prefixAccess := params.Prefixaccess
|
||||
client, err := newS3BucketClient(session, params.Bucket, prefixAccess.Prefix, getClientIP(params.HTTPRequest))
|
||||
if err != nil {
|
||||
return false, ErrorWithContext(ctx, err)
|
||||
}
|
||||
errorVal := client.SetAccess(ctx, prefixAccess.Access, false)
|
||||
if errorVal != nil {
|
||||
returnError := ErrorWithContext(ctx, errorVal.Cause)
|
||||
minioError := s3.ToErrorResponse(errorVal.Cause)
|
||||
if minioError.Code == "NoSuchBucket" {
|
||||
returnError.Code = 404
|
||||
}
|
||||
return false, returnError
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func getDeleteAccessRuleWithBucketResponse(session *models.Principal, params bucketApi.DeleteAccessRuleWithBucketParams) (bool, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
bucket := params.Bucket
|
||||
prefix := params.Prefix
|
||||
client, err := newS3BucketClient(session, bucket, prefix.Prefix, getClientIP(params.HTTPRequest))
|
||||
if err != nil {
|
||||
return false, ErrorWithContext(ctx, err)
|
||||
}
|
||||
errorVal := client.SetAccess(ctx, "none", false)
|
||||
if errorVal != nil {
|
||||
return false, ErrorWithContext(ctx, errorVal.Cause)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func getListPoliciesWithBucketResponse(session *models.Principal, params bucketApi.ListPoliciesWithBucketParams) (*models.ListPoliciesResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
policies, err := listPoliciesWithBucket(ctx, params.Bucket, adminClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// serialize output
|
||||
listPoliciesResponse := &models.ListPoliciesResponse{
|
||||
Policies: policies,
|
||||
Total: int64(len(policies)),
|
||||
}
|
||||
return listPoliciesResponse, nil
|
||||
}
|
||||
|
||||
// listPoliciesWithBucket calls MinIO server to list all policy names present on the server that apply to a particular bucket.
|
||||
// listPoliciesWithBucket() converts the map[string][]byte returned by client.listPolicies()
|
||||
// to []*models.Policy by iterating over each key in policyRawMap and
|
||||
// then using Unmarshal on the raw bytes to create a *models.Policy
|
||||
func listPoliciesWithBucket(ctx context.Context, bucket string, client MinioAdmin) ([]*models.Policy, error) {
|
||||
policyMap, err := client.listPolicies(ctx)
|
||||
var policies []*models.Policy
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for name, policy := range policyMap {
|
||||
policy, err := parsePolicy(name, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if policyMatchesBucket(ctx, policy, bucket) {
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
}
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
func policyMatchesBucket(ctx context.Context, policy *models.Policy, bucket string) bool {
|
||||
policyData := &iampolicy.Policy{}
|
||||
err := json.Unmarshal([]byte(policy.Policy), policyData)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("error parsing policy: %v", err))
|
||||
return false
|
||||
}
|
||||
policyStatements := policyData.Statements
|
||||
for i := 0; i < len(policyStatements); i++ {
|
||||
resources := policyStatements[i].Resources
|
||||
if resources.Match(bucket, map[string][]string{}) {
|
||||
return true
|
||||
}
|
||||
if resources.Match(fmt.Sprintf("%s/*", bucket), map[string][]string{}) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// listPolicies calls MinIO server to list all policy names present on the server.
|
||||
// listPolicies() converts the map[string][]byte returned by client.listPolicies()
|
||||
// to []*models.Policy by iterating over each key in policyRawMap and
|
||||
// then using Unmarshal on the raw bytes to create a *models.Policy
|
||||
func listPolicies(ctx context.Context, client MinioAdmin) ([]*models.Policy, error) {
|
||||
policyMap, err := client.listPolicies(ctx)
|
||||
var policies []*models.Policy
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for name, policy := range policyMap {
|
||||
policy, err := parsePolicy(name, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
// getListPoliciesResponse performs listPolicies() and serializes it to the handler's output
|
||||
func getListPoliciesResponse(session *models.Principal, params policyApi.ListPoliciesParams) (*models.ListPoliciesResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
policies, err := listPolicies(ctx, adminClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// serialize output
|
||||
listPoliciesResponse := &models.ListPoliciesResponse{
|
||||
Policies: policies,
|
||||
Total: int64(len(policies)),
|
||||
}
|
||||
return listPoliciesResponse, nil
|
||||
}
|
||||
|
||||
// getListUsersForPoliciesResponse performs lists users affected by a given policy.
|
||||
func getListUsersForPolicyResponse(session *models.Principal, params policyApi.ListUsersForPolicyParams) ([]string, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
policies, err := listPolicies(ctx, adminClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
found := false
|
||||
for i := range policies {
|
||||
if policies[i].Name == params.Policy {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, ErrorWithContext(ctx, ErrPolicyNotFound, fmt.Errorf("the policy %s does not exist", params.Policy))
|
||||
}
|
||||
users, err := listUsers(ctx, adminClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
var filteredUsers []string
|
||||
for _, user := range users {
|
||||
for _, upolicy := range user.Policy {
|
||||
if upolicy == params.Policy {
|
||||
filteredUsers = append(filteredUsers, user.AccessKey)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(filteredUsers)
|
||||
return filteredUsers, nil
|
||||
}
|
||||
|
||||
func getUserPolicyResponse(ctx context.Context, session *models.Principal) (string, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
// serialize output
|
||||
if session == nil {
|
||||
return "nil", ErrorWithContext(ctx, ErrPolicyNotFound)
|
||||
}
|
||||
tokenClaims, _ := getClaimsFromToken(session.STSSessionToken)
|
||||
|
||||
// initialize admin client
|
||||
mAdminClient, err := NewMinioAdminClient(ctx, &models.Principal{
|
||||
STSAccessKeyID: session.STSAccessKeyID,
|
||||
STSSecretAccessKey: session.STSSecretAccessKey,
|
||||
STSSessionToken: session.STSSessionToken,
|
||||
})
|
||||
if err != nil {
|
||||
return "nil", ErrorWithContext(ctx, err)
|
||||
}
|
||||
userAdminClient := AdminClient{Client: mAdminClient}
|
||||
// Obtain the current policy assigned to this user
|
||||
// necessary for generating the list of allowed endpoints
|
||||
accountInfo, err := getAccountInfo(ctx, userAdminClient)
|
||||
if err != nil {
|
||||
return "nil", ErrorWithContext(ctx, err)
|
||||
}
|
||||
rawPolicy := policies.ReplacePolicyVariables(tokenClaims, accountInfo)
|
||||
return string(rawPolicy), nil
|
||||
}
|
||||
|
||||
func getSAUserPolicyResponse(session *models.Principal, params policyApi.GetSAUserPolicyParams) (*models.AUserPolicyResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
// serialize output
|
||||
if session == nil {
|
||||
return nil, ErrorWithContext(ctx, ErrPolicyNotFound)
|
||||
}
|
||||
// initialize admin client
|
||||
mAdminClient, err := NewMinioAdminClient(params.HTTPRequest.Context(), &models.Principal{
|
||||
STSAccessKeyID: session.STSAccessKeyID,
|
||||
STSSecretAccessKey: session.STSSecretAccessKey,
|
||||
STSSessionToken: session.STSSessionToken,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
userAdminClient := AdminClient{Client: mAdminClient}
|
||||
|
||||
user, err := getUserInfo(ctx, userAdminClient, params.Name)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
var userPolicies []string
|
||||
if len(user.PolicyName) > 0 {
|
||||
userPolicies = strings.Split(user.PolicyName, ",")
|
||||
}
|
||||
|
||||
for _, group := range user.MemberOf {
|
||||
groupDesc, err := groupInfo(ctx, userAdminClient, group)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
if groupDesc.Policy != "" {
|
||||
userPolicies = append(userPolicies, strings.Split(groupDesc.Policy, ",")...)
|
||||
}
|
||||
}
|
||||
|
||||
allKeys := make(map[string]bool)
|
||||
var userPolicyList []string
|
||||
|
||||
for _, item := range userPolicies {
|
||||
if _, value := allKeys[item]; !value {
|
||||
allKeys[item] = true
|
||||
userPolicyList = append(userPolicyList, item)
|
||||
}
|
||||
}
|
||||
var userStatements []iampolicy.Statement
|
||||
|
||||
for _, pol := range userPolicyList {
|
||||
policy, err := getPolicyStatements(ctx, userAdminClient, pol)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
userStatements = append(userStatements, policy...)
|
||||
}
|
||||
|
||||
combinedPolicy := iampolicy.Policy{
|
||||
Version: "2012-10-17",
|
||||
Statements: userStatements,
|
||||
}
|
||||
|
||||
stringPolicy, err := json.Marshal(combinedPolicy)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
parsedPolicy := string(stringPolicy)
|
||||
|
||||
getUserPoliciesResponse := &models.AUserPolicyResponse{
|
||||
Policy: parsedPolicy,
|
||||
}
|
||||
|
||||
return getUserPoliciesResponse, nil
|
||||
}
|
||||
|
||||
func getListGroupsForPolicyResponse(session *models.Principal, params policyApi.ListGroupsForPolicyParams) ([]string, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
policies, err := listPolicies(ctx, adminClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
found := false
|
||||
for i := range policies {
|
||||
if policies[i].Name == params.Policy {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, ErrorWithContext(ctx, ErrPolicyNotFound, fmt.Errorf("the policy %s does not exist", params.Policy))
|
||||
}
|
||||
|
||||
groups, err := adminClient.listGroups(ctx)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
var filteredGroups []string
|
||||
for _, group := range groups {
|
||||
info, err := groupInfo(ctx, adminClient, group)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
groupPolicies := strings.Split(info.Policy, ",")
|
||||
for _, groupPolicy := range groupPolicies {
|
||||
if groupPolicy == params.Policy {
|
||||
filteredGroups = append(filteredGroups, group)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(filteredGroups)
|
||||
return filteredGroups, nil
|
||||
}
|
||||
|
||||
// removePolicy() calls MinIO server to remove a policy based on name.
|
||||
func removePolicy(ctx context.Context, client MinioAdmin, name string) error {
|
||||
err := client.removePolicy(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRemovePolicyResponse() performs removePolicy() and serializes it to the handler's output
|
||||
func getRemovePolicyResponse(session *models.Principal, params policyApi.RemovePolicyParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
if params.Name == "" {
|
||||
return ErrorWithContext(ctx, ErrPolicyNameNotInRequest)
|
||||
}
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
if err := removePolicy(ctx, adminClient, params.Name); err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addPolicy calls MinIO server to add a canned policy.
|
||||
// addPolicy() takes name and policy in string format, policy
|
||||
// policy must be string in json format, in the future this will change
|
||||
// to a Policy struct{} - https://github.com/minio/minio/issues/9171
|
||||
func addPolicy(ctx context.Context, client MinioAdmin, name, policy string) (*models.Policy, error) {
|
||||
iamp, err := iampolicy.ParseConfig(bytes.NewReader([]byte(policy)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.addPolicy(ctx, name, iamp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policyObject, err := policyInfo(ctx, client, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return policyObject, nil
|
||||
}
|
||||
|
||||
// getAddPolicyResponse performs addPolicy() and serializes it to the handler's output
|
||||
func getAddPolicyResponse(session *models.Principal, params policyApi.AddPolicyParams) (*models.Policy, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
if params.Body == nil {
|
||||
return nil, ErrorWithContext(ctx, ErrPolicyBodyNotInRequest)
|
||||
}
|
||||
if strings.Contains(*params.Body.Name, " ") {
|
||||
return nil, ErrorWithContext(ctx, ErrPolicyNameContainsSpace)
|
||||
}
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
policy, err := addPolicy(ctx, adminClient, *params.Body.Name, *params.Body.Policy)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
// policyInfo calls MinIO server to retrieve information of a canned policy.
|
||||
// policyInfo() takes a policy name, obtains the []byte (represents a string in JSON format)
|
||||
// and return it as *models.Policy , in the future this will change
|
||||
// to a Policy struct{} - https://github.com/minio/minio/issues/9171
|
||||
func policyInfo(ctx context.Context, client MinioAdmin, name string) (*models.Policy, error) {
|
||||
policyRaw, err := client.getPolicy(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policy, err := parsePolicy(name, policyRaw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
// getPolicy Statements calls MinIO server to retrieve information of a canned policy.
|
||||
// and returns the associated Statements
|
||||
func getPolicyStatements(ctx context.Context, client MinioAdmin, name string) ([]iampolicy.Statement, error) {
|
||||
policyRaw, err := client.getPolicy(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return policyRaw.Statements, nil
|
||||
}
|
||||
|
||||
// getPolicyInfoResponse performs policyInfo() and serializes it to the handler's output
|
||||
func getPolicyInfoResponse(session *models.Principal, params policyApi.PolicyInfoParams) (*models.Policy, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
policy, err := policyInfo(ctx, adminClient, params.Name)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
// SetPolicy calls MinIO server to assign policy to a group or user.
|
||||
func SetPolicy(ctx context.Context, client MinioAdmin, name, entityName string, entityType models.PolicyEntity) error {
|
||||
isGroup := false
|
||||
if entityType == models.PolicyEntityGroup {
|
||||
isGroup = true
|
||||
}
|
||||
return client.setPolicy(ctx, name, entityName, isGroup)
|
||||
}
|
||||
|
||||
// getSetPolicyResponse() performs SetPolicy() and serializes it to the handler's output
|
||||
func getSetPolicyResponse(session *models.Principal, params policyApi.SetPolicyParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
// Removing this section
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
if err := SetPolicy(ctx, adminClient, strings.Join(params.Body.Name, ","), *params.Body.EntityName, *params.Body.EntityType); err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSetPolicyMultipleResponse(session *models.Principal, params policyApi.SetPolicyMultipleParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
if err := setPolicyMultipleEntities(ctx, adminClient, strings.Join(params.Body.Name, ","), params.Body.Users, params.Body.Groups); err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setPolicyMultipleEntities sets a policy to multiple users/groups
|
||||
func setPolicyMultipleEntities(ctx context.Context, client MinioAdmin, policyName string, users, groups []models.IamEntity) error {
|
||||
for _, user := range users {
|
||||
if err := client.setPolicy(ctx, policyName, string(user), false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, group := range groups {
|
||||
groupDesc, err := groupInfo(ctx, client, string(group))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allGroupPolicies := ""
|
||||
if len(groups) > 1 {
|
||||
allGroupPolicies = groupDesc.Policy + "," + policyName
|
||||
s := strings.Split(allGroupPolicies, ",")
|
||||
allGroupPolicies = strings.Join(UniqueKeys(s), ",")
|
||||
} else {
|
||||
allGroupPolicies = policyName
|
||||
}
|
||||
if err := client.setPolicy(ctx, allGroupPolicies, string(group), true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parsePolicy() converts from *rawPolicy to *models.Policy
|
||||
func parsePolicy(name string, rawPolicy *iampolicy.Policy) (*models.Policy, error) {
|
||||
stringPolicy, err := json.Marshal(rawPolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policy := &models.Policy{
|
||||
Name: name,
|
||||
Policy: string(stringPolicy),
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
382
api/admin_policies_test.go
Normal file
382
api/admin_policies_test.go
Normal file
@@ -0,0 +1,382 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
iampolicy "github.com/minio/pkg/v3/policy"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestListPolicies(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
funcAssert := assert.New(t)
|
||||
adminClient := AdminClientMock{}
|
||||
// mock function response from listPolicies()
|
||||
minioListPoliciesMock = func() (map[string]*iampolicy.Policy, error) {
|
||||
var readonly iampolicy.Policy
|
||||
var readwrite iampolicy.Policy
|
||||
var diagnostis iampolicy.Policy
|
||||
|
||||
for _, p := range iampolicy.DefaultPolicies {
|
||||
switch p.Name {
|
||||
case "readonly":
|
||||
readonly = p.Definition
|
||||
case "readwrite":
|
||||
readwrite = p.Definition
|
||||
case "diagnostics":
|
||||
diagnostis = p.Definition
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]*iampolicy.Policy{
|
||||
"readonly": &readonly,
|
||||
"readwrite": &readwrite,
|
||||
"diagnostics": &diagnostis,
|
||||
}, nil
|
||||
}
|
||||
// Test-1 : listPolicies() Get response from minio client with three Canned Policies and return the same number on listPolicies()
|
||||
function := "listPolicies()"
|
||||
policiesList, err := listPolicies(ctx, adminClient)
|
||||
if err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// verify length of Policies is correct
|
||||
funcAssert.Equal(3, len(policiesList), fmt.Sprintf("Failed on %s: length of Policies's lists is not the same", function))
|
||||
// Test-2 : listPolicies() Return error and see that the error is handled correctly and returned
|
||||
minioListPoliciesMock = func() (map[string]*iampolicy.Policy, error) {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
_, err = listPolicies(ctx, adminClient)
|
||||
if funcAssert.Error(err) {
|
||||
funcAssert.Equal("error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemovePolicy(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
funcAssert := assert.New(t)
|
||||
adminClient := AdminClientMock{}
|
||||
// Test-1 : removePolicy() remove an existing policy
|
||||
policyToRemove := "console-policy"
|
||||
minioRemovePolicyMock = func(_ string) error {
|
||||
return nil
|
||||
}
|
||||
function := "removePolicy()"
|
||||
if err := removePolicy(ctx, adminClient, policyToRemove); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// Test-2 : removePolicy() Return error and see that the error is handled correctly and returned
|
||||
minioRemovePolicyMock = func(_ string) error {
|
||||
return errors.New("error")
|
||||
}
|
||||
if err := removePolicy(ctx, adminClient, policyToRemove); funcAssert.Error(err) {
|
||||
funcAssert.Equal("error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddPolicy(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
funcAssert := assert.New(t)
|
||||
adminClient := AdminClientMock{}
|
||||
policyName := "new-policy"
|
||||
policyDefinition := "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:GetBucketLocation\",\"s3:GetObject\",\"s3:ListAllMyBuckets\"],\"Resource\":[\"arn:aws:s3:::*\"]}]}"
|
||||
minioAddPolicyMock = func(_ string, _ *iampolicy.Policy) error {
|
||||
return nil
|
||||
}
|
||||
minioGetPolicyMock = func(_ string) (*iampolicy.Policy, error) {
|
||||
policy := "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:GetBucketLocation\",\"s3:GetObject\",\"s3:ListAllMyBuckets\"],\"Resource\":[\"arn:aws:s3:::*\"]}]}"
|
||||
iamp, err := iampolicy.ParseConfig(bytes.NewReader([]byte(policy)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return iamp, nil
|
||||
}
|
||||
assertPolicy := models.Policy{
|
||||
Name: "new-policy",
|
||||
Policy: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:GetBucketLocation\",\"s3:GetObject\",\"s3:ListAllMyBuckets\"],\"Resource\":[\"arn:aws:s3:::*\"]}]}",
|
||||
}
|
||||
// Test-1 : addPolicy() adds a new policy
|
||||
function := "addPolicy()"
|
||||
policy, err := addPolicy(ctx, adminClient, policyName, policyDefinition)
|
||||
if err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
} else {
|
||||
funcAssert.Equal(policy.Name, assertPolicy.Name)
|
||||
|
||||
var expectedPolicy iampolicy.Policy
|
||||
var actualPolicy iampolicy.Policy
|
||||
err1 := json.Unmarshal([]byte(policy.Policy), &expectedPolicy)
|
||||
funcAssert.NoError(err1)
|
||||
err2 := json.Unmarshal([]byte(assertPolicy.Policy), &actualPolicy)
|
||||
funcAssert.NoError(err2)
|
||||
funcAssert.Equal(expectedPolicy, actualPolicy)
|
||||
}
|
||||
// Test-2 : addPolicy() got an error while adding policy
|
||||
minioAddPolicyMock = func(_ string, _ *iampolicy.Policy) error {
|
||||
return errors.New("error")
|
||||
}
|
||||
if _, err := addPolicy(ctx, adminClient, policyName, policyDefinition); funcAssert.Error(err) {
|
||||
funcAssert.Equal("error", err.Error())
|
||||
}
|
||||
// Test-3 : addPolicy() got an error while retrieving policy
|
||||
minioAddPolicyMock = func(_ string, _ *iampolicy.Policy) error {
|
||||
return nil
|
||||
}
|
||||
minioGetPolicyMock = func(_ string) (*iampolicy.Policy, error) {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
if _, err := addPolicy(ctx, adminClient, policyName, policyDefinition); funcAssert.Error(err) {
|
||||
funcAssert.Equal("error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPolicy(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
funcAssert := assert.New(t)
|
||||
adminClient := AdminClientMock{}
|
||||
policyName := "readOnly"
|
||||
entityName := "alevsk"
|
||||
entityObject := models.PolicyEntityUser
|
||||
minioSetPolicyMock = func(_, _ string, _ bool) error {
|
||||
return nil
|
||||
}
|
||||
// Test-1 : SetPolicy() set policy to user
|
||||
function := "SetPolicy()"
|
||||
err := SetPolicy(ctx, adminClient, policyName, entityName, entityObject)
|
||||
if err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// Test-2 : SetPolicy() set policy to group
|
||||
entityObject = models.PolicyEntityGroup
|
||||
err = SetPolicy(ctx, adminClient, policyName, entityName, entityObject)
|
||||
if err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// Test-3 : SetPolicy() set policy to user and get error
|
||||
entityObject = models.PolicyEntityUser
|
||||
minioSetPolicyMock = func(_, _ string, _ bool) error {
|
||||
return errors.New("error")
|
||||
}
|
||||
if err := SetPolicy(ctx, adminClient, policyName, entityName, entityObject); funcAssert.Error(err) {
|
||||
funcAssert.Equal("error", err.Error())
|
||||
}
|
||||
// Test-4 : SetPolicy() set policy to group and get error
|
||||
entityObject = models.PolicyEntityGroup
|
||||
minioSetPolicyMock = func(_, _ string, _ bool) error {
|
||||
return errors.New("error")
|
||||
}
|
||||
if err := SetPolicy(ctx, adminClient, policyName, entityName, entityObject); funcAssert.Error(err) {
|
||||
funcAssert.Equal("error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SetPolicyMultiple(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
adminClient := AdminClientMock{}
|
||||
|
||||
type args struct {
|
||||
policyName string
|
||||
users []models.IamEntity
|
||||
groups []models.IamEntity
|
||||
setPolicyFunc func(policyName, entityName string, isGroup bool) error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
errorExpected error
|
||||
}{
|
||||
{
|
||||
name: "Set policy to multiple users and groups",
|
||||
args: args{
|
||||
policyName: "readonly",
|
||||
users: []models.IamEntity{"user1", "user2"},
|
||||
groups: []models.IamEntity{"group1", "group2"},
|
||||
setPolicyFunc: func(_, _ string, _ bool) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
errorExpected: nil,
|
||||
},
|
||||
{
|
||||
name: "Return error on set policy function",
|
||||
args: args{
|
||||
policyName: "readonly",
|
||||
users: []models.IamEntity{"user1", "user2"},
|
||||
groups: []models.IamEntity{"group1", "group2"},
|
||||
setPolicyFunc: func(_, _ string, _ bool) error {
|
||||
return errors.New("error set")
|
||||
},
|
||||
},
|
||||
errorExpected: errors.New("error set"),
|
||||
},
|
||||
{
|
||||
// Description: Empty lists of users and groups are acceptable
|
||||
name: "Empty lists of users and groups",
|
||||
args: args{
|
||||
policyName: "readonly",
|
||||
users: []models.IamEntity{},
|
||||
groups: []models.IamEntity{},
|
||||
setPolicyFunc: func(_, _ string, _ bool) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
errorExpected: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
minioSetPolicyMock = tt.args.setPolicyFunc
|
||||
got := setPolicyMultipleEntities(ctx, adminClient, tt.args.policyName, tt.args.users, tt.args.groups)
|
||||
if !reflect.DeepEqual(got, tt.errorExpected) {
|
||||
ji, _ := json.Marshal(got)
|
||||
vi, _ := json.Marshal(tt.errorExpected)
|
||||
t.Errorf("got %s want %s", ji, vi)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_policyMatchesBucket(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
policy *models.Policy
|
||||
bucket string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "Test1",
|
||||
args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"admin:*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:*"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`}, bucket: "test1"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Test2",
|
||||
args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:*"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::bucket1"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`}, bucket: "test1"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Test3",
|
||||
args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "VisualEditor0",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:ListStorageLensConfigurations",
|
||||
"s3:GetAccessPoint",
|
||||
"s3:PutAccountPublicAccessBlock",
|
||||
"s3:GetAccountPublicAccessBlock",
|
||||
"s3:ListAllMyBuckets",
|
||||
"s3:ListAccessPoints",
|
||||
"s3:ListJobs",
|
||||
"s3:PutStorageLensConfiguration",
|
||||
"s3:CreateJob"
|
||||
],
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Sid": "VisualEditor1",
|
||||
"Effect": "Allow",
|
||||
"Action": "s3:*",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::test",
|
||||
"arn:aws:s3:::test/*",
|
||||
"arn:aws:s3:::lkasdkljasd090901",
|
||||
"arn:aws:s3:::lkasdkljasd090901/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`}, bucket: "test1"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Test4",
|
||||
args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:*"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::bucket1"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`}, bucket: "bucket1"},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
if got := policyMatchesBucket(tt.args.ctx, tt.args.policy, tt.args.bucket); got != tt.want {
|
||||
t.Errorf("policyMatchesBucket() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
63
api/admin_profiling.go
Normal file
63
api/admin_profiling.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/websocket"
|
||||
)
|
||||
|
||||
var items []*models.StartProfilingItem
|
||||
|
||||
type profileOptions struct {
|
||||
Types string
|
||||
}
|
||||
|
||||
func getProfileOptionsFromReq(req *http.Request) (*profileOptions, error) {
|
||||
pOptions := profileOptions{}
|
||||
pOptions.Types = req.FormValue("types")
|
||||
return &pOptions, nil
|
||||
}
|
||||
|
||||
func startProfiling(ctx context.Context, conn WSConn, client MinioAdmin, pOpts *profileOptions) error {
|
||||
profilingResults, err := client.startProfiling(ctx, madmin.ProfilerType(pOpts.Types))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items = []*models.StartProfilingItem{}
|
||||
for _, result := range profilingResults {
|
||||
items = append(items, &models.StartProfilingItem{
|
||||
Success: result.Success,
|
||||
Error: result.Error,
|
||||
NodeName: result.NodeName,
|
||||
})
|
||||
}
|
||||
zippedData, err := client.stopProfiling(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
message, err := io.ReadAll(zippedData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return conn.writeMessage(websocket.BinaryMessage, message)
|
||||
}
|
||||
105
api/admin_profiling_test.go
Normal file
105
api/admin_profiling_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Implementing fake closingBuffer to mock stopProfiling() (io.ReadCloser, error)
|
||||
type ClosingBuffer struct {
|
||||
*bytes.Buffer
|
||||
}
|
||||
|
||||
// Implementing a fake Close function for io.ReadCloser
|
||||
func (cb *ClosingBuffer) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestStartProfiling(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
assert := assert.New(t)
|
||||
adminClient := AdminClientMock{}
|
||||
mockWSConn := mockConn{}
|
||||
function := "startProfiling()"
|
||||
testOptions := &profileOptions{
|
||||
Types: "cpu",
|
||||
}
|
||||
|
||||
// Test-1 : startProfiling() Get response from MinIO server with one profiling object without errors
|
||||
// mock function response from startProfiling()
|
||||
minioStartProfiling = func(_ madmin.ProfilerType) ([]madmin.StartProfilingResult, error) {
|
||||
return []madmin.StartProfilingResult{
|
||||
{
|
||||
NodeName: "http://127.0.0.1:9000/",
|
||||
Success: true,
|
||||
Error: "",
|
||||
},
|
||||
{
|
||||
NodeName: "http://127.0.0.1:9001/",
|
||||
Success: true,
|
||||
Error: "",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
// mock function response from stopProfiling()
|
||||
minioStopProfiling = func() (io.ReadCloser, error) {
|
||||
return &ClosingBuffer{bytes.NewBufferString("In memory string eaeae")}, nil
|
||||
}
|
||||
// mock function response from mockConn.writeMessage()
|
||||
connWriteMessageMock = func(_ int, _ []byte) error {
|
||||
return nil
|
||||
}
|
||||
err := startProfiling(ctx, mockWSConn, adminClient, testOptions)
|
||||
if err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
assert.Equal(err, nil)
|
||||
|
||||
// Test-2 : startProfiling() Correctly handles errors returned by MinIO
|
||||
// mock function response from startProfiling()
|
||||
minioStartProfiling = func(_ madmin.ProfilerType) ([]madmin.StartProfilingResult, error) {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
err = startProfiling(ctx, mockWSConn, adminClient, testOptions)
|
||||
if assert.Error(err) {
|
||||
assert.Equal("error", err.Error())
|
||||
}
|
||||
|
||||
// Test-3: getProfileOptionsFromReq() correctly returns profile options from request
|
||||
u, _ := url.Parse("ws://localhost/ws/profile?types=cpu,mem,block,mutex,trace,threads,goroutines")
|
||||
req := &http.Request{
|
||||
URL: u,
|
||||
}
|
||||
opts, err := getProfileOptionsFromReq(req)
|
||||
if assert.NoError(err) {
|
||||
expectedOptions := profileOptions{
|
||||
Types: "cpu,mem,block,mutex,trace,threads,goroutines",
|
||||
}
|
||||
assert.Equal(expectedOptions.Types, opts.Types)
|
||||
}
|
||||
}
|
||||
116
api/admin_releases.go
Normal file
116
api/admin_releases.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
release "github.com/minio/console/api/operations/release"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/pkg/v3/env"
|
||||
)
|
||||
|
||||
var (
|
||||
releaseServiceHostEnvVar = "RELEASE_SERVICE_HOST"
|
||||
defaultReleaseServiceHost = "https://enterprise-updates.ic.min.dev"
|
||||
)
|
||||
|
||||
func registerReleasesHandlers(api *operations.ConsoleAPI) {
|
||||
api.ReleaseListReleasesHandler = release.ListReleasesHandlerFunc(func(params release.ListReleasesParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := GetReleaseListResponse(session, params)
|
||||
if err != nil {
|
||||
return release.NewListReleasesDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return release.NewListReleasesOK().WithPayload(resp)
|
||||
})
|
||||
}
|
||||
|
||||
func GetReleaseListResponse(_ *models.Principal, params release.ListReleasesParams) (*models.ReleaseListResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
repo := params.Repo
|
||||
currentRelease := ""
|
||||
if params.Current != nil {
|
||||
currentRelease = *params.Current
|
||||
}
|
||||
search := ""
|
||||
if params.Search != nil {
|
||||
search = *params.Search
|
||||
}
|
||||
filter := ""
|
||||
if params.Filter != nil {
|
||||
filter = *params.Filter
|
||||
}
|
||||
ctx = context.WithValue(ctx, utils.ContextClientIP, getClientIP(params.HTTPRequest))
|
||||
return releaseList(ctx, repo, currentRelease, search, filter)
|
||||
}
|
||||
|
||||
func releaseList(ctx context.Context, repo, currentRelease, search, filter string) (*models.ReleaseListResponse, *CodedAPIError) {
|
||||
serviceURL := getReleaseServiceURL()
|
||||
clientIP := utils.ClientIPFromContext(ctx)
|
||||
releases, err := getReleases(serviceURL, repo, currentRelease, search, filter, clientIP)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return releases, nil
|
||||
}
|
||||
|
||||
func getReleaseServiceURL() string {
|
||||
host := env.Get(releaseServiceHostEnvVar, defaultReleaseServiceHost)
|
||||
return fmt.Sprintf("%s/releases", host)
|
||||
}
|
||||
|
||||
func getReleases(endpoint, repo, currentRelease, search, filter, clientIP string) (*models.ReleaseListResponse, error) {
|
||||
rl := &models.ReleaseListResponse{}
|
||||
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := &url.Values{}
|
||||
q.Add("repo", repo)
|
||||
q.Add("search", search)
|
||||
q.Add("filter", filter)
|
||||
q.Add("current", currentRelease)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := GetConsoleHTTPClient(clientIP)
|
||||
client.Timeout = time.Second * 5
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("error getting releases: %s", resp.Status)
|
||||
}
|
||||
err = json.NewDecoder(resp.Body).Decode(&rl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rl, nil
|
||||
}
|
||||
104
api/admin_releases_test.go
Normal file
104
api/admin_releases_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/console/api/operations"
|
||||
release "github.com/minio/console/api/operations/release"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ReleasesTestSuite struct {
|
||||
suite.Suite
|
||||
assert *assert.Assertions
|
||||
currentServer string
|
||||
isServerSet bool
|
||||
getServer *httptest.Server
|
||||
withError bool
|
||||
}
|
||||
|
||||
func (suite *ReleasesTestSuite) SetupSuite() {
|
||||
suite.assert = assert.New(suite.T())
|
||||
suite.getServer = httptest.NewServer(http.HandlerFunc(suite.getHandler))
|
||||
suite.currentServer, suite.isServerSet = os.LookupEnv(releaseServiceHostEnvVar)
|
||||
os.Setenv(releaseServiceHostEnvVar, suite.getServer.URL)
|
||||
}
|
||||
|
||||
func (suite *ReleasesTestSuite) TearDownSuite() {
|
||||
if suite.isServerSet {
|
||||
os.Setenv(releaseServiceHostEnvVar, suite.currentServer)
|
||||
} else {
|
||||
os.Unsetenv(releaseServiceHostEnvVar)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ReleasesTestSuite) getHandler(
|
||||
w http.ResponseWriter, _ *http.Request,
|
||||
) {
|
||||
if suite.withError {
|
||||
w.WriteHeader(400)
|
||||
} else {
|
||||
w.WriteHeader(200)
|
||||
response := &models.ReleaseListResponse{}
|
||||
bytes, _ := json.Marshal(response)
|
||||
fmt.Fprint(w, string(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ReleasesTestSuite) TestRegisterReleasesHandlers() {
|
||||
api := &operations.ConsoleAPI{}
|
||||
suite.assert.Nil(api.ReleaseListReleasesHandler)
|
||||
registerReleasesHandlers(api)
|
||||
suite.assert.NotNil(api.ReleaseListReleasesHandler)
|
||||
}
|
||||
|
||||
func (suite *ReleasesTestSuite) TestGetReleasesWithError() {
|
||||
api := &operations.ConsoleAPI{}
|
||||
current := "mock"
|
||||
registerReleasesHandlers(api)
|
||||
params := release.NewListReleasesParams()
|
||||
params.Current = ¤t
|
||||
params.HTTPRequest = &http.Request{}
|
||||
suite.withError = true
|
||||
response := api.ReleaseListReleasesHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*release.ListReleasesDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *ReleasesTestSuite) TestGetReleasesWithoutError() {
|
||||
api := &operations.ConsoleAPI{}
|
||||
registerReleasesHandlers(api)
|
||||
params := release.NewListReleasesParams()
|
||||
params.HTTPRequest = &http.Request{}
|
||||
suite.withError = false
|
||||
response := api.ReleaseListReleasesHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*release.ListReleasesOK)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func TestReleases(t *testing.T) {
|
||||
suite.Run(t, new(ReleasesTestSuite))
|
||||
}
|
||||
810
api/admin_remote_buckets.go
Normal file
810
api/admin_remote_buckets.go
Normal file
@@ -0,0 +1,810 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/minio/console/api/operations"
|
||||
bucketApi "github.com/minio/console/api/operations/bucket"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/minio-go/v7/pkg/replication"
|
||||
)
|
||||
|
||||
type RemoteBucketResult struct {
|
||||
OriginBucket string
|
||||
TargetBucket string
|
||||
Error string
|
||||
}
|
||||
|
||||
func registerAdminBucketRemoteHandlers(api *operations.ConsoleAPI) {
|
||||
// return list of remote buckets
|
||||
api.BucketListRemoteBucketsHandler = bucketApi.ListRemoteBucketsHandlerFunc(func(params bucketApi.ListRemoteBucketsParams, session *models.Principal) middleware.Responder {
|
||||
listResp, err := getListRemoteBucketsResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewListRemoteBucketsDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewListRemoteBucketsOK().WithPayload(listResp)
|
||||
})
|
||||
|
||||
// return information about a specific bucket
|
||||
api.BucketRemoteBucketDetailsHandler = bucketApi.RemoteBucketDetailsHandlerFunc(func(params bucketApi.RemoteBucketDetailsParams, session *models.Principal) middleware.Responder {
|
||||
response, err := getRemoteBucketDetailsResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewRemoteBucketDetailsDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewRemoteBucketDetailsOK().WithPayload(response)
|
||||
})
|
||||
|
||||
// delete remote bucket
|
||||
api.BucketDeleteRemoteBucketHandler = bucketApi.DeleteRemoteBucketHandlerFunc(func(params bucketApi.DeleteRemoteBucketParams, session *models.Principal) middleware.Responder {
|
||||
err := getDeleteRemoteBucketResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewDeleteRemoteBucketDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewDeleteRemoteBucketNoContent()
|
||||
})
|
||||
|
||||
// set remote bucket
|
||||
api.BucketAddRemoteBucketHandler = bucketApi.AddRemoteBucketHandlerFunc(func(params bucketApi.AddRemoteBucketParams, session *models.Principal) middleware.Responder {
|
||||
err := getAddRemoteBucketResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewAddRemoteBucketDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewAddRemoteBucketCreated()
|
||||
})
|
||||
|
||||
// set multi-bucket replication
|
||||
api.BucketSetMultiBucketReplicationHandler = bucketApi.SetMultiBucketReplicationHandlerFunc(func(params bucketApi.SetMultiBucketReplicationParams, session *models.Principal) middleware.Responder {
|
||||
response, err := setMultiBucketReplicationResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewSetMultiBucketReplicationDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
|
||||
return bucketApi.NewSetMultiBucketReplicationOK().WithPayload(response)
|
||||
})
|
||||
|
||||
// list external buckets
|
||||
api.BucketListExternalBucketsHandler = bucketApi.ListExternalBucketsHandlerFunc(func(params bucketApi.ListExternalBucketsParams, _ *models.Principal) middleware.Responder {
|
||||
response, err := listExternalBucketsResponse(params)
|
||||
if err != nil {
|
||||
return bucketApi.NewListExternalBucketsDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
|
||||
return bucketApi.NewListExternalBucketsOK().WithPayload(response)
|
||||
})
|
||||
|
||||
// delete replication rule
|
||||
api.BucketDeleteBucketReplicationRuleHandler = bucketApi.DeleteBucketReplicationRuleHandlerFunc(func(params bucketApi.DeleteBucketReplicationRuleParams, session *models.Principal) middleware.Responder {
|
||||
err := deleteReplicationRuleResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewDeleteBucketReplicationRuleDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
|
||||
return bucketApi.NewDeleteBucketReplicationRuleNoContent()
|
||||
})
|
||||
|
||||
// delete all replication rules for a bucket
|
||||
api.BucketDeleteAllReplicationRulesHandler = bucketApi.DeleteAllReplicationRulesHandlerFunc(func(params bucketApi.DeleteAllReplicationRulesParams, session *models.Principal) middleware.Responder {
|
||||
err := deleteBucketReplicationRulesResponse(session, params)
|
||||
if err != nil {
|
||||
if err.Code == 500 && err.APIError.DetailedMessage == "The remote target does not exist" {
|
||||
// We should ignore this MinIO error when deleting all replication rules
|
||||
return bucketApi.NewDeleteAllReplicationRulesNoContent() // This will return 204 as per swagger spec
|
||||
}
|
||||
// If there is a different error, then we should handle it
|
||||
// This will return a generic error with err.Code (likely a 500 or 404) and its *err.DetailedMessage
|
||||
return bucketApi.NewDeleteAllReplicationRulesDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewDeleteAllReplicationRulesNoContent()
|
||||
})
|
||||
|
||||
// delete selected replication rules for a bucket
|
||||
api.BucketDeleteSelectedReplicationRulesHandler = bucketApi.DeleteSelectedReplicationRulesHandlerFunc(func(params bucketApi.DeleteSelectedReplicationRulesParams, session *models.Principal) middleware.Responder {
|
||||
err := deleteSelectedReplicationRulesResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewDeleteSelectedReplicationRulesDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
|
||||
return bucketApi.NewDeleteSelectedReplicationRulesNoContent()
|
||||
})
|
||||
|
||||
// update local bucket replication config item
|
||||
api.BucketUpdateMultiBucketReplicationHandler = bucketApi.UpdateMultiBucketReplicationHandlerFunc(func(params bucketApi.UpdateMultiBucketReplicationParams, session *models.Principal) middleware.Responder {
|
||||
err := updateBucketReplicationResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewUpdateMultiBucketReplicationDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewUpdateMultiBucketReplicationCreated()
|
||||
})
|
||||
}
|
||||
|
||||
func getListRemoteBucketsResponse(session *models.Principal, params bucketApi.ListRemoteBucketsParams) (*models.ListRemoteBucketsResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err))
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
return listRemoteBuckets(ctx, adminClient)
|
||||
}
|
||||
|
||||
func getRemoteBucketDetailsResponse(session *models.Principal, params bucketApi.RemoteBucketDetailsParams) (*models.RemoteBucket, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err))
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
return getRemoteBucket(ctx, adminClient, params.Name)
|
||||
}
|
||||
|
||||
func getDeleteRemoteBucketResponse(session *models.Principal, params bucketApi.DeleteRemoteBucketParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err))
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
err = deleteRemoteBucket(ctx, adminClient, params.SourceBucketName, params.Arn)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, fmt.Errorf("error deleting remote bucket: %v", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAddRemoteBucketResponse(session *models.Principal, params bucketApi.AddRemoteBucketParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err))
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
_, err = addRemoteBucket(ctx, adminClient, *params.Body)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, fmt.Errorf("error adding remote bucket: %v", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func listRemoteBuckets(ctx context.Context, client MinioAdmin) (*models.ListRemoteBucketsResponse, *CodedAPIError) {
|
||||
var remoteBuckets []*models.RemoteBucket
|
||||
buckets, err := client.listRemoteBuckets(ctx, "", "")
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, fmt.Errorf("error listing remote buckets: %v", err))
|
||||
}
|
||||
for _, bucket := range buckets {
|
||||
remoteBucket := &models.RemoteBucket{
|
||||
AccessKey: swag.String(bucket.Credentials.AccessKey),
|
||||
RemoteARN: swag.String(bucket.Arn),
|
||||
SecretKey: bucket.Credentials.SecretKey,
|
||||
Service: "replication",
|
||||
SourceBucket: swag.String(bucket.SourceBucket),
|
||||
Status: "",
|
||||
TargetBucket: bucket.TargetBucket,
|
||||
TargetURL: bucket.Endpoint,
|
||||
SyncMode: "async",
|
||||
Bandwidth: bucket.BandwidthLimit,
|
||||
HealthCheckPeriod: int64(bucket.HealthCheckDuration.Seconds()),
|
||||
}
|
||||
if bucket.ReplicationSync {
|
||||
remoteBucket.SyncMode = "sync"
|
||||
}
|
||||
remoteBuckets = append(remoteBuckets, remoteBucket)
|
||||
}
|
||||
|
||||
return &models.ListRemoteBucketsResponse{
|
||||
Buckets: remoteBuckets,
|
||||
Total: int64(len(remoteBuckets)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getRemoteBucket(ctx context.Context, client MinioAdmin, name string) (*models.RemoteBucket, *CodedAPIError) {
|
||||
remoteBucket, err := client.getRemoteBucket(ctx, name, "")
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, fmt.Errorf("error getting remote bucket details: %v", err))
|
||||
}
|
||||
if remoteBucket == nil {
|
||||
return nil, ErrorWithContext(ctx, "error getting remote bucket details: bucket not found")
|
||||
}
|
||||
return &models.RemoteBucket{
|
||||
AccessKey: &remoteBucket.Credentials.AccessKey,
|
||||
RemoteARN: &remoteBucket.Arn,
|
||||
SecretKey: remoteBucket.Credentials.SecretKey,
|
||||
Service: "replication",
|
||||
SourceBucket: &remoteBucket.SourceBucket,
|
||||
Status: "",
|
||||
TargetBucket: remoteBucket.TargetBucket,
|
||||
TargetURL: remoteBucket.Endpoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func deleteRemoteBucket(ctx context.Context, client MinioAdmin, sourceBucketName, arn string) error {
|
||||
return client.removeRemoteBucket(ctx, sourceBucketName, arn)
|
||||
}
|
||||
|
||||
func addRemoteBucket(ctx context.Context, client MinioAdmin, params models.CreateRemoteBucket) (string, error) {
|
||||
TargetURL := *params.TargetURL
|
||||
accessKey := *params.AccessKey
|
||||
secretKey := *params.SecretKey
|
||||
u, err := url.Parse(TargetURL)
|
||||
if err != nil {
|
||||
return "", errors.New("malformed Remote target URL")
|
||||
}
|
||||
secure := u.Scheme == "https"
|
||||
host := u.Host
|
||||
if u.Port() == "" {
|
||||
port := 80
|
||||
if secure {
|
||||
port = 443
|
||||
}
|
||||
host = host + ":" + strconv.Itoa(port)
|
||||
}
|
||||
creds := &madmin.Credentials{AccessKey: accessKey, SecretKey: secretKey}
|
||||
remoteBucket := &madmin.BucketTarget{
|
||||
TargetBucket: *params.TargetBucket,
|
||||
Secure: secure,
|
||||
Credentials: creds,
|
||||
Endpoint: host,
|
||||
Path: "",
|
||||
API: "s3v4",
|
||||
Type: "replication",
|
||||
Region: params.Region,
|
||||
ReplicationSync: *params.SyncMode == "sync",
|
||||
}
|
||||
if *params.SyncMode == "async" {
|
||||
remoteBucket.BandwidthLimit = params.Bandwidth
|
||||
}
|
||||
if params.HealthCheckPeriod > 0 {
|
||||
remoteBucket.HealthCheckDuration = time.Duration(params.HealthCheckPeriod) * time.Second
|
||||
}
|
||||
bucketARN, err := client.addRemoteBucket(ctx, *params.SourceBucket, remoteBucket)
|
||||
|
||||
return bucketARN, err
|
||||
}
|
||||
|
||||
func addBucketReplicationItem(ctx context.Context, session *models.Principal, minClient minioClient, bucketName, prefix, destinationARN string, repExistingObj, repDelMark, repDels, repMeta bool, tags string, priority int32, storageClass string) error {
|
||||
// we will tolerate this call failing
|
||||
cfg, err := minClient.getBucketReplication(ctx, bucketName)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("error fetching replication configuration for bucket %s: %v", bucketName, err))
|
||||
}
|
||||
|
||||
// add rule
|
||||
maxPrio := 0
|
||||
|
||||
if priority <= 0 { // We pick next priority by default
|
||||
for _, r := range cfg.Rules {
|
||||
if r.Priority > maxPrio {
|
||||
maxPrio = r.Priority
|
||||
}
|
||||
}
|
||||
maxPrio++
|
||||
} else { // User picked priority, we try to set this manually
|
||||
maxPrio = int(priority)
|
||||
}
|
||||
clientIP := utils.ClientIPFromContext(ctx)
|
||||
s3Client, err := newS3BucketClient(session, bucketName, prefix, clientIP)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("error creating S3Client: %v", err))
|
||||
return err
|
||||
}
|
||||
// create a mc S3Client interface implementation
|
||||
// defining the client to be used
|
||||
mcClient := mcClient{client: s3Client}
|
||||
|
||||
repDelMarkStatus := "disable"
|
||||
if repDelMark {
|
||||
repDelMarkStatus = "enable"
|
||||
}
|
||||
|
||||
repDelsStatus := "disable"
|
||||
if repDels {
|
||||
repDelsStatus = "enable"
|
||||
}
|
||||
|
||||
repMetaStatus := "disable"
|
||||
if repMeta {
|
||||
repMetaStatus = "enable"
|
||||
}
|
||||
|
||||
existingRepStatus := "disable"
|
||||
if repExistingObj {
|
||||
existingRepStatus = "enable"
|
||||
}
|
||||
|
||||
opts := replication.Options{
|
||||
Priority: fmt.Sprintf("%d", maxPrio),
|
||||
RuleStatus: "enable",
|
||||
DestBucket: destinationARN,
|
||||
Op: replication.AddOption,
|
||||
TagString: tags,
|
||||
ExistingObjectReplicate: existingRepStatus,
|
||||
ReplicateDeleteMarkers: repDelMarkStatus,
|
||||
ReplicateDeletes: repDelsStatus,
|
||||
ReplicaSync: repMetaStatus,
|
||||
StorageClass: storageClass,
|
||||
}
|
||||
|
||||
err2 := mcClient.setReplication(ctx, &cfg, opts)
|
||||
if err2 != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("error creating replication for bucket: %v", err2.Cause))
|
||||
return err2.Cause
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func editBucketReplicationItem(ctx context.Context, session *models.Principal, minClient minioClient, ruleID, bucketName, prefix, destinationARN string, ruleStatus, repDelMark, repDels, repMeta, existingObjectRep bool, tags string, priority int32, storageClass string) error {
|
||||
// we will tolerate this call failing
|
||||
cfg, err := minClient.getBucketReplication(ctx, bucketName)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("error fetching replication configuration for bucket %s: %v", bucketName, err))
|
||||
}
|
||||
|
||||
maxPrio := int(priority)
|
||||
|
||||
clientIP := utils.ClientIPFromContext(ctx)
|
||||
s3Client, err := newS3BucketClient(session, bucketName, prefix, clientIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating S3Client: %v", err)
|
||||
}
|
||||
// create a mc S3Client interface implementation
|
||||
// defining the client to be used
|
||||
mcClient := mcClient{client: s3Client}
|
||||
|
||||
ruleState := "disable"
|
||||
if ruleStatus {
|
||||
ruleState = "enable"
|
||||
}
|
||||
|
||||
repDelMarkStatus := "disable"
|
||||
if repDelMark {
|
||||
repDelMarkStatus = "enable"
|
||||
}
|
||||
|
||||
repDelsStatus := "disable"
|
||||
if repDels {
|
||||
repDelsStatus = "enable"
|
||||
}
|
||||
|
||||
repMetaStatus := "disable"
|
||||
if repMeta {
|
||||
repMetaStatus = "enable"
|
||||
}
|
||||
|
||||
existingRepStatus := "disable"
|
||||
if existingObjectRep {
|
||||
existingRepStatus = "enable"
|
||||
}
|
||||
|
||||
opts := replication.Options{
|
||||
ID: ruleID,
|
||||
Priority: fmt.Sprintf("%d", maxPrio),
|
||||
RuleStatus: ruleState,
|
||||
DestBucket: destinationARN,
|
||||
Op: replication.SetOption,
|
||||
TagString: tags,
|
||||
IsTagSet: true,
|
||||
ExistingObjectReplicate: existingRepStatus,
|
||||
ReplicateDeleteMarkers: repDelMarkStatus,
|
||||
ReplicateDeletes: repDelsStatus,
|
||||
ReplicaSync: repMetaStatus,
|
||||
StorageClass: storageClass,
|
||||
IsSCSet: true,
|
||||
}
|
||||
|
||||
err2 := mcClient.setReplication(ctx, &cfg, opts)
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("error modifying replication for bucket: %v", err2.Cause)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setMultiBucketReplication(ctx context.Context, session *models.Principal, client MinioAdmin, minClient minioClient, params bucketApi.SetMultiBucketReplicationParams) []RemoteBucketResult {
|
||||
bucketsRelation := params.Body.BucketsRelation
|
||||
|
||||
// Parallel remote bucket adding
|
||||
parallelRemoteBucket := func(bucketRelationData *models.MultiBucketsRelation) chan RemoteBucketResult {
|
||||
remoteProc := make(chan RemoteBucketResult)
|
||||
sourceBucket := bucketRelationData.OriginBucket
|
||||
targetBucket := bucketRelationData.DestinationBucket
|
||||
|
||||
go func() {
|
||||
defer close(remoteProc)
|
||||
|
||||
createRemoteBucketParams := models.CreateRemoteBucket{
|
||||
AccessKey: params.Body.AccessKey,
|
||||
SecretKey: params.Body.SecretKey,
|
||||
SourceBucket: &sourceBucket,
|
||||
TargetBucket: &targetBucket,
|
||||
Region: params.Body.Region,
|
||||
TargetURL: params.Body.TargetURL,
|
||||
SyncMode: params.Body.SyncMode,
|
||||
Bandwidth: params.Body.Bandwidth,
|
||||
HealthCheckPeriod: params.Body.HealthCheckPeriod,
|
||||
}
|
||||
|
||||
// We add the remote bucket reference & store the arn or errors returned
|
||||
arn, err := addRemoteBucket(ctx, client, createRemoteBucketParams)
|
||||
|
||||
if err == nil {
|
||||
err = addBucketReplicationItem(
|
||||
ctx,
|
||||
session,
|
||||
minClient,
|
||||
sourceBucket,
|
||||
params.Body.Prefix,
|
||||
arn,
|
||||
params.Body.ReplicateExistingObjects,
|
||||
params.Body.ReplicateDeleteMarkers,
|
||||
params.Body.ReplicateDeletes,
|
||||
params.Body.ReplicateMetadata,
|
||||
params.Body.Tags,
|
||||
params.Body.Priority,
|
||||
params.Body.StorageClass)
|
||||
}
|
||||
|
||||
errorReturn := ""
|
||||
|
||||
if err != nil {
|
||||
deleteRemoteBucket(ctx, client, sourceBucket, arn)
|
||||
errorReturn = err.Error()
|
||||
}
|
||||
|
||||
retParams := RemoteBucketResult{
|
||||
OriginBucket: sourceBucket,
|
||||
TargetBucket: targetBucket,
|
||||
Error: errorReturn,
|
||||
}
|
||||
|
||||
remoteProc <- retParams
|
||||
}()
|
||||
return remoteProc
|
||||
}
|
||||
|
||||
var bucketsManagement []chan RemoteBucketResult
|
||||
|
||||
for _, bucketName := range bucketsRelation {
|
||||
// We generate the ARNs for each bucket
|
||||
rBucket := parallelRemoteBucket(bucketName)
|
||||
bucketsManagement = append(bucketsManagement, rBucket)
|
||||
}
|
||||
|
||||
resultsList := []RemoteBucketResult{}
|
||||
for _, result := range bucketsManagement {
|
||||
res := <-result
|
||||
resultsList = append(resultsList, res)
|
||||
}
|
||||
|
||||
return resultsList
|
||||
}
|
||||
|
||||
func setMultiBucketReplicationResponse(session *models.Principal, params bucketApi.SetMultiBucketReplicationParams) (*models.MultiBucketResponseState, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err))
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, fmt.Errorf("error creating MinIO Client: %v", err))
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
mnClient := minioClient{client: mClient}
|
||||
|
||||
replicationResults := setMultiBucketReplication(ctx, session, adminClient, mnClient, params)
|
||||
|
||||
if replicationResults == nil {
|
||||
return nil, ErrorWithContext(ctx, errors.New("error setting buckets replication"))
|
||||
}
|
||||
|
||||
resParsed := []*models.MultiBucketResponseItem{}
|
||||
|
||||
for _, repResult := range replicationResults {
|
||||
responseItem := models.MultiBucketResponseItem{
|
||||
ErrorString: repResult.Error,
|
||||
OriginBucket: repResult.OriginBucket,
|
||||
TargetBucket: repResult.TargetBucket,
|
||||
}
|
||||
|
||||
resParsed = append(resParsed, &responseItem)
|
||||
}
|
||||
|
||||
resultsParsed := models.MultiBucketResponseState{
|
||||
ReplicationState: resParsed,
|
||||
}
|
||||
|
||||
return &resultsParsed, nil
|
||||
}
|
||||
|
||||
func listExternalBucketsResponse(params bucketApi.ListExternalBucketsParams) (*models.ListBucketsResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
remoteAdmin, err := newAdminFromCreds(*params.Body.AccessKey, *params.Body.SecretKey, *params.Body.TargetURL, *params.Body.UseTLS)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return listExternalBuckets(ctx, AdminClient{Client: remoteAdmin})
|
||||
}
|
||||
|
||||
func listExternalBuckets(ctx context.Context, client MinioAdmin) (*models.ListBucketsResponse, *CodedAPIError) {
|
||||
buckets, err := getAccountBuckets(ctx, client)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
return &models.ListBucketsResponse{
|
||||
Buckets: buckets,
|
||||
Total: int64(len(buckets)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getARNFromID(conf *replication.Config, rule string) string {
|
||||
for i := range conf.Rules {
|
||||
if conf.Rules[i].ID == rule {
|
||||
return conf.Rules[i].Destination.Bucket
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getARNsFromIDs(conf *replication.Config, rules []string) []string {
|
||||
temp := make(map[string]string)
|
||||
for i := range conf.Rules {
|
||||
temp[conf.Rules[i].ID] = conf.Rules[i].Destination.Bucket
|
||||
}
|
||||
var retval []string
|
||||
for i := range rules {
|
||||
if val, ok := temp[rules[i]]; ok {
|
||||
retval = append(retval, val)
|
||||
}
|
||||
}
|
||||
return retval
|
||||
}
|
||||
|
||||
func deleteReplicationRule(ctx context.Context, session *models.Principal, bucketName, ruleID string) error {
|
||||
clientIP := utils.ClientIPFromContext(ctx)
|
||||
mClient, err := newMinioClient(session, clientIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating MinIO Client: %v", err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
minClient := minioClient{client: mClient}
|
||||
|
||||
cfg, err := minClient.getBucketReplication(ctx, bucketName)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("error versioning bucket: %v", err))
|
||||
}
|
||||
|
||||
s3Client, err := newS3BucketClient(session, bucketName, "", clientIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating S3Client: %v", err)
|
||||
}
|
||||
mAdmin, err := NewMinioAdminClient(ctx, session)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating Admin Client: %v", err)
|
||||
}
|
||||
admClient := AdminClient{Client: mAdmin}
|
||||
|
||||
// create a mc S3Client interface implementation
|
||||
// defining the client to be used
|
||||
mcClient := mcClient{client: s3Client}
|
||||
|
||||
opts := replication.Options{
|
||||
ID: ruleID,
|
||||
Op: replication.RemoveOption,
|
||||
}
|
||||
|
||||
err2 := mcClient.setReplication(ctx, &cfg, opts)
|
||||
if err2 != nil {
|
||||
return err2.Cause
|
||||
}
|
||||
|
||||
// Replication rule was successfully deleted. We remove remote bucket
|
||||
err3 := deleteRemoteBucket(ctx, admClient, bucketName, getARNFromID(&cfg, ruleID))
|
||||
if err3 != nil {
|
||||
return err3
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteAllReplicationRules(ctx context.Context, session *models.Principal, bucketName string) error {
|
||||
clientIP := utils.ClientIPFromContext(ctx)
|
||||
|
||||
s3Client, err := newS3BucketClient(session, bucketName, "", clientIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating S3Client: %v", err)
|
||||
}
|
||||
// create a mc S3Client interface implementation
|
||||
// defining the client to be used
|
||||
mcClient := mcClient{client: s3Client}
|
||||
mClient, err := newMinioClient(session, clientIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating MinIO Client: %v", err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
minClient := minioClient{client: mClient}
|
||||
|
||||
cfg, err := minClient.getBucketReplication(ctx, bucketName)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("error versioning bucket: %v", err))
|
||||
}
|
||||
|
||||
mAdmin, err := NewMinioAdminClient(ctx, session)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating Admin Client: %v", err)
|
||||
}
|
||||
admClient := AdminClient{Client: mAdmin}
|
||||
|
||||
err2 := mcClient.deleteAllReplicationRules(ctx)
|
||||
|
||||
if err2 != nil {
|
||||
return err2.ToGoError()
|
||||
}
|
||||
|
||||
for i := range cfg.Rules {
|
||||
err3 := deleteRemoteBucket(ctx, admClient, bucketName, cfg.Rules[i].Destination.Bucket)
|
||||
if err3 != nil {
|
||||
return err3
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteSelectedReplicationRules(ctx context.Context, session *models.Principal, bucketName string, rules []string) error {
|
||||
clientIP := utils.ClientIPFromContext(ctx)
|
||||
mClient, err := newMinioClient(session, clientIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating MinIO Client: %v", err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
minClient := minioClient{client: mClient}
|
||||
|
||||
cfg, err := minClient.getBucketReplication(ctx, bucketName)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("error versioning bucket: %v", err))
|
||||
}
|
||||
|
||||
s3Client, err := newS3BucketClient(session, bucketName, "", clientIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating S3Client: %v", err)
|
||||
}
|
||||
// create a mc S3Client interface implementation
|
||||
// defining the client to be used
|
||||
mcClient := mcClient{client: s3Client}
|
||||
|
||||
mAdmin, err := NewMinioAdminClient(ctx, session)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating Admin Client: %v", err)
|
||||
}
|
||||
admClient := AdminClient{Client: mAdmin}
|
||||
|
||||
ARNs := getARNsFromIDs(&cfg, rules)
|
||||
|
||||
for i := range rules {
|
||||
opts := replication.Options{
|
||||
ID: rules[i],
|
||||
Op: replication.RemoveOption,
|
||||
}
|
||||
err2 := mcClient.setReplication(ctx, &cfg, opts)
|
||||
if err2 != nil {
|
||||
return err2.Cause
|
||||
}
|
||||
|
||||
// In case replication rule was deleted successfully, we remove the remote bucket ARN
|
||||
err3 := deleteRemoteBucket(ctx, admClient, bucketName, ARNs[i])
|
||||
if err3 != nil {
|
||||
return err3
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteReplicationRuleResponse(session *models.Principal, params bucketApi.DeleteBucketReplicationRuleParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
ctx = context.WithValue(ctx, utils.ContextClientIP, getClientIP(params.HTTPRequest))
|
||||
err := deleteReplicationRule(ctx, session, params.BucketName, params.RuleID)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteBucketReplicationRulesResponse(session *models.Principal, params bucketApi.DeleteAllReplicationRulesParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
ctx = context.WithValue(ctx, utils.ContextClientIP, getClientIP(params.HTTPRequest))
|
||||
err := deleteAllReplicationRules(ctx, session, params.BucketName)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteSelectedReplicationRulesResponse(session *models.Principal, params bucketApi.DeleteSelectedReplicationRulesParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
ctx = context.WithValue(ctx, utils.ContextClientIP, getClientIP(params.HTTPRequest))
|
||||
|
||||
err := deleteSelectedReplicationRules(ctx, session, params.BucketName, params.Rules.Rules)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateBucketReplicationResponse(session *models.Principal, params bucketApi.UpdateMultiBucketReplicationParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
minClient := minioClient{client: mClient}
|
||||
|
||||
err = editBucketReplicationItem(
|
||||
ctx,
|
||||
session,
|
||||
minClient,
|
||||
params.RuleID,
|
||||
params.BucketName,
|
||||
params.Body.Prefix,
|
||||
params.Body.Arn,
|
||||
params.Body.RuleState,
|
||||
params.Body.ReplicateDeleteMarkers,
|
||||
params.Body.ReplicateDeletes,
|
||||
params.Body.ReplicateMetadata,
|
||||
params.Body.ReplicateExistingObjects,
|
||||
params.Body.Tags,
|
||||
params.Body.Priority,
|
||||
params.Body.StorageClass)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
386
api/admin_remote_buckets_test.go
Normal file
386
api/admin_remote_buckets_test.go
Normal file
@@ -0,0 +1,386 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/minio/console/api/operations"
|
||||
bucketApi "github.com/minio/console/api/operations/bucket"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type RemoteBucketsTestSuite struct {
|
||||
suite.Suite
|
||||
assert *assert.Assertions
|
||||
currentServer string
|
||||
isServerSet bool
|
||||
server *httptest.Server
|
||||
adminClient AdminClientMock
|
||||
minioClient minioClientMock
|
||||
mockRemoteBucket *models.RemoteBucket
|
||||
mockBucketTarget *madmin.BucketTarget
|
||||
mockListBuckets *models.ListBucketsResponse
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) SetupSuite() {
|
||||
suite.assert = assert.New(suite.T())
|
||||
suite.adminClient = AdminClientMock{}
|
||||
suite.minioClient = minioClientMock{}
|
||||
suite.mockObjects()
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) mockObjects() {
|
||||
suite.mockListBuckets = &models.ListBucketsResponse{
|
||||
Buckets: []*models.Bucket{},
|
||||
Total: 0,
|
||||
}
|
||||
suite.mockRemoteBucket = &models.RemoteBucket{
|
||||
AccessKey: swag.String("accessKey"),
|
||||
SecretKey: "secretKey",
|
||||
RemoteARN: swag.String("remoteARN"),
|
||||
Service: "replication",
|
||||
SourceBucket: swag.String("sourceBucket"),
|
||||
TargetBucket: "targetBucket",
|
||||
TargetURL: "targetURL",
|
||||
Status: "",
|
||||
}
|
||||
suite.mockBucketTarget = &madmin.BucketTarget{
|
||||
Credentials: &madmin.Credentials{
|
||||
AccessKey: *suite.mockRemoteBucket.AccessKey,
|
||||
SecretKey: suite.mockRemoteBucket.SecretKey,
|
||||
},
|
||||
Arn: *suite.mockRemoteBucket.RemoteARN,
|
||||
SourceBucket: *suite.mockRemoteBucket.SourceBucket,
|
||||
TargetBucket: suite.mockRemoteBucket.TargetBucket,
|
||||
Endpoint: suite.mockRemoteBucket.TargetURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) SetupTest() {
|
||||
suite.server = httptest.NewServer(http.HandlerFunc(suite.serverHandler))
|
||||
suite.currentServer, suite.isServerSet = os.LookupEnv(ConsoleMinIOServer)
|
||||
os.Setenv(ConsoleMinIOServer, suite.server.URL)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) serverHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(400)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TearDownSuite() {
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TearDownTest() {
|
||||
if suite.isServerSet {
|
||||
os.Setenv(ConsoleMinIOServer, suite.currentServer)
|
||||
} else {
|
||||
os.Unsetenv(ConsoleMinIOServer)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestRegisterRemoteBucketsHandlers() {
|
||||
api := &operations.ConsoleAPI{}
|
||||
suite.assertHandlersAreNil(api)
|
||||
registerAdminBucketRemoteHandlers(api)
|
||||
suite.assertHandlersAreNotNil(api)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) assertHandlersAreNil(api *operations.ConsoleAPI) {
|
||||
suite.assert.Nil(api.BucketListRemoteBucketsHandler)
|
||||
suite.assert.Nil(api.BucketRemoteBucketDetailsHandler)
|
||||
suite.assert.Nil(api.BucketDeleteRemoteBucketHandler)
|
||||
suite.assert.Nil(api.BucketAddRemoteBucketHandler)
|
||||
suite.assert.Nil(api.BucketSetMultiBucketReplicationHandler)
|
||||
suite.assert.Nil(api.BucketListExternalBucketsHandler)
|
||||
suite.assert.Nil(api.BucketDeleteBucketReplicationRuleHandler)
|
||||
suite.assert.Nil(api.BucketDeleteAllReplicationRulesHandler)
|
||||
suite.assert.Nil(api.BucketDeleteSelectedReplicationRulesHandler)
|
||||
suite.assert.Nil(api.BucketUpdateMultiBucketReplicationHandler)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) assertHandlersAreNotNil(api *operations.ConsoleAPI) {
|
||||
suite.assert.NotNil(api.BucketListRemoteBucketsHandler)
|
||||
suite.assert.NotNil(api.BucketRemoteBucketDetailsHandler)
|
||||
suite.assert.NotNil(api.BucketDeleteRemoteBucketHandler)
|
||||
suite.assert.NotNil(api.BucketAddRemoteBucketHandler)
|
||||
suite.assert.NotNil(api.BucketSetMultiBucketReplicationHandler)
|
||||
suite.assert.NotNil(api.BucketListExternalBucketsHandler)
|
||||
suite.assert.NotNil(api.BucketDeleteBucketReplicationRuleHandler)
|
||||
suite.assert.NotNil(api.BucketDeleteAllReplicationRulesHandler)
|
||||
suite.assert.NotNil(api.BucketDeleteSelectedReplicationRulesHandler)
|
||||
suite.assert.NotNil(api.BucketUpdateMultiBucketReplicationHandler)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestListRemoteBucketsHandlerWithError() {
|
||||
params, api := suite.initListRemoteBucketsRequest()
|
||||
response := api.BucketListRemoteBucketsHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*bucketApi.ListRemoteBucketsDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) initListRemoteBucketsRequest() (params bucketApi.ListRemoteBucketsParams, api operations.ConsoleAPI) {
|
||||
registerAdminBucketRemoteHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestListRemoteBucketsWithoutError() {
|
||||
ctx := context.Background()
|
||||
minioListRemoteBucketsMock = func(_ context.Context, _, _ string) (targets []madmin.BucketTarget, err error) {
|
||||
return []madmin.BucketTarget{{
|
||||
Credentials: &madmin.Credentials{
|
||||
AccessKey: "accessKey",
|
||||
SecretKey: "secretKey",
|
||||
},
|
||||
}}, nil
|
||||
}
|
||||
res, err := listRemoteBuckets(ctx, &suite.adminClient)
|
||||
suite.assert.NotNil(res)
|
||||
suite.assert.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestRemoteBucketDetailsHandlerWithError() {
|
||||
params, api := suite.initRemoteBucketDetailsRequest()
|
||||
response := api.BucketRemoteBucketDetailsHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*bucketApi.RemoteBucketDetailsDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) initRemoteBucketDetailsRequest() (params bucketApi.RemoteBucketDetailsParams, api operations.ConsoleAPI) {
|
||||
registerAdminBucketRemoteHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestGetRemoteBucketWithoutError() {
|
||||
ctx := context.Background()
|
||||
minioGetRemoteBucketMock = func(_ context.Context, _, _ string) (targets *madmin.BucketTarget, err error) {
|
||||
return suite.mockBucketTarget, nil
|
||||
}
|
||||
res, err := getRemoteBucket(ctx, &suite.adminClient, "bucketName")
|
||||
suite.assert.Nil(err)
|
||||
suite.assert.NotNil(res)
|
||||
suite.assert.Equal(suite.mockRemoteBucket, res)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestDeleteRemoteBucketHandlerWithError() {
|
||||
params, api := suite.initDeleteRemoteBucketRequest()
|
||||
response := api.BucketDeleteRemoteBucketHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*bucketApi.DeleteRemoteBucketDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) initDeleteRemoteBucketRequest() (params bucketApi.DeleteRemoteBucketParams, api operations.ConsoleAPI) {
|
||||
registerAdminBucketRemoteHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestAddRemoteBucketHandlerWithError() {
|
||||
params, api := suite.initAddRemoteBucketRequest()
|
||||
response := api.BucketAddRemoteBucketHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*bucketApi.AddRemoteBucketDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) initAddRemoteBucketRequest() (params bucketApi.AddRemoteBucketParams, api operations.ConsoleAPI) {
|
||||
registerAdminBucketRemoteHandlers(&api)
|
||||
url := "^&*&^%^"
|
||||
accessKey := "accessKey"
|
||||
secretKey := "secretKey"
|
||||
params.HTTPRequest = &http.Request{}
|
||||
params.Body = &models.CreateRemoteBucket{
|
||||
TargetURL: &url,
|
||||
AccessKey: &accessKey,
|
||||
SecretKey: &secretKey,
|
||||
}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestAddRemoteBucketWithoutError() {
|
||||
ctx := context.Background()
|
||||
minioAddRemoteBucketMock = func(_ context.Context, _ string, _ *madmin.BucketTarget) (string, error) {
|
||||
return "bucketName", nil
|
||||
}
|
||||
url := "https://localhost"
|
||||
accessKey := "accessKey"
|
||||
secretKey := "secretKey"
|
||||
targetBucket := "targetBucket"
|
||||
syncMode := "async"
|
||||
sourceBucket := "sourceBucket"
|
||||
data := models.CreateRemoteBucket{
|
||||
TargetURL: &url,
|
||||
TargetBucket: &targetBucket,
|
||||
AccessKey: &accessKey,
|
||||
SecretKey: &secretKey,
|
||||
SyncMode: &syncMode,
|
||||
HealthCheckPeriod: 10,
|
||||
SourceBucket: &sourceBucket,
|
||||
}
|
||||
res, err := addRemoteBucket(ctx, &suite.adminClient, data)
|
||||
suite.assert.NotNil(res)
|
||||
suite.assert.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestSetMultiBucketReplicationHandlerWithError() {
|
||||
params, api := suite.initSetMultiBucketReplicationRequest()
|
||||
response := api.BucketSetMultiBucketReplicationHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*bucketApi.SetMultiBucketReplicationOK)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) initSetMultiBucketReplicationRequest() (params bucketApi.SetMultiBucketReplicationParams, api operations.ConsoleAPI) {
|
||||
registerAdminBucketRemoteHandlers(&api)
|
||||
accessKey := "accessKey"
|
||||
secretKey := "secretKey"
|
||||
targetURL := "https://localhost"
|
||||
syncMode := "async"
|
||||
params.HTTPRequest = &http.Request{}
|
||||
params.Body = &models.MultiBucketReplication{
|
||||
BucketsRelation: []*models.MultiBucketsRelation{{}},
|
||||
AccessKey: &accessKey,
|
||||
SecretKey: &secretKey,
|
||||
Region: "region",
|
||||
TargetURL: &targetURL,
|
||||
SyncMode: &syncMode,
|
||||
Bandwidth: 10,
|
||||
HealthCheckPeriod: 10,
|
||||
}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestListExternalBucketsHandlerWithError() {
|
||||
params, api := suite.initListExternalBucketsRequest()
|
||||
response := api.BucketListExternalBucketsHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*bucketApi.ListExternalBucketsDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) initListExternalBucketsRequest() (params bucketApi.ListExternalBucketsParams, api operations.ConsoleAPI) {
|
||||
registerAdminBucketRemoteHandlers(&api)
|
||||
url := "http://localhost:9000"
|
||||
accessKey := "accessKey"
|
||||
secretKey := "secretKey"
|
||||
tls := false
|
||||
params.HTTPRequest = &http.Request{}
|
||||
params.Body = &models.ListExternalBucketsParams{
|
||||
TargetURL: &url,
|
||||
AccessKey: &accessKey,
|
||||
SecretKey: &secretKey,
|
||||
UseTLS: &tls,
|
||||
}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestListExternalBucketsWithError() {
|
||||
ctx := context.Background()
|
||||
minioAccountInfoMock = func(_ context.Context) (madmin.AccountInfo, error) {
|
||||
return madmin.AccountInfo{}, errors.New("error")
|
||||
}
|
||||
res, err := listExternalBuckets(ctx, &suite.adminClient)
|
||||
suite.assert.NotNil(err)
|
||||
suite.assert.Nil(res)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestListExternalBucketsWithoutError() {
|
||||
ctx := context.Background()
|
||||
minioAccountInfoMock = func(_ context.Context) (madmin.AccountInfo, error) {
|
||||
return madmin.AccountInfo{
|
||||
Buckets: []madmin.BucketAccessInfo{},
|
||||
}, nil
|
||||
}
|
||||
res, err := listExternalBuckets(ctx, &suite.adminClient)
|
||||
suite.assert.Nil(err)
|
||||
suite.assert.NotNil(res)
|
||||
suite.assert.Equal(suite.mockListBuckets, res)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestDeleteBucketReplicationRuleHandlerWithError() {
|
||||
params, api := suite.initDeleteBucketReplicationRuleRequest()
|
||||
response := api.BucketDeleteBucketReplicationRuleHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*bucketApi.DeleteBucketReplicationRuleDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) initDeleteBucketReplicationRuleRequest() (params bucketApi.DeleteBucketReplicationRuleParams, api operations.ConsoleAPI) {
|
||||
registerAdminBucketRemoteHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestDeleteAllReplicationRulesHandlerWithError() {
|
||||
params, api := suite.initDeleteAllReplicationRulesRequest()
|
||||
response := api.BucketDeleteAllReplicationRulesHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*bucketApi.DeleteAllReplicationRulesDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) initDeleteAllReplicationRulesRequest() (params bucketApi.DeleteAllReplicationRulesParams, api operations.ConsoleAPI) {
|
||||
registerAdminBucketRemoteHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestDeleteSelectedReplicationRulesHandlerWithError() {
|
||||
params, api := suite.initDeleteSelectedReplicationRulesRequest()
|
||||
response := api.BucketDeleteSelectedReplicationRulesHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*bucketApi.DeleteSelectedReplicationRulesDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) initDeleteSelectedReplicationRulesRequest() (params bucketApi.DeleteSelectedReplicationRulesParams, api operations.ConsoleAPI) {
|
||||
registerAdminBucketRemoteHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
params.BucketName = "bucketName"
|
||||
params.Rules = &models.BucketReplicationRuleList{
|
||||
Rules: []string{"rule1", "rule2"},
|
||||
}
|
||||
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) TestUpdateMultiBucketReplicationHandlerWithError() {
|
||||
params, api := suite.initUpdateMultiBucketReplicationRequest()
|
||||
response := api.BucketUpdateMultiBucketReplicationHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*bucketApi.UpdateMultiBucketReplicationDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *RemoteBucketsTestSuite) initUpdateMultiBucketReplicationRequest() (params bucketApi.UpdateMultiBucketReplicationParams, api operations.ConsoleAPI) {
|
||||
registerAdminBucketRemoteHandlers(&api)
|
||||
r := &http.Request{}
|
||||
ctx := context.WithValue(context.Background(), utils.ContextClientIP, "127.0.0.1")
|
||||
rc := r.WithContext(ctx)
|
||||
params.HTTPRequest = rc
|
||||
params.Body = &models.MultiBucketReplicationEdit{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func TestRemoteBuckets(t *testing.T) {
|
||||
suite.Run(t, new(RemoteBucketsTestSuite))
|
||||
}
|
||||
86
api/admin_replication_status.go
Normal file
86
api/admin_replication_status.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
siteRepApi "github.com/minio/console/api/operations/site_replication"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
func registerSiteReplicationStatusHandler(api *operations.ConsoleAPI) {
|
||||
api.SiteReplicationGetSiteReplicationStatusHandler = siteRepApi.GetSiteReplicationStatusHandlerFunc(func(params siteRepApi.GetSiteReplicationStatusParams, session *models.Principal) middleware.Responder {
|
||||
rInfo, err := getSRStatusResponse(session, params)
|
||||
if err != nil {
|
||||
return siteRepApi.NewGetSiteReplicationStatusDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return siteRepApi.NewGetSiteReplicationStatusOK().WithPayload(rInfo)
|
||||
})
|
||||
}
|
||||
|
||||
func getSRStatusResponse(session *models.Principal, params siteRepApi.GetSiteReplicationStatusParams) (*models.SiteReplicationStatusResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
res, err := getSRStats(ctx, adminClient, params)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getSRStats(ctx context.Context, client MinioAdmin, params siteRepApi.GetSiteReplicationStatusParams) (info *models.SiteReplicationStatusResponse, err error) {
|
||||
srParams := madmin.SRStatusOptions{
|
||||
Buckets: *params.Buckets,
|
||||
Policies: *params.Policies,
|
||||
Users: *params.Users,
|
||||
Groups: *params.Groups,
|
||||
}
|
||||
if params.EntityType != nil && params.EntityValue != nil {
|
||||
srParams.Entity = madmin.GetSREntityType(*params.EntityType)
|
||||
srParams.EntityValue = *params.EntityValue
|
||||
}
|
||||
|
||||
srInfo, err := client.getSiteReplicationStatus(ctx, srParams)
|
||||
|
||||
retInfo := models.SiteReplicationStatusResponse{
|
||||
BucketStats: &srInfo.BucketStats,
|
||||
Enabled: srInfo.Enabled,
|
||||
GroupStats: srInfo.GroupStats,
|
||||
MaxBuckets: int64(srInfo.MaxBuckets),
|
||||
MaxGroups: int64(srInfo.MaxGroups),
|
||||
MaxPolicies: int64(srInfo.MaxPolicies),
|
||||
MaxUsers: int64(srInfo.MaxUsers),
|
||||
PolicyStats: &srInfo.PolicyStats,
|
||||
Sites: &srInfo.Sites,
|
||||
StatsSummary: srInfo.StatsSummary,
|
||||
UserStats: &srInfo.UserStats,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &retInfo, nil
|
||||
}
|
||||
77
api/admin_service.go
Normal file
77
api/admin_service.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
"github.com/minio/console/models"
|
||||
|
||||
svcApi "github.com/minio/console/api/operations/service"
|
||||
)
|
||||
|
||||
func registerServiceHandlers(api *operations.ConsoleAPI) {
|
||||
// Restart Service
|
||||
api.ServiceRestartServiceHandler = svcApi.RestartServiceHandlerFunc(func(params svcApi.RestartServiceParams, session *models.Principal) middleware.Responder {
|
||||
if err := getRestartServiceResponse(session, params); err != nil {
|
||||
return svcApi.NewRestartServiceDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return svcApi.NewRestartServiceNoContent()
|
||||
})
|
||||
}
|
||||
|
||||
// serviceRestart - restarts the MinIO cluster
|
||||
func serviceRestart(ctx context.Context, client MinioAdmin) error {
|
||||
if err := client.serviceRestart(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
// copy behavior from minio/mc mainAdminServiceRestart()
|
||||
//
|
||||
// Max. time taken by the server to shutdown is 5 seconds.
|
||||
// This can happen when there are lot of s3 requests pending when the server
|
||||
// receives a restart command.
|
||||
// Sleep for 6 seconds and then check if the server is online.
|
||||
time.Sleep(6 * time.Second)
|
||||
|
||||
// Fetch the service status of the specified MinIO server
|
||||
_, err := client.serverInfo(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRestartServiceResponse performs serviceRestart()
|
||||
func getRestartServiceResponse(session *models.Principal, params svcApi.RestartServiceParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a MinIO Admin Client interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
if err := serviceRestart(ctx, adminClient); err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
68
api/admin_service_test.go
Normal file
68
api/admin_service_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestServiceRestart(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := AdminClientMock{}
|
||||
ctx := context.Background()
|
||||
function := "serviceRestart()"
|
||||
// Test-1 : serviceRestart() restart services no errors
|
||||
// mock function response from listGroups()
|
||||
minioServiceRestartMock = func(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
MinioServerInfoMock = func(_ context.Context) (madmin.InfoMessage, error) {
|
||||
return madmin.InfoMessage{}, nil
|
||||
}
|
||||
if err := serviceRestart(ctx, adminClient); err != nil {
|
||||
t.Errorf("Failed on %s:, errors occurred: %s", function, err.Error())
|
||||
}
|
||||
|
||||
// Test-2 : serviceRestart() returns errors on client.serviceRestart call
|
||||
// and see that the errors is handled correctly and returned
|
||||
minioServiceRestartMock = func(_ context.Context) error {
|
||||
return errors.New("error")
|
||||
}
|
||||
MinioServerInfoMock = func(_ context.Context) (madmin.InfoMessage, error) {
|
||||
return madmin.InfoMessage{}, nil
|
||||
}
|
||||
if err := serviceRestart(ctx, adminClient); assert.Error(err) {
|
||||
assert.Equal("error", err.Error())
|
||||
}
|
||||
|
||||
// Test-3 : serviceRestart() returns errors on client.serverInfo() call
|
||||
// and see that the errors is handled correctly and returned
|
||||
minioServiceRestartMock = func(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
MinioServerInfoMock = func(_ context.Context) (madmin.InfoMessage, error) {
|
||||
return madmin.InfoMessage{}, errors.New("error on server info")
|
||||
}
|
||||
if err := serviceRestart(ctx, adminClient); assert.Error(err) {
|
||||
assert.Equal("error on server info", err.Error())
|
||||
}
|
||||
}
|
||||
242
api/admin_site_replication.go
Normal file
242
api/admin_site_replication.go
Normal file
@@ -0,0 +1,242 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
siteRepApi "github.com/minio/console/api/operations/site_replication"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
func registerSiteReplicationHandler(api *operations.ConsoleAPI) {
|
||||
api.SiteReplicationGetSiteReplicationInfoHandler = siteRepApi.GetSiteReplicationInfoHandlerFunc(func(params siteRepApi.GetSiteReplicationInfoParams, session *models.Principal) middleware.Responder {
|
||||
rInfo, err := getSRInfoResponse(session, params)
|
||||
if err != nil {
|
||||
return siteRepApi.NewGetSiteReplicationInfoDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return siteRepApi.NewGetSiteReplicationInfoOK().WithPayload(rInfo)
|
||||
})
|
||||
|
||||
api.SiteReplicationSiteReplicationInfoAddHandler = siteRepApi.SiteReplicationInfoAddHandlerFunc(func(params siteRepApi.SiteReplicationInfoAddParams, session *models.Principal) middleware.Responder {
|
||||
eInfo, err := getSRAddResponse(session, params)
|
||||
if err != nil {
|
||||
return siteRepApi.NewSiteReplicationInfoAddDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return siteRepApi.NewSiteReplicationInfoAddOK().WithPayload(eInfo)
|
||||
})
|
||||
|
||||
api.SiteReplicationSiteReplicationRemoveHandler = siteRepApi.SiteReplicationRemoveHandlerFunc(func(params siteRepApi.SiteReplicationRemoveParams, session *models.Principal) middleware.Responder {
|
||||
remRes, err := getSRRemoveResponse(session, params)
|
||||
if err != nil {
|
||||
return siteRepApi.NewSiteReplicationRemoveDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return siteRepApi.NewSiteReplicationRemoveNoContent().WithPayload(remRes)
|
||||
})
|
||||
|
||||
api.SiteReplicationSiteReplicationEditHandler = siteRepApi.SiteReplicationEditHandlerFunc(func(params siteRepApi.SiteReplicationEditParams, session *models.Principal) middleware.Responder {
|
||||
eInfo, err := getSREditResponse(session, params)
|
||||
if err != nil {
|
||||
return siteRepApi.NewSiteReplicationRemoveDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return siteRepApi.NewSiteReplicationEditOK().WithPayload(eInfo)
|
||||
})
|
||||
}
|
||||
|
||||
func getSRInfoResponse(session *models.Principal, params siteRepApi.GetSiteReplicationInfoParams) (*models.SiteReplicationInfoResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
res, err := getSRConfig(ctx, adminClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getSRAddResponse(session *models.Principal, params siteRepApi.SiteReplicationInfoAddParams) (*models.SiteReplicationAddResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
res, err := addSiteReplication(ctx, adminClient, ¶ms)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getSREditResponse(session *models.Principal, params siteRepApi.SiteReplicationEditParams) (*models.PeerSiteEditResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
eRes, err := editSiteReplication(ctx, adminClient, ¶ms)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return eRes, nil
|
||||
}
|
||||
|
||||
func getSRRemoveResponse(session *models.Principal, params siteRepApi.SiteReplicationRemoveParams) (*models.PeerSiteRemoveResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
rRes, err := removeSiteReplication(ctx, adminClient, ¶ms)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return rRes, nil
|
||||
}
|
||||
|
||||
func getSRConfig(ctx context.Context, client MinioAdmin) (info *models.SiteReplicationInfoResponse, err error) {
|
||||
srInfo, err := client.getSiteReplicationInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sites []*models.PeerInfo
|
||||
|
||||
if len(srInfo.Sites) > 0 {
|
||||
for _, s := range srInfo.Sites {
|
||||
pInfo := &models.PeerInfo{
|
||||
DeploymentID: s.DeploymentID,
|
||||
Endpoint: s.Endpoint,
|
||||
Name: s.Name,
|
||||
}
|
||||
sites = append(sites, pInfo)
|
||||
}
|
||||
}
|
||||
res := &models.SiteReplicationInfoResponse{
|
||||
Enabled: srInfo.Enabled,
|
||||
Name: srInfo.Name,
|
||||
ServiceAccountAccessKey: srInfo.ServiceAccountAccessKey,
|
||||
Sites: sites,
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func addSiteReplication(ctx context.Context, client MinioAdmin, params *siteRepApi.SiteReplicationInfoAddParams) (info *models.SiteReplicationAddResponse, err error) {
|
||||
var rSites []madmin.PeerSite
|
||||
|
||||
if len(params.Body) > 0 {
|
||||
for _, aSite := range params.Body {
|
||||
pInfo := &madmin.PeerSite{
|
||||
AccessKey: aSite.AccessKey,
|
||||
Name: aSite.Name,
|
||||
SecretKey: aSite.SecretKey,
|
||||
Endpoint: aSite.Endpoint,
|
||||
}
|
||||
rSites = append(rSites, *pInfo)
|
||||
}
|
||||
}
|
||||
qs := runtime.Values(params.HTTPRequest.URL.Query())
|
||||
_, qhkReplicateILMExpiry, _ := qs.GetOK("replicate-ilm-expiry")
|
||||
var opts madmin.SRAddOptions
|
||||
if qhkReplicateILMExpiry {
|
||||
opts.ReplicateILMExpiry = true
|
||||
}
|
||||
cc, err := client.addSiteReplicationInfo(ctx, rSites, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &models.SiteReplicationAddResponse{
|
||||
ErrorDetail: cc.ErrDetail,
|
||||
InitialSyncErrorMessage: cc.InitialSyncErrorMessage,
|
||||
Status: cc.Status,
|
||||
Success: cc.Success,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func editSiteReplication(ctx context.Context, client MinioAdmin, params *siteRepApi.SiteReplicationEditParams) (info *models.PeerSiteEditResponse, err error) {
|
||||
peerSiteInfo := &madmin.PeerInfo{
|
||||
Endpoint: params.Body.Endpoint, // only endpoint can be edited.
|
||||
Name: params.Body.Name, // does not get updated.
|
||||
DeploymentID: params.Body.DeploymentID, // readonly
|
||||
}
|
||||
qs := runtime.Values(params.HTTPRequest.URL.Query())
|
||||
_, qhkDisableILMExpiryReplication, _ := qs.GetOK("disable-ilm-expiry-replication")
|
||||
_, qhkEnableILMExpiryReplication, _ := qs.GetOK("enable-ilm-expiry-replication")
|
||||
var opts madmin.SREditOptions
|
||||
if qhkDisableILMExpiryReplication {
|
||||
opts.DisableILMExpiryReplication = true
|
||||
}
|
||||
if qhkEnableILMExpiryReplication {
|
||||
opts.EnableILMExpiryReplication = true
|
||||
}
|
||||
eRes, err := client.editSiteReplicationInfo(ctx, *peerSiteInfo, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
editRes := &models.PeerSiteEditResponse{
|
||||
ErrorDetail: eRes.ErrDetail,
|
||||
Status: eRes.Status,
|
||||
Success: eRes.Success,
|
||||
}
|
||||
return editRes, nil
|
||||
}
|
||||
|
||||
func removeSiteReplication(ctx context.Context, client MinioAdmin, params *siteRepApi.SiteReplicationRemoveParams) (info *models.PeerSiteRemoveResponse, err error) {
|
||||
delAll := params.Body.All
|
||||
siteNames := params.Body.Sites
|
||||
|
||||
var req *madmin.SRRemoveReq
|
||||
if delAll {
|
||||
req = &madmin.SRRemoveReq{
|
||||
RemoveAll: delAll,
|
||||
}
|
||||
} else {
|
||||
req = &madmin.SRRemoveReq{
|
||||
SiteNames: siteNames,
|
||||
RemoveAll: delAll,
|
||||
}
|
||||
}
|
||||
|
||||
rRes, err := client.deleteSiteReplicationInfo(ctx, *req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
removeRes := &models.PeerSiteRemoveResponse{
|
||||
ErrorDetail: rRes.ErrDetail,
|
||||
Status: rRes.Status,
|
||||
}
|
||||
return removeRes, nil
|
||||
}
|
||||
255
api/admin_site_replication_test.go
Normal file
255
api/admin_site_replication_test.go
Normal file
@@ -0,0 +1,255 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// These tests are for AdminAPI Tag based on swagger-console.yml
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetSiteReplicationInfo(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// mock minIO client
|
||||
adminClient := AdminClientMock{}
|
||||
|
||||
function := "getSiteReplicationInfo()"
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
retValueMock := madmin.SiteReplicationInfo{
|
||||
Enabled: true,
|
||||
Name: "site1",
|
||||
Sites: []madmin.PeerInfo{
|
||||
{
|
||||
Endpoint: "http://localhost:9000",
|
||||
Name: "site1",
|
||||
DeploymentID: "12345",
|
||||
},
|
||||
{
|
||||
Endpoint: "http://localhost:9001",
|
||||
Name: "site2",
|
||||
DeploymentID: "123456",
|
||||
},
|
||||
},
|
||||
ServiceAccountAccessKey: "test-key",
|
||||
}
|
||||
|
||||
expValueMock := &madmin.SiteReplicationInfo{
|
||||
Enabled: true,
|
||||
Name: "site1",
|
||||
Sites: []madmin.PeerInfo{
|
||||
{
|
||||
Endpoint: "http://localhost:9000",
|
||||
Name: "site1",
|
||||
DeploymentID: "12345",
|
||||
},
|
||||
{
|
||||
Endpoint: "http://localhost:9001",
|
||||
Name: "site2",
|
||||
DeploymentID: "123456",
|
||||
},
|
||||
},
|
||||
ServiceAccountAccessKey: "test-key",
|
||||
}
|
||||
|
||||
getSiteReplicationInfo = func(_ context.Context) (info *madmin.SiteReplicationInfo, err error) {
|
||||
return &retValueMock, nil
|
||||
}
|
||||
|
||||
srInfo, err := adminClient.getSiteReplicationInfo(ctx)
|
||||
assert.Nil(err)
|
||||
assert.Equal(expValueMock, srInfo, fmt.Sprintf("Failed on %s: length of lists is not the same", function))
|
||||
}
|
||||
|
||||
func TestAddSiteReplicationInfo(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// mock minIO client
|
||||
adminClient := AdminClientMock{}
|
||||
|
||||
function := "addSiteReplicationInfo()"
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
retValueMock := &madmin.ReplicateAddStatus{
|
||||
Success: true,
|
||||
Status: "success",
|
||||
ErrDetail: "",
|
||||
InitialSyncErrorMessage: "",
|
||||
}
|
||||
|
||||
expValueMock := &madmin.ReplicateAddStatus{
|
||||
Success: true,
|
||||
Status: "success",
|
||||
ErrDetail: "",
|
||||
InitialSyncErrorMessage: "",
|
||||
}
|
||||
|
||||
addSiteReplicationInfo = func(_ context.Context, _ []madmin.PeerSite) (res *madmin.ReplicateAddStatus, err error) {
|
||||
return retValueMock, nil
|
||||
}
|
||||
|
||||
sites := []madmin.PeerSite{
|
||||
{
|
||||
Name: "site1",
|
||||
Endpoint: "http://localhost:9000",
|
||||
AccessKey: "test",
|
||||
SecretKey: "test",
|
||||
},
|
||||
{
|
||||
Name: "site2",
|
||||
Endpoint: "http://localhost:9001",
|
||||
AccessKey: "test",
|
||||
SecretKey: "test",
|
||||
},
|
||||
}
|
||||
|
||||
srInfo, err := adminClient.addSiteReplicationInfo(ctx, sites, madmin.SRAddOptions{})
|
||||
assert.Nil(err)
|
||||
assert.Equal(expValueMock, srInfo, fmt.Sprintf("Failed on %s: length of lists is not the same", function))
|
||||
}
|
||||
|
||||
func TestEditSiteReplicationInfo(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// mock minIO client
|
||||
adminClient := AdminClientMock{}
|
||||
|
||||
function := "editSiteReplicationInfo()"
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
retValueMock := &madmin.ReplicateEditStatus{
|
||||
Success: true,
|
||||
Status: "success",
|
||||
ErrDetail: "",
|
||||
}
|
||||
|
||||
expValueMock := &madmin.ReplicateEditStatus{
|
||||
Success: true,
|
||||
Status: "success",
|
||||
ErrDetail: "",
|
||||
}
|
||||
|
||||
editSiteReplicationInfo = func(_ context.Context, _ madmin.PeerInfo) (res *madmin.ReplicateEditStatus, err error) {
|
||||
return retValueMock, nil
|
||||
}
|
||||
|
||||
site := madmin.PeerInfo{
|
||||
Name: "",
|
||||
Endpoint: "",
|
||||
DeploymentID: "12345",
|
||||
}
|
||||
|
||||
srInfo, err := adminClient.editSiteReplicationInfo(ctx, site, madmin.SREditOptions{})
|
||||
assert.Nil(err)
|
||||
assert.Equal(expValueMock, srInfo, fmt.Sprintf("Failed on %s: length of lists is not the same", function))
|
||||
}
|
||||
|
||||
func TestDeleteSiteReplicationInfo(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// mock minIO client
|
||||
adminClient := AdminClientMock{}
|
||||
|
||||
function := "deleteSiteReplicationInfo()"
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
retValueMock := &madmin.ReplicateRemoveStatus{
|
||||
Status: "success",
|
||||
ErrDetail: "",
|
||||
}
|
||||
|
||||
expValueMock := &madmin.ReplicateRemoveStatus{
|
||||
Status: "success",
|
||||
ErrDetail: "",
|
||||
}
|
||||
|
||||
deleteSiteReplicationInfoMock = func(_ context.Context, _ madmin.SRRemoveReq) (res *madmin.ReplicateRemoveStatus, err error) {
|
||||
return retValueMock, nil
|
||||
}
|
||||
|
||||
remReq := madmin.SRRemoveReq{
|
||||
SiteNames: []string{
|
||||
"test1",
|
||||
},
|
||||
RemoveAll: false,
|
||||
}
|
||||
|
||||
srInfo, err := adminClient.deleteSiteReplicationInfo(ctx, remReq)
|
||||
assert.Nil(err)
|
||||
assert.Equal(expValueMock, srInfo, fmt.Sprintf("Failed on %s: length of lists is not the same", function))
|
||||
}
|
||||
|
||||
func TestSiteReplicationStatus(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// mock minIO client
|
||||
adminClient := AdminClientMock{}
|
||||
|
||||
function := "getSiteReplicationStatus()"
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
retValueMock := madmin.SRStatusInfo{
|
||||
Enabled: true,
|
||||
MaxBuckets: 0,
|
||||
MaxUsers: 0,
|
||||
MaxGroups: 0,
|
||||
MaxPolicies: 0,
|
||||
Sites: nil,
|
||||
StatsSummary: nil,
|
||||
BucketStats: nil,
|
||||
PolicyStats: nil,
|
||||
UserStats: nil,
|
||||
GroupStats: nil,
|
||||
}
|
||||
|
||||
expValueMock := &madmin.SRStatusInfo{
|
||||
Enabled: true,
|
||||
MaxBuckets: 0,
|
||||
MaxUsers: 0,
|
||||
MaxGroups: 0,
|
||||
MaxPolicies: 0,
|
||||
Sites: nil,
|
||||
StatsSummary: nil,
|
||||
BucketStats: nil,
|
||||
PolicyStats: nil,
|
||||
UserStats: nil,
|
||||
GroupStats: nil,
|
||||
}
|
||||
|
||||
getSiteReplicationStatus = func(_ context.Context, _ madmin.SRStatusOptions) (info *madmin.SRStatusInfo, err error) {
|
||||
return &retValueMock, nil
|
||||
}
|
||||
|
||||
reqValues := madmin.SRStatusOptions{
|
||||
Buckets: true,
|
||||
Policies: true,
|
||||
Users: true,
|
||||
Groups: true,
|
||||
}
|
||||
srInfo, err := adminClient.getSiteReplicationStatus(ctx, reqValues)
|
||||
if err != nil {
|
||||
assert.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(expValueMock, srInfo, fmt.Sprintf("Failed on %s: expected result is not same", function))
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package restapi
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -25,8 +25,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/websocket"
|
||||
)
|
||||
|
||||
// getSpeedtesthOptionsFromReq gets duration, size & concurrent requests from a websocket
|
||||
@@ -60,15 +60,10 @@ func getSpeedtestOptionsFromReq(req *http.Request) (*madmin.SpeedtestOpts, error
|
||||
}
|
||||
|
||||
size, err := humanize.ParseBytes(paramSize)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse object size")
|
||||
}
|
||||
|
||||
if size < 0 {
|
||||
return nil, fmt.Errorf("size is expected to be atleast 0 bytes")
|
||||
}
|
||||
|
||||
optionsSet.Size = int(size)
|
||||
|
||||
paramConcurrent := queryPairs.Get("concurrent")
|
||||
@@ -78,7 +73,6 @@ func getSpeedtestOptionsFromReq(req *http.Request) (*madmin.SpeedtestOpts, error
|
||||
}
|
||||
|
||||
concurrent, err := strconv.Atoi(paramConcurrent)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid concurrent value: %s", paramConcurrent)
|
||||
}
|
||||
@@ -100,7 +94,6 @@ func getSpeedtestOptionsFromReq(req *http.Request) (*madmin.SpeedtestOpts, error
|
||||
|
||||
func startSpeedtest(ctx context.Context, conn WSConn, client MinioAdmin, speedtestOpts *madmin.SpeedtestOpts) error {
|
||||
speedtestRes, err := client.speedtest(ctx, *speedtestOpts)
|
||||
|
||||
if err != nil {
|
||||
LogError("error initializing speedtest: %v", err)
|
||||
return err
|
||||
435
api/admin_subnet.go
Normal file
435
api/admin_subnet.go
Normal file
@@ -0,0 +1,435 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
|
||||
xhttp "github.com/minio/console/pkg/http"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
subnetApi "github.com/minio/console/api/operations/subnet"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/pkg/subnet"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
func registerSubnetHandlers(api *operations.ConsoleAPI) {
|
||||
// Get subnet login handler
|
||||
api.SubnetSubnetLoginHandler = subnetApi.SubnetLoginHandlerFunc(func(params subnetApi.SubnetLoginParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := GetSubnetLoginResponse(session, params)
|
||||
if err != nil {
|
||||
return subnetApi.NewSubnetLoginDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return subnetApi.NewSubnetLoginOK().WithPayload(resp)
|
||||
})
|
||||
// Get subnet login with MFA handler
|
||||
api.SubnetSubnetLoginMFAHandler = subnetApi.SubnetLoginMFAHandlerFunc(func(params subnetApi.SubnetLoginMFAParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := GetSubnetLoginWithMFAResponse(session, params)
|
||||
if err != nil {
|
||||
return subnetApi.NewSubnetLoginMFADefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return subnetApi.NewSubnetLoginMFAOK().WithPayload(resp)
|
||||
})
|
||||
// Get subnet register
|
||||
api.SubnetSubnetRegisterHandler = subnetApi.SubnetRegisterHandlerFunc(func(params subnetApi.SubnetRegisterParams, session *models.Principal) middleware.Responder {
|
||||
err := GetSubnetRegisterResponse(session, params)
|
||||
if err != nil {
|
||||
return subnetApi.NewSubnetRegisterDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return subnetApi.NewSubnetRegisterOK()
|
||||
})
|
||||
// Get subnet info
|
||||
api.SubnetSubnetInfoHandler = subnetApi.SubnetInfoHandlerFunc(func(params subnetApi.SubnetInfoParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := GetSubnetInfoResponse(session, params)
|
||||
if err != nil {
|
||||
return subnetApi.NewSubnetInfoDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return subnetApi.NewSubnetInfoOK().WithPayload(resp)
|
||||
})
|
||||
// Get subnet registration token
|
||||
api.SubnetSubnetRegTokenHandler = subnetApi.SubnetRegTokenHandlerFunc(func(params subnetApi.SubnetRegTokenParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := GetSubnetRegTokenResponse(session, params)
|
||||
if err != nil {
|
||||
return subnetApi.NewSubnetRegTokenDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return subnetApi.NewSubnetRegTokenOK().WithPayload(resp)
|
||||
})
|
||||
|
||||
api.SubnetSubnetAPIKeyHandler = subnetApi.SubnetAPIKeyHandlerFunc(func(params subnetApi.SubnetAPIKeyParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := GetSubnetAPIKeyResponse(session, params)
|
||||
if err != nil {
|
||||
return subnetApi.NewSubnetAPIKeyDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return subnetApi.NewSubnetAPIKeyOK().WithPayload(resp)
|
||||
})
|
||||
}
|
||||
|
||||
const EnvSubnetLicense = "CONSOLE_SUBNET_LICENSE"
|
||||
|
||||
func SubnetRegisterWithAPIKey(ctx context.Context, minioClient MinioAdmin, apiKey string) (bool, error) {
|
||||
serverInfo, err := minioClient.serverInfo(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
clientIP := utils.ClientIPFromContext(ctx)
|
||||
registerResult, err := subnet.Register(GetConsoleHTTPClient(clientIP), serverInfo, apiKey, "", "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Keep existing subnet proxy if exists
|
||||
subnetKey, err := GetSubnetKeyFromMinIOConfig(ctx, minioClient)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
configStr := fmt.Sprintf("subnet license=%s api_key=%s proxy=%s", registerResult.License, registerResult.APIKey, subnetKey.Proxy)
|
||||
_, err = minioClient.setConfigKV(ctx, configStr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// cluster registered correctly
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func SubnetLogin(client xhttp.ClientI, username, password string) (string, string, error) {
|
||||
tokens, err := subnet.Login(client, username, password)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if tokens.MfaToken != "" {
|
||||
// user needs to complete login flow using mfa
|
||||
return "", tokens.MfaToken, nil
|
||||
}
|
||||
if tokens.AccessToken != "" {
|
||||
// register token to minio
|
||||
return tokens.AccessToken, "", nil
|
||||
}
|
||||
return "", "", errors.New("something went wrong")
|
||||
}
|
||||
|
||||
func GetSubnetLoginResponse(session *models.Principal, params subnetApi.SubnetLoginParams) (*models.SubnetLoginResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return subnetLoginResponse(ctx, AdminClient{Client: mAdmin}, params)
|
||||
}
|
||||
|
||||
func subnetLoginResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetLoginParams) (*models.SubnetLoginResponse, *CodedAPIError) {
|
||||
subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
apiKey := params.Body.APIKey
|
||||
if apiKey != "" {
|
||||
registered, err := SubnetRegisterWithAPIKey(ctx, minioClient, apiKey)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.SubnetLoginResponse{
|
||||
Registered: registered,
|
||||
Organizations: []*models.SubnetOrganization{},
|
||||
}, nil
|
||||
}
|
||||
username := params.Body.Username
|
||||
password := params.Body.Password
|
||||
if username != "" && password != "" {
|
||||
token, mfa, err := SubnetLogin(subnetHTTPClient, username, password)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.SubnetLoginResponse{
|
||||
MfaToken: mfa,
|
||||
AccessToken: token,
|
||||
Organizations: []*models.SubnetOrganization{},
|
||||
}, nil
|
||||
}
|
||||
return nil, ErrorWithContext(ctx, ErrDefault)
|
||||
}
|
||||
|
||||
type SubnetRegistration struct {
|
||||
AccessToken string
|
||||
MFAToken string
|
||||
Organizations []models.SubnetOrganization
|
||||
}
|
||||
|
||||
func SubnetLoginWithMFA(client xhttp.ClientI, username, mfaToken, otp string) (*models.SubnetLoginResponse, error) {
|
||||
tokens, err := subnet.LoginWithMFA(client, username, mfaToken, otp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tokens.AccessToken != "" {
|
||||
organizations, errOrg := subnet.GetOrganizations(client, tokens.AccessToken)
|
||||
if errOrg != nil {
|
||||
return nil, errOrg
|
||||
}
|
||||
return &models.SubnetLoginResponse{
|
||||
AccessToken: tokens.AccessToken,
|
||||
Organizations: organizations,
|
||||
}, nil
|
||||
}
|
||||
return nil, errors.New("something went wrong")
|
||||
}
|
||||
|
||||
// GetSubnetHTTPClient will return a client with proxy if configured, otherwise will return the default console http client
|
||||
func GetSubnetHTTPClient(ctx context.Context, minioClient MinioAdmin) (*xhttp.Client, error) {
|
||||
clientIP := utils.ClientIPFromContext(ctx)
|
||||
subnetKey, err := GetSubnetKeyFromMinIOConfig(ctx, minioClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := getSubnetProxy()
|
||||
if subnetKey.Proxy != "" {
|
||||
proxy = subnetKey.Proxy
|
||||
}
|
||||
|
||||
tr := GlobalTransport.Clone()
|
||||
if proxy != "" {
|
||||
u, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tr.Proxy = http.ProxyURL(u)
|
||||
}
|
||||
|
||||
return &xhttp.Client{
|
||||
Client: &http.Client{
|
||||
Transport: &ConsoleTransport{
|
||||
Transport: tr,
|
||||
ClientIP: clientIP,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetSubnetLoginWithMFAResponse(session *models.Principal, params subnetApi.SubnetLoginMFAParams) (*models.SubnetLoginResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
minioClient := AdminClient{Client: mAdmin}
|
||||
return subnetLoginWithMFAResponse(ctx, minioClient, params)
|
||||
}
|
||||
|
||||
func subnetLoginWithMFAResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetLoginMFAParams) (*models.SubnetLoginResponse, *CodedAPIError) {
|
||||
subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
resp, err := SubnetLoginWithMFA(subnetHTTPClient, *params.Body.Username, *params.Body.MfaToken, *params.Body.Otp)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func GetSubnetKeyFromMinIOConfig(ctx context.Context, minioClient MinioAdmin) (*subnet.LicenseTokenConfig, error) {
|
||||
buf, err := minioClient.getConfigKV(ctx, madmin.SubnetSubSys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subSysConfigs, err := madmin.ParseServerConfigOutput(string(buf))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, scfg := range subSysConfigs {
|
||||
if scfg.Target == "" {
|
||||
res := subnet.LicenseTokenConfig{}
|
||||
res.APIKey, _ = scfg.Lookup("api_key")
|
||||
res.License, _ = scfg.Lookup("license")
|
||||
res.Proxy, _ = scfg.Lookup("proxy")
|
||||
return &res, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("unable to find subnet configuration")
|
||||
}
|
||||
|
||||
func GetSubnetRegister(ctx context.Context, minioClient MinioAdmin, httpClient xhttp.ClientI, params subnetApi.SubnetRegisterParams) error {
|
||||
serverInfo, err := minioClient.serverInfo(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
registerResult, err := subnet.Register(httpClient, serverInfo, "", *params.Body.Token, *params.Body.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Keep existing subnet proxy if exists
|
||||
subnetKey, err := GetSubnetKeyFromMinIOConfig(ctx, minioClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configStr := fmt.Sprintf("subnet license=%s api_key=%s proxy=%s", registerResult.License, registerResult.APIKey, subnetKey.Proxy)
|
||||
_, err = minioClient.setConfigKV(ctx, configStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetSubnetRegisterResponse(session *models.Principal, params subnetApi.SubnetRegisterParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
return subnetRegisterResponse(ctx, adminClient, params)
|
||||
}
|
||||
|
||||
func subnetRegisterResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetRegisterParams) *CodedAPIError {
|
||||
subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
err = GetSubnetRegister(ctx, minioClient, subnetHTTPClient, params)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var ErrSubnetLicenseNotFound = errors.New("license not found")
|
||||
|
||||
func GetSubnetInfoResponse(session *models.Principal, params subnetApi.SubnetInfoParams) (*models.License, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
clientIP := utils.ClientIPFromContext(ctx)
|
||||
client := &xhttp.Client{
|
||||
Client: GetConsoleHTTPClient(clientIP),
|
||||
}
|
||||
// license gets seeded to us by MinIO
|
||||
seededLicense := os.Getenv(EnvSubnetLicense)
|
||||
// if it's missing, we will gracefully fallback to attempt to fetch it from MinIO
|
||||
if seededLicense == "" {
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
configBytes, err := adminClient.getConfigKV(params.HTTPRequest.Context(), "subnet")
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
subSysConfigs, err := madmin.ParseServerConfigOutput(string(configBytes))
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// search for licese
|
||||
for _, v := range subSysConfigs {
|
||||
for _, sv := range v.KV {
|
||||
if sv.Key == "license" {
|
||||
seededLicense = sv.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// still empty means not found
|
||||
if seededLicense == "" {
|
||||
return nil, ErrorWithContext(ctx, ErrSubnetLicenseNotFound)
|
||||
}
|
||||
|
||||
licenseInfo, err := getLicenseInfo(*client.Client, seededLicense)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
license := &models.License{
|
||||
Email: licenseInfo.Email,
|
||||
AccountID: licenseInfo.AccountID,
|
||||
StorageCapacity: licenseInfo.StorageCapacity,
|
||||
Plan: licenseInfo.Plan,
|
||||
ExpiresAt: licenseInfo.ExpiresAt.String(),
|
||||
Organization: licenseInfo.Organization,
|
||||
}
|
||||
return license, nil
|
||||
}
|
||||
|
||||
func GetSubnetRegToken(ctx context.Context, minioClient MinioAdmin) (string, error) {
|
||||
serverInfo, err := minioClient.serverInfo(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
regInfo := subnet.GetClusterRegInfo(serverInfo)
|
||||
regToken, err := subnet.GenerateRegToken(regInfo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return regToken, nil
|
||||
}
|
||||
|
||||
func GetSubnetRegTokenResponse(session *models.Principal, params subnetApi.SubnetRegTokenParams) (*models.SubnetRegTokenResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
return subnetRegTokenResponse(ctx, adminClient)
|
||||
}
|
||||
|
||||
func subnetRegTokenResponse(ctx context.Context, minioClient MinioAdmin) (*models.SubnetRegTokenResponse, *CodedAPIError) {
|
||||
token, err := GetSubnetRegToken(ctx, minioClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.SubnetRegTokenResponse{
|
||||
RegToken: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetSubnetAPIKeyResponse(session *models.Principal, params subnetApi.SubnetAPIKeyParams) (*models.APIKey, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
return subnetAPIKeyResponse(ctx, adminClient, params)
|
||||
}
|
||||
|
||||
func subnetAPIKeyResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetAPIKeyParams) (*models.APIKey, *CodedAPIError) {
|
||||
subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
token := params.HTTPRequest.URL.Query().Get("token")
|
||||
apiKey, err := subnet.GetAPIKey(subnetHTTPClient, token)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.APIKey{APIKey: apiKey}, nil
|
||||
}
|
||||
233
api/admin_subnet_test.go
Normal file
233
api/admin_subnet_test.go
Normal file
@@ -0,0 +1,233 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/console/api/operations"
|
||||
subnetApi "github.com/minio/console/api/operations/subnet"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type AdminSubnetTestSuite struct {
|
||||
suite.Suite
|
||||
assert *assert.Assertions
|
||||
currentServer string
|
||||
isServerSet bool
|
||||
server *httptest.Server
|
||||
adminClient AdminClientMock
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) SetupSuite() {
|
||||
suite.assert = assert.New(suite.T())
|
||||
suite.adminClient = AdminClientMock{}
|
||||
minioGetConfigKVMock = func(_ string) ([]byte, error) {
|
||||
return []byte("subnet license=mock api_key=mock proxy=http://mock.com"), nil
|
||||
}
|
||||
MinioServerInfoMock = func(_ context.Context) (madmin.InfoMessage, error) {
|
||||
return madmin.InfoMessage{Servers: []madmin.ServerProperties{{}}}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) SetupTest() {
|
||||
suite.server = httptest.NewServer(http.HandlerFunc(suite.serverHandler))
|
||||
suite.currentServer, suite.isServerSet = os.LookupEnv(ConsoleMinIOServer)
|
||||
os.Setenv(ConsoleMinIOServer, suite.server.URL)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) serverHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(400)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TearDownSuite() {
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TearDownTest() {
|
||||
if suite.isServerSet {
|
||||
os.Setenv(ConsoleMinIOServer, suite.currentServer)
|
||||
} else {
|
||||
os.Unsetenv(ConsoleMinIOServer)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TestRegisterSubnetHandlers() {
|
||||
api := &operations.ConsoleAPI{}
|
||||
suite.assertHandlersAreNil(api)
|
||||
registerSubnetHandlers(api)
|
||||
suite.assertHandlersAreNotNil(api)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) assertHandlersAreNil(api *operations.ConsoleAPI) {
|
||||
suite.assert.Nil(api.SubnetSubnetLoginHandler)
|
||||
suite.assert.Nil(api.SubnetSubnetLoginMFAHandler)
|
||||
suite.assert.Nil(api.SubnetSubnetRegisterHandler)
|
||||
suite.assert.Nil(api.SubnetSubnetInfoHandler)
|
||||
suite.assert.Nil(api.SubnetSubnetRegTokenHandler)
|
||||
suite.assert.Nil(api.SubnetSubnetAPIKeyHandler)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) assertHandlersAreNotNil(api *operations.ConsoleAPI) {
|
||||
suite.assert.NotNil(api.SubnetSubnetLoginHandler)
|
||||
suite.assert.NotNil(api.SubnetSubnetLoginMFAHandler)
|
||||
suite.assert.NotNil(api.SubnetSubnetRegisterHandler)
|
||||
suite.assert.NotNil(api.SubnetSubnetInfoHandler)
|
||||
suite.assert.NotNil(api.SubnetSubnetRegTokenHandler)
|
||||
suite.assert.NotNil(api.SubnetSubnetAPIKeyHandler)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TestSubnetLoginWithSubnetClientError() {
|
||||
params, api := suite.initSubnetLoginRequest("", "", "")
|
||||
response := api.SubnetSubnetLoginHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*subnetApi.SubnetLoginDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TestSubnetLoginResponseWithApiKeyError() {
|
||||
params, _ := suite.initSubnetLoginRequest("mock", "", "")
|
||||
res, err := subnetLoginResponse(context.TODO(), suite.adminClient, params)
|
||||
suite.assert.NotNil(err)
|
||||
suite.assert.Nil(res)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TestSubnetLoginResponseWithCredentialsError() {
|
||||
params, _ := suite.initSubnetLoginRequest("", "mock", "mock")
|
||||
res, err := subnetLoginResponse(context.TODO(), suite.adminClient, params)
|
||||
suite.assert.NotNil(err)
|
||||
suite.assert.Nil(res)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) initSubnetLoginRequest(apiKey, username, password string) (params subnetApi.SubnetLoginParams, api operations.ConsoleAPI) {
|
||||
registerSubnetHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
params.Body = &models.SubnetLoginRequest{}
|
||||
params.Body.APIKey = apiKey
|
||||
params.Body.Username = username
|
||||
params.Body.Password = password
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TestSubnetLoginMFAWithSubnetClientError() {
|
||||
params, api := suite.initSubnetLoginMFARequest("", "", "")
|
||||
response := api.SubnetSubnetLoginMFAHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*subnetApi.SubnetLoginMFADefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TestSubnetLoginWithMFAResponseError() {
|
||||
params, _ := suite.initSubnetLoginMFARequest("mock", "mock", "mock")
|
||||
res, err := subnetLoginWithMFAResponse(context.TODO(), suite.adminClient, params)
|
||||
suite.assert.NotNil(err)
|
||||
suite.assert.Nil(res)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) initSubnetLoginMFARequest(username, mfaToken, otp string) (params subnetApi.SubnetLoginMFAParams, api operations.ConsoleAPI) {
|
||||
registerSubnetHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
params.Body = &models.SubnetLoginMFARequest{}
|
||||
params.Body.Username = &username
|
||||
params.Body.MfaToken = &mfaToken
|
||||
params.Body.Otp = &otp
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TestSubnetRegisterClientError() {
|
||||
params, api := suite.initSubnetRegisterRequest("", "")
|
||||
response := api.SubnetSubnetRegisterHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*subnetApi.SubnetRegisterDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TestSubnetRegisterResponseError() {
|
||||
params, _ := suite.initSubnetRegisterRequest("mock", "mock")
|
||||
err := subnetRegisterResponse(context.TODO(), suite.adminClient, params)
|
||||
suite.assert.NotNil(err)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) initSubnetRegisterRequest(token, accountID string) (params subnetApi.SubnetRegisterParams, api operations.ConsoleAPI) {
|
||||
registerSubnetHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
params.Body = &models.SubnetRegisterRequest{}
|
||||
params.Body.Token = &token
|
||||
params.Body.AccountID = &accountID
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TestSubnetInfoError() {
|
||||
params, api := suite.initSubnetInfoRequest()
|
||||
response := api.SubnetSubnetInfoHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*subnetApi.SubnetInfoDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) initSubnetInfoRequest() (params subnetApi.SubnetInfoParams, api operations.ConsoleAPI) {
|
||||
registerSubnetHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TestSubnetRegTokenError() {
|
||||
params, api := suite.initSubnetRegTokenRequest()
|
||||
response := api.SubnetSubnetRegTokenHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*subnetApi.SubnetRegTokenDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TestSubnetRegTokenResponse() {
|
||||
res, err := subnetRegTokenResponse(context.TODO(), suite.adminClient)
|
||||
suite.assert.Nil(err)
|
||||
suite.assert.NotEqual("", res)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) initSubnetRegTokenRequest() (params subnetApi.SubnetRegTokenParams, api operations.ConsoleAPI) {
|
||||
registerSubnetHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TestSubnetAPIKeyWithClientError() {
|
||||
params, api := suite.initSubnetAPIKeyRequest()
|
||||
response := api.SubnetSubnetAPIKeyHandler.Handle(params, &models.Principal{})
|
||||
_, ok := response.(*subnetApi.SubnetAPIKeyDefault)
|
||||
suite.assert.True(ok)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) TestSubnetAPIKeyResponseError() {
|
||||
params, _ := suite.initSubnetAPIKeyRequest()
|
||||
res, err := subnetAPIKeyResponse(context.TODO(), suite.adminClient, params)
|
||||
suite.assert.NotNil(err)
|
||||
suite.assert.Nil(res)
|
||||
}
|
||||
|
||||
func (suite *AdminSubnetTestSuite) initSubnetAPIKeyRequest() (params subnetApi.SubnetAPIKeyParams, api operations.ConsoleAPI) {
|
||||
registerSubnetHandlers(&api)
|
||||
params.HTTPRequest = &http.Request{}
|
||||
params.HTTPRequest.URL = &url.URL{}
|
||||
return params, api
|
||||
}
|
||||
|
||||
func TestAdminSubnet(t *testing.T) {
|
||||
suite.Run(t, new(AdminSubnetTestSuite))
|
||||
}
|
||||
488
api/admin_tiers.go
Normal file
488
api/admin_tiers.go
Normal file
@@ -0,0 +1,488 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
"github.com/minio/console/api/operations/tiering"
|
||||
tieringApi "github.com/minio/console/api/operations/tiering"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
func registerAdminTiersHandlers(api *operations.ConsoleAPI) {
|
||||
// return a list of notification endpoints
|
||||
api.TieringTiersListHandler = tieringApi.TiersListHandlerFunc(func(params tieringApi.TiersListParams, session *models.Principal) middleware.Responder {
|
||||
tierList, err := getTiersResponse(session, params)
|
||||
if err != nil {
|
||||
return tieringApi.NewTiersListDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return tieringApi.NewTiersListOK().WithPayload(tierList)
|
||||
})
|
||||
api.TieringTiersListNamesHandler = tiering.TiersListNamesHandlerFunc(func(params tiering.TiersListNamesParams, session *models.Principal) middleware.Responder {
|
||||
tierList, err := getTiersNameResponse(session, params)
|
||||
if err != nil {
|
||||
return tieringApi.NewTiersListDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return tieringApi.NewTiersListNamesOK().WithPayload(tierList)
|
||||
})
|
||||
// add a new tiers
|
||||
api.TieringAddTierHandler = tieringApi.AddTierHandlerFunc(func(params tieringApi.AddTierParams, session *models.Principal) middleware.Responder {
|
||||
err := getAddTierResponse(session, params)
|
||||
if err != nil {
|
||||
return tieringApi.NewAddTierDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return tieringApi.NewAddTierCreated()
|
||||
})
|
||||
// get a tier
|
||||
api.TieringGetTierHandler = tieringApi.GetTierHandlerFunc(func(params tieringApi.GetTierParams, session *models.Principal) middleware.Responder {
|
||||
notifEndpoints, err := getGetTierResponse(session, params)
|
||||
if err != nil {
|
||||
return tieringApi.NewGetTierDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return tieringApi.NewGetTierOK().WithPayload(notifEndpoints)
|
||||
})
|
||||
// edit credentials for a tier
|
||||
api.TieringEditTierCredentialsHandler = tieringApi.EditTierCredentialsHandlerFunc(func(params tieringApi.EditTierCredentialsParams, session *models.Principal) middleware.Responder {
|
||||
err := getEditTierCredentialsResponse(session, params)
|
||||
if err != nil {
|
||||
return tieringApi.NewEditTierCredentialsDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return tieringApi.NewEditTierCredentialsOK()
|
||||
})
|
||||
// remove an empty tier
|
||||
api.TieringRemoveTierHandler = tieringApi.RemoveTierHandlerFunc(func(params tieringApi.RemoveTierParams, session *models.Principal) middleware.Responder {
|
||||
err := getRemoveTierResponse(session, params)
|
||||
if err != nil {
|
||||
return tieringApi.NewRemoveTierDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return tieringApi.NewRemoveTierNoContent()
|
||||
})
|
||||
}
|
||||
|
||||
// getTiers returns a list of tiers with their stats
|
||||
func getTiers(ctx context.Context, client MinioAdmin) (*models.TierListResponse, error) {
|
||||
tiers, err := client.listTiers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tierStatsInfo, err := client.tierStats(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tiersStatsMap := make(map[string]madmin.TierStats, len(tierStatsInfo))
|
||||
for _, stat := range tierStatsInfo {
|
||||
tiersStatsMap[stat.Name] = stat.Stats
|
||||
}
|
||||
|
||||
var tiersList []*models.Tier
|
||||
for _, tierData := range tiers {
|
||||
// Default Tier Stats
|
||||
tierStats := madmin.TierStats{
|
||||
NumObjects: 0,
|
||||
NumVersions: 0,
|
||||
TotalSize: 0,
|
||||
}
|
||||
if stats, ok := tiersStatsMap[tierData.Name]; ok {
|
||||
tierStats = stats
|
||||
}
|
||||
|
||||
status := client.verifyTierStatus(ctx, tierData.Name) == nil
|
||||
|
||||
switch tierData.Type {
|
||||
case madmin.S3:
|
||||
tiersList = append(tiersList, &models.Tier{
|
||||
Type: models.TierTypeS3,
|
||||
S3: &models.TierS3{
|
||||
Accesskey: tierData.S3.AccessKey,
|
||||
Bucket: tierData.S3.Bucket,
|
||||
Endpoint: tierData.S3.Endpoint,
|
||||
Name: tierData.Name,
|
||||
Prefix: tierData.S3.Prefix,
|
||||
Region: tierData.S3.Region,
|
||||
Secretkey: tierData.S3.SecretKey,
|
||||
Storageclass: tierData.S3.StorageClass,
|
||||
Usage: humanize.IBytes(tierStats.TotalSize),
|
||||
Objects: strconv.Itoa(tierStats.NumObjects),
|
||||
Versions: strconv.Itoa(tierStats.NumVersions),
|
||||
},
|
||||
Status: status,
|
||||
})
|
||||
case madmin.MinIO:
|
||||
tiersList = append(tiersList, &models.Tier{
|
||||
Type: models.TierTypeMinio,
|
||||
Minio: &models.TierMinio{
|
||||
Accesskey: tierData.MinIO.AccessKey,
|
||||
Bucket: tierData.MinIO.Bucket,
|
||||
Endpoint: tierData.MinIO.Endpoint,
|
||||
Name: tierData.Name,
|
||||
Prefix: tierData.MinIO.Prefix,
|
||||
Region: tierData.MinIO.Region,
|
||||
Secretkey: tierData.MinIO.SecretKey,
|
||||
Usage: humanize.IBytes(tierStats.TotalSize),
|
||||
Objects: strconv.Itoa(tierStats.NumObjects),
|
||||
Versions: strconv.Itoa(tierStats.NumVersions),
|
||||
},
|
||||
Status: status,
|
||||
})
|
||||
case madmin.GCS:
|
||||
tiersList = append(tiersList, &models.Tier{
|
||||
Type: models.TierTypeGcs,
|
||||
Gcs: &models.TierGcs{
|
||||
Bucket: tierData.GCS.Bucket,
|
||||
Creds: tierData.GCS.Creds,
|
||||
Endpoint: tierData.GCS.Endpoint,
|
||||
Name: tierData.Name,
|
||||
Prefix: tierData.GCS.Prefix,
|
||||
Region: tierData.GCS.Region,
|
||||
Usage: humanize.IBytes(tierStats.TotalSize),
|
||||
Objects: strconv.Itoa(tierStats.NumObjects),
|
||||
Versions: strconv.Itoa(tierStats.NumVersions),
|
||||
},
|
||||
Status: status,
|
||||
})
|
||||
case madmin.Azure:
|
||||
tiersList = append(tiersList, &models.Tier{
|
||||
Type: models.TierTypeAzure,
|
||||
Azure: &models.TierAzure{
|
||||
Accountkey: tierData.Azure.AccountKey,
|
||||
Accountname: tierData.Azure.AccountName,
|
||||
Bucket: tierData.Azure.Bucket,
|
||||
Endpoint: tierData.Azure.Endpoint,
|
||||
Name: tierData.Name,
|
||||
Prefix: tierData.Azure.Prefix,
|
||||
Region: tierData.Azure.Region,
|
||||
Usage: humanize.IBytes(tierStats.TotalSize),
|
||||
Objects: strconv.Itoa(tierStats.NumObjects),
|
||||
Versions: strconv.Itoa(tierStats.NumVersions),
|
||||
},
|
||||
Status: status,
|
||||
})
|
||||
case madmin.Unsupported:
|
||||
tiersList = append(tiersList, &models.Tier{
|
||||
Type: models.TierTypeUnsupported,
|
||||
Status: status,
|
||||
})
|
||||
}
|
||||
}
|
||||
// build response
|
||||
return &models.TierListResponse{
|
||||
Items: tiersList,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getTiersResponse returns a response with a list of tiers
|
||||
func getTiersResponse(session *models.Principal, params tieringApi.TiersListParams) (*models.TierListResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
// serialize output
|
||||
tiersResp, err := getTiers(ctx, adminClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return tiersResp, nil
|
||||
}
|
||||
|
||||
// getTiersNameResponse returns a response with a list of tiers' names
|
||||
func getTiersNameResponse(session *models.Principal, params tieringApi.TiersListNamesParams) (*models.TiersNameListResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
// serialize output
|
||||
tiersResp, err := getTiersName(ctx, adminClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return tiersResp, nil
|
||||
}
|
||||
|
||||
// getTiersName fetches listTiers and returns a list of the tiers' names
|
||||
func getTiersName(ctx context.Context, client MinioAdmin) (*models.TiersNameListResponse, error) {
|
||||
tiers, err := client.listTiers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tiersNameList := make([]string, len(tiers))
|
||||
for i, tierData := range tiers {
|
||||
tiersNameList[i] = tierData.Name
|
||||
}
|
||||
|
||||
return &models.TiersNameListResponse{
|
||||
Items: tiersNameList,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func addTier(ctx context.Context, client MinioAdmin, params *tieringApi.AddTierParams) error {
|
||||
var cfg *madmin.TierConfig
|
||||
var err error
|
||||
|
||||
switch params.Body.Type {
|
||||
|
||||
case models.TierTypeS3:
|
||||
cfg, err = madmin.NewTierS3(
|
||||
params.Body.S3.Name,
|
||||
params.Body.S3.Accesskey,
|
||||
params.Body.S3.Secretkey,
|
||||
params.Body.S3.Bucket,
|
||||
madmin.S3Region(params.Body.S3.Region),
|
||||
madmin.S3Prefix(params.Body.S3.Prefix),
|
||||
madmin.S3Endpoint(params.Body.S3.Endpoint),
|
||||
madmin.S3StorageClass(params.Body.S3.Storageclass),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case models.TierTypeMinio:
|
||||
cfg, err = madmin.NewTierMinIO(
|
||||
params.Body.Minio.Name,
|
||||
params.Body.Minio.Endpoint,
|
||||
params.Body.Minio.Accesskey,
|
||||
params.Body.Minio.Secretkey,
|
||||
params.Body.Minio.Bucket,
|
||||
madmin.MinIORegion(params.Body.Minio.Region),
|
||||
madmin.MinIOPrefix(params.Body.Minio.Prefix),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case models.TierTypeGcs:
|
||||
gcsOpts := []madmin.GCSOptions{}
|
||||
prefix := params.Body.Gcs.Prefix
|
||||
if prefix != "" {
|
||||
gcsOpts = append(gcsOpts, madmin.GCSPrefix(prefix))
|
||||
}
|
||||
|
||||
region := params.Body.Gcs.Region
|
||||
if region != "" {
|
||||
gcsOpts = append(gcsOpts, madmin.GCSRegion(region))
|
||||
}
|
||||
base64Text := make([]byte, base64.StdEncoding.EncodedLen(len(params.Body.Gcs.Creds)))
|
||||
l, _ := base64.StdEncoding.Decode(base64Text, []byte(params.Body.Gcs.Creds))
|
||||
|
||||
cfg, err = madmin.NewTierGCS(
|
||||
params.Body.Gcs.Name,
|
||||
base64Text[:l],
|
||||
params.Body.Gcs.Bucket,
|
||||
gcsOpts...,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case models.TierTypeAzure:
|
||||
cfg, err = madmin.NewTierAzure(
|
||||
params.Body.Azure.Name,
|
||||
params.Body.Azure.Accountname,
|
||||
params.Body.Azure.Accountkey,
|
||||
params.Body.Azure.Bucket,
|
||||
madmin.AzurePrefix(params.Body.Azure.Prefix),
|
||||
madmin.AzureEndpoint(params.Body.Azure.Endpoint),
|
||||
madmin.AzureRegion(params.Body.Azure.Region),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case models.TierTypeUnsupported:
|
||||
cfg = &madmin.TierConfig{
|
||||
Type: madmin.Unsupported,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
err = client.addTier(ctx, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAddTierResponse returns the response of admin tier
|
||||
func getAddTierResponse(session *models.Principal, params tieringApi.AddTierParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
// serialize output
|
||||
errTier := addTier(ctx, adminClient, ¶ms)
|
||||
if errTier != nil {
|
||||
return ErrorWithContext(ctx, errTier)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTier(ctx context.Context, client MinioAdmin, params *tieringApi.GetTierParams) (*models.Tier, error) {
|
||||
tiers, err := client.listTiers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range tiers {
|
||||
switch tiers[i].Type {
|
||||
case madmin.S3:
|
||||
if params.Type != models.TierTypeS3 || tiers[i].Name != params.Name {
|
||||
continue
|
||||
}
|
||||
return &models.Tier{
|
||||
Type: models.TierTypeS3,
|
||||
S3: &models.TierS3{
|
||||
Accesskey: tiers[i].S3.AccessKey,
|
||||
Bucket: tiers[i].S3.Bucket,
|
||||
Endpoint: tiers[i].S3.Endpoint,
|
||||
Name: tiers[i].Name,
|
||||
Prefix: tiers[i].S3.Prefix,
|
||||
Region: tiers[i].S3.Region,
|
||||
Secretkey: tiers[i].S3.SecretKey,
|
||||
Storageclass: tiers[i].S3.StorageClass,
|
||||
},
|
||||
}, err
|
||||
case madmin.GCS:
|
||||
if params.Type != models.TierTypeGcs || tiers[i].Name != params.Name {
|
||||
continue
|
||||
}
|
||||
return &models.Tier{
|
||||
Type: models.TierTypeGcs,
|
||||
Gcs: &models.TierGcs{
|
||||
Bucket: tiers[i].GCS.Bucket,
|
||||
Creds: tiers[i].GCS.Creds,
|
||||
Endpoint: tiers[i].GCS.Endpoint,
|
||||
Name: tiers[i].Name,
|
||||
Prefix: tiers[i].GCS.Prefix,
|
||||
Region: tiers[i].GCS.Region,
|
||||
},
|
||||
}, nil
|
||||
case madmin.Azure:
|
||||
if params.Type != models.TierTypeAzure || tiers[i].Name != params.Name {
|
||||
continue
|
||||
}
|
||||
return &models.Tier{
|
||||
Type: models.TierTypeAzure,
|
||||
Azure: &models.TierAzure{
|
||||
Accountkey: tiers[i].Azure.AccountKey,
|
||||
Accountname: tiers[i].Azure.AccountName,
|
||||
Bucket: tiers[i].Azure.Bucket,
|
||||
Endpoint: tiers[i].Azure.Endpoint,
|
||||
Name: tiers[i].Name,
|
||||
Prefix: tiers[i].Azure.Prefix,
|
||||
Region: tiers[i].Azure.Region,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// build response
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// getGetTierResponse returns a tier
|
||||
func getGetTierResponse(session *models.Principal, params tieringApi.GetTierParams) (*models.Tier, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
// serialize output
|
||||
addTierResp, err := getTier(ctx, adminClient, ¶ms)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return addTierResp, nil
|
||||
}
|
||||
|
||||
func editTierCredentials(ctx context.Context, client MinioAdmin, params *tieringApi.EditTierCredentialsParams) error {
|
||||
base64Text := make([]byte, base64.StdEncoding.EncodedLen(len(params.Body.Creds)))
|
||||
l, err := base64.StdEncoding.Decode(base64Text, []byte(params.Body.Creds))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
creds := madmin.TierCreds{
|
||||
AccessKey: params.Body.AccessKey,
|
||||
SecretKey: params.Body.SecretKey,
|
||||
CredsJSON: base64Text[:l],
|
||||
}
|
||||
return client.editTierCreds(ctx, params.Name, creds)
|
||||
}
|
||||
|
||||
// getEditTierCredentialsResponse returns the result of editing credentials for a tier
|
||||
func getEditTierCredentialsResponse(session *models.Principal, params tieringApi.EditTierCredentialsParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
// serialize output
|
||||
err = editTierCredentials(ctx, adminClient, ¶ms)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeTier(ctx context.Context, client MinioAdmin, params *tieringApi.RemoveTierParams) error {
|
||||
return client.removeTier(ctx, params.Name)
|
||||
}
|
||||
|
||||
func getRemoveTierResponse(session *models.Principal, params tieringApi.RemoveTierParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
// serialize output
|
||||
err = removeTier(ctx, adminClient, ¶ms)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
310
api/admin_tiers_test.go
Normal file
310
api/admin_tiers_test.go
Normal file
@@ -0,0 +1,310 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
tieringApi "github.com/minio/console/api/operations/tiering"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetTiers(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// mock minIO client
|
||||
adminClient := AdminClientMock{}
|
||||
|
||||
function := "getTiers()"
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Test-1 : getTiers() get list of tiers
|
||||
// mock lifecycle response from MinIO
|
||||
returnListMock := []*madmin.TierConfig{
|
||||
{
|
||||
Version: "V1",
|
||||
Type: madmin.S3,
|
||||
Name: "S3 Tier",
|
||||
S3: &madmin.TierS3{
|
||||
Endpoint: "https://s3tier.test.com/",
|
||||
AccessKey: "Access Key",
|
||||
SecretKey: "Secret Key",
|
||||
Bucket: "buckets3",
|
||||
Prefix: "pref1",
|
||||
Region: "us-west-1",
|
||||
StorageClass: "TT1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Version: "V1",
|
||||
Type: madmin.MinIO,
|
||||
Name: "MinIO Tier",
|
||||
MinIO: &madmin.TierMinIO{
|
||||
Endpoint: "https://minio-endpoint.test.com/",
|
||||
AccessKey: "access",
|
||||
SecretKey: "secret",
|
||||
Bucket: "somebucket",
|
||||
Prefix: "p1",
|
||||
Region: "us-east-2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
returnStatsMock := []madmin.TierInfo{
|
||||
{
|
||||
Name: "STANDARD",
|
||||
Type: "internal",
|
||||
Stats: madmin.TierStats{NumObjects: 2, NumVersions: 2, TotalSize: 228915},
|
||||
},
|
||||
{
|
||||
Name: "MinIO Tier",
|
||||
Type: "internal",
|
||||
Stats: madmin.TierStats{NumObjects: 10, NumVersions: 3, TotalSize: 132788},
|
||||
},
|
||||
{
|
||||
Name: "S3 Tier",
|
||||
Type: "s3",
|
||||
Stats: madmin.TierStats{NumObjects: 0, NumVersions: 0, TotalSize: 0},
|
||||
},
|
||||
}
|
||||
|
||||
expectedOutput := &models.TierListResponse{
|
||||
Items: []*models.Tier{
|
||||
{
|
||||
Type: models.TierTypeS3,
|
||||
S3: &models.TierS3{
|
||||
Accesskey: "Access Key",
|
||||
Secretkey: "Secret Key",
|
||||
Bucket: "buckets3",
|
||||
Endpoint: "https://s3tier.test.com/",
|
||||
Name: "S3 Tier",
|
||||
Prefix: "pref1",
|
||||
Region: "us-west-1",
|
||||
Storageclass: "TT1",
|
||||
Usage: "0 B",
|
||||
Objects: "0",
|
||||
Versions: "0",
|
||||
},
|
||||
Status: false,
|
||||
},
|
||||
{
|
||||
Type: models.TierTypeMinio,
|
||||
Minio: &models.TierMinio{
|
||||
Accesskey: "access",
|
||||
Secretkey: "secret",
|
||||
Bucket: "somebucket",
|
||||
Endpoint: "https://minio-endpoint.test.com/",
|
||||
Name: "MinIO Tier",
|
||||
Prefix: "p1",
|
||||
Region: "us-east-2",
|
||||
Usage: "130 KiB",
|
||||
Objects: "10",
|
||||
Versions: "3",
|
||||
},
|
||||
Status: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
minioListTiersMock = func(_ context.Context) ([]*madmin.TierConfig, error) {
|
||||
return returnListMock, nil
|
||||
}
|
||||
|
||||
minioTierStatsMock = func(_ context.Context) ([]madmin.TierInfo, error) {
|
||||
return returnStatsMock, nil
|
||||
}
|
||||
|
||||
minioVerifyTierStatusMock = func(_ context.Context, _ string) error {
|
||||
return fmt.Errorf("someerror")
|
||||
}
|
||||
|
||||
tiersList, err := getTiers(ctx, adminClient)
|
||||
if err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// verify length of tiers list is correct
|
||||
assert.Equal(len(tiersList.Items), len(returnListMock), fmt.Sprintf("Failed on %s: length of lists is not the same", function))
|
||||
assert.Equal(expectedOutput, tiersList)
|
||||
|
||||
// Test-2 : getTiers() list is empty
|
||||
returnListMockT2 := []*madmin.TierConfig{}
|
||||
minioListTiersMock = func(_ context.Context) ([]*madmin.TierConfig, error) {
|
||||
return returnListMockT2, nil
|
||||
}
|
||||
|
||||
tiersListT2, err := getTiers(ctx, adminClient)
|
||||
if err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
|
||||
if len(tiersListT2.Items) != 0 {
|
||||
t.Errorf("Failed on %s:, returned list was not empty", function)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTiersName(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// mock minIO client
|
||||
adminClient := AdminClientMock{}
|
||||
|
||||
function := "getTiersName()"
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Test-1 : getTiersName() get list tiers' names
|
||||
// mock lifecycle response from MinIO
|
||||
returnListMock := []*madmin.TierConfig{
|
||||
{
|
||||
Version: "V1",
|
||||
Type: madmin.S3,
|
||||
Name: "S3 Tier",
|
||||
S3: &madmin.TierS3{
|
||||
Endpoint: "https://s3tier.test.com/",
|
||||
AccessKey: "Access Key",
|
||||
SecretKey: "Secret Key",
|
||||
Bucket: "buckets3",
|
||||
Prefix: "pref1",
|
||||
Region: "us-west-1",
|
||||
StorageClass: "TT1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Version: "V1",
|
||||
Type: madmin.MinIO,
|
||||
Name: "MinIO Tier",
|
||||
MinIO: &madmin.TierMinIO{
|
||||
Endpoint: "https://minio-endpoint.test.com/",
|
||||
AccessKey: "access",
|
||||
SecretKey: "secret",
|
||||
Bucket: "somebucket",
|
||||
Prefix: "p1",
|
||||
Region: "us-east-2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedOutput := &models.TiersNameListResponse{
|
||||
Items: []string{"S3 Tier", "MinIO Tier"},
|
||||
}
|
||||
|
||||
minioListTiersMock = func(_ context.Context) ([]*madmin.TierConfig, error) {
|
||||
return returnListMock, nil
|
||||
}
|
||||
|
||||
tiersList, err := getTiersName(ctx, adminClient)
|
||||
if err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// verify length of tiers list is correct
|
||||
assert.Equal(len(tiersList.Items), len(returnListMock), fmt.Sprintf("Failed on %s: length of lists is not the same", function))
|
||||
assert.Equal(expectedOutput, tiersList)
|
||||
|
||||
// Test-2 : getTiersName() list is empty
|
||||
returnListMockT2 := []*madmin.TierConfig{}
|
||||
minioListTiersMock = func(_ context.Context) ([]*madmin.TierConfig, error) {
|
||||
return returnListMockT2, nil
|
||||
}
|
||||
|
||||
emptyTierList, err := getTiersName(ctx, adminClient)
|
||||
if err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
|
||||
if len(emptyTierList.Items) != 0 {
|
||||
t.Errorf("Failed on %s:, returned list was not empty", function)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddTier(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// mock minIO client
|
||||
adminClient := AdminClientMock{}
|
||||
|
||||
function := "addTier()"
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Test-1: addTier() add new Tier
|
||||
minioAddTiersMock = func(_ context.Context, _ *madmin.TierConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
paramsToAdd := tieringApi.AddTierParams{
|
||||
Body: &models.Tier{
|
||||
Type: "S3",
|
||||
S3: &models.TierS3{
|
||||
Accesskey: "TestAK",
|
||||
Bucket: "bucket1",
|
||||
Endpoint: "https://test.com/",
|
||||
Name: "TIERS3",
|
||||
Prefix: "Pr1",
|
||||
Region: "us-west-1",
|
||||
Secretkey: "SecretK",
|
||||
Storageclass: "STCLASS",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := addTier(ctx, adminClient, ¶msToAdd)
|
||||
assert.Equal(nil, err, fmt.Sprintf("Failed on %s: Error returned", function))
|
||||
|
||||
// Test-2: addTier() error adding Tier
|
||||
minioAddTiersMock = func(_ context.Context, _ *madmin.TierConfig) error {
|
||||
return errors.New("error setting new tier")
|
||||
}
|
||||
|
||||
err2 := addTier(ctx, adminClient, ¶msToAdd)
|
||||
|
||||
assert.Equal(errors.New("error setting new tier"), err2, fmt.Sprintf("Failed on %s: Error returned", function))
|
||||
}
|
||||
|
||||
func TestUpdateTierCreds(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// mock minIO client
|
||||
adminClient := AdminClientMock{}
|
||||
|
||||
function := "editTierCredentials()"
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Test-1: editTierCredentials() update Tier configuration
|
||||
minioEditTiersMock = func(_ context.Context, _ string, _ madmin.TierCreds) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
params := &tieringApi.EditTierCredentialsParams{
|
||||
Name: "TESTTIER",
|
||||
Body: &models.TierCredentialsRequest{
|
||||
AccessKey: "New Key",
|
||||
SecretKey: "Secret Key",
|
||||
},
|
||||
}
|
||||
|
||||
err := editTierCredentials(ctx, adminClient, params)
|
||||
|
||||
assert.Equal(nil, err, fmt.Sprintf("Failed on %s: Error returned", function))
|
||||
|
||||
// Test-2: editTierCredentials() update Tier configuration failure
|
||||
minioEditTiersMock = func(_ context.Context, _ string, _ madmin.TierCreds) error {
|
||||
return errors.New("error message")
|
||||
}
|
||||
|
||||
errT2 := editTierCredentials(ctx, adminClient, params)
|
||||
|
||||
assert.Equal(errors.New("error message"), errT2, fmt.Sprintf("Failed on %s: Error returned", function))
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package restapi
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/websocket"
|
||||
)
|
||||
|
||||
// shortTraceMsg Short trace record
|
||||
@@ -62,21 +62,21 @@ func matchTrace(opts TraceRequest, traceInfo madmin.ServiceTraceInfo) bool {
|
||||
// Filter request path if passed by the user
|
||||
if apiPath != "" {
|
||||
pathToLookup := strings.ToLower(apiPath)
|
||||
pathFromTrace := strings.ToLower(traceInfo.Trace.ReqInfo.Path)
|
||||
pathFromTrace := strings.ToLower(traceInfo.Trace.Path)
|
||||
|
||||
return strings.Contains(pathFromTrace, pathToLookup)
|
||||
}
|
||||
|
||||
// Filter response status codes if passed by the user
|
||||
if statusCode > 0 {
|
||||
statusCodeFromTrace := traceInfo.Trace.RespInfo.StatusCode
|
||||
if statusCode > 0 && traceInfo.Trace.HTTP != nil {
|
||||
statusCodeFromTrace := traceInfo.Trace.HTTP.RespInfo.StatusCode
|
||||
|
||||
return statusCodeFromTrace == statusCode
|
||||
}
|
||||
|
||||
// Filter request method if passed by the user
|
||||
if method != "" {
|
||||
methodFromTrace := traceInfo.Trace.ReqInfo.Method
|
||||
if method != "" && traceInfo.Trace.HTTP != nil {
|
||||
methodFromTrace := traceInfo.Trace.HTTP.ReqInfo.Method
|
||||
|
||||
return methodFromTrace == method
|
||||
}
|
||||
@@ -127,26 +127,28 @@ func startTraceInfo(ctx context.Context, conn WSConn, client MinioAdmin, opts Tr
|
||||
}
|
||||
|
||||
// shortTrace creates a shorter Trace Info message.
|
||||
// Same implementation as github/minio/mc/cmd/admin-trace.go
|
||||
// Same implementation as github/minio/mc/cmd/admin-trace.go
|
||||
func shortTrace(info *madmin.ServiceTraceInfo) shortTraceMsg {
|
||||
t := info.Trace
|
||||
s := shortTraceMsg{}
|
||||
|
||||
s.Time = t.ReqInfo.Time.Format(time.RFC3339)
|
||||
s.Path = t.ReqInfo.Path
|
||||
s.Query = t.ReqInfo.RawQuery
|
||||
s.Time = t.Time.Format(time.RFC3339)
|
||||
s.Path = t.Path
|
||||
s.FuncName = t.FuncName
|
||||
s.StatusCode = t.RespInfo.StatusCode
|
||||
s.StatusMsg = http.StatusText(t.RespInfo.StatusCode)
|
||||
s.CallStats.Duration = t.CallStats.Latency.String()
|
||||
s.CallStats.Rx = t.CallStats.InputBytes
|
||||
s.CallStats.Tx = t.CallStats.OutputBytes
|
||||
s.CallStats.Ttfb = t.CallStats.TimeToFirstByte.String()
|
||||
|
||||
if host, ok := t.ReqInfo.Headers["Host"]; ok {
|
||||
s.Host = strings.Join(host, "")
|
||||
s.CallStats.Duration = t.Duration.String()
|
||||
if info.Trace.HTTP != nil {
|
||||
s.Query = t.HTTP.ReqInfo.RawQuery
|
||||
s.StatusCode = t.HTTP.RespInfo.StatusCode
|
||||
s.StatusMsg = http.StatusText(t.HTTP.RespInfo.StatusCode)
|
||||
s.CallStats.Rx = t.HTTP.CallStats.InputBytes
|
||||
s.CallStats.Tx = t.HTTP.CallStats.OutputBytes
|
||||
s.CallStats.Ttfb = t.HTTP.CallStats.TimeToFirstByte.String()
|
||||
if host, ok := t.HTTP.ReqInfo.Headers["Host"]; ok {
|
||||
s.Host = strings.Join(host, "")
|
||||
}
|
||||
cSlice := strings.Split(t.HTTP.ReqInfo.Client, ":")
|
||||
s.Client = cSlice[0]
|
||||
}
|
||||
cSlice := strings.Split(t.ReqInfo.Client, ":")
|
||||
s.Client = cSlice[0]
|
||||
|
||||
return s
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package restapi
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -22,24 +22,17 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// assigning mock at runtime instead of compile time
|
||||
var minioServiceTraceMock func(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo
|
||||
|
||||
// mock function of listPolicies()
|
||||
func (ac adminClientMock) serviceTrace(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo {
|
||||
return minioServiceTraceMock(ctx, threshold, s3, internal, storage, os, errTrace)
|
||||
}
|
||||
|
||||
func TestAdminTrace(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
adminClient := AdminClientMock{}
|
||||
mockWSConn := mockConn{}
|
||||
function := "startTraceInfo(ctx, )"
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
testReceiver := make(chan shortTraceMsg, 5)
|
||||
textToReceive := "test"
|
||||
@@ -48,7 +41,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
|
||||
// Test-1: Serve Trace with no errors until trace finishes sending
|
||||
// define mock function behavior for minio server Trace
|
||||
minioServiceTraceMock = func(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo {
|
||||
minioServiceTraceMock = func(_ context.Context, _ int64, _, _, _, _, _ bool) <-chan madmin.ServiceTraceInfo {
|
||||
ch := make(chan madmin.ServiceTraceInfo)
|
||||
// Only success, start a routine to start reading line by line.
|
||||
go func(ch chan<- madmin.ServiceTraceInfo) {
|
||||
@@ -66,7 +59,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
}
|
||||
writesCount := 1
|
||||
// mock connection WriteMessage() no error
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
connWriteMessageMock = func(_ int, data []byte) error {
|
||||
// emulate that receiver gets the message written
|
||||
var t shortTraceMsg
|
||||
_ = json.Unmarshal(data, &t)
|
||||
@@ -91,7 +84,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test-2: if error happens while writing, return error
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
connWriteMessageMock = func(_ int, _ []byte) error {
|
||||
return fmt.Errorf("error on write")
|
||||
}
|
||||
if err := startTraceInfo(ctx, mockWSConn, adminClient, TraceRequest{}); assert.Error(err) {
|
||||
@@ -100,7 +93,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
|
||||
// Test-3: error happens on serviceTrace Minio, trace should stop
|
||||
// and error shall be returned.
|
||||
minioServiceTraceMock = func(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo {
|
||||
minioServiceTraceMock = func(_ context.Context, _ int64, _, _, _, _, _ bool) <-chan madmin.ServiceTraceInfo {
|
||||
ch := make(chan madmin.ServiceTraceInfo)
|
||||
// Only success, start a routine to start reading line by line.
|
||||
go func(ch chan<- madmin.ServiceTraceInfo) {
|
||||
@@ -117,7 +110,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
}(ch)
|
||||
return ch
|
||||
}
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
connWriteMessageMock = func(_ int, _ []byte) error {
|
||||
return nil
|
||||
}
|
||||
if err := startTraceInfo(ctx, mockWSConn, adminClient, TraceRequest{}); assert.Error(err) {
|
||||
722
api/admin_users.go
Normal file
722
api/admin_users.go
Normal file
@@ -0,0 +1,722 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/api/operations"
|
||||
accountApi "github.com/minio/console/api/operations/account"
|
||||
bucketApi "github.com/minio/console/api/operations/bucket"
|
||||
userApi "github.com/minio/console/api/operations/user"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
iampolicy "github.com/minio/pkg/v3/policy"
|
||||
)
|
||||
|
||||
// Policy evaluated constants
|
||||
const (
|
||||
Unknown = 0
|
||||
Allow = 1
|
||||
Deny = -1
|
||||
)
|
||||
|
||||
func registerUsersHandlers(api *operations.ConsoleAPI) {
|
||||
// List Users
|
||||
api.UserListUsersHandler = userApi.ListUsersHandlerFunc(func(params userApi.ListUsersParams, session *models.Principal) middleware.Responder {
|
||||
listUsersResponse, err := getListUsersResponse(session, params)
|
||||
if err != nil {
|
||||
return userApi.NewListUsersDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return userApi.NewListUsersOK().WithPayload(listUsersResponse)
|
||||
})
|
||||
// Add User
|
||||
api.UserAddUserHandler = userApi.AddUserHandlerFunc(func(params userApi.AddUserParams, session *models.Principal) middleware.Responder {
|
||||
userResponse, err := getUserAddResponse(session, params)
|
||||
if err != nil {
|
||||
return userApi.NewAddUserDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return userApi.NewAddUserCreated().WithPayload(userResponse)
|
||||
})
|
||||
// Remove User
|
||||
api.UserRemoveUserHandler = userApi.RemoveUserHandlerFunc(func(params userApi.RemoveUserParams, session *models.Principal) middleware.Responder {
|
||||
err := getRemoveUserResponse(session, params)
|
||||
if err != nil {
|
||||
return userApi.NewRemoveUserDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return userApi.NewRemoveUserNoContent()
|
||||
})
|
||||
// Update User-Groups
|
||||
api.UserUpdateUserGroupsHandler = userApi.UpdateUserGroupsHandlerFunc(func(params userApi.UpdateUserGroupsParams, session *models.Principal) middleware.Responder {
|
||||
userUpdateResponse, err := getUpdateUserGroupsResponse(session, params)
|
||||
if err != nil {
|
||||
return userApi.NewUpdateUserGroupsDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
|
||||
return userApi.NewUpdateUserGroupsOK().WithPayload(userUpdateResponse)
|
||||
})
|
||||
// Get User
|
||||
api.UserGetUserInfoHandler = userApi.GetUserInfoHandlerFunc(func(params userApi.GetUserInfoParams, session *models.Principal) middleware.Responder {
|
||||
userInfoResponse, err := getUserInfoResponse(session, params)
|
||||
if err != nil {
|
||||
return userApi.NewGetUserInfoDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
|
||||
return userApi.NewGetUserInfoOK().WithPayload(userInfoResponse)
|
||||
})
|
||||
// Update User
|
||||
api.UserUpdateUserInfoHandler = userApi.UpdateUserInfoHandlerFunc(func(params userApi.UpdateUserInfoParams, session *models.Principal) middleware.Responder {
|
||||
userUpdateResponse, err := getUpdateUserResponse(session, params)
|
||||
if err != nil {
|
||||
return userApi.NewUpdateUserInfoDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
|
||||
return userApi.NewUpdateUserInfoOK().WithPayload(userUpdateResponse)
|
||||
})
|
||||
// Update User-Groups Bulk
|
||||
api.UserBulkUpdateUsersGroupsHandler = userApi.BulkUpdateUsersGroupsHandlerFunc(func(params userApi.BulkUpdateUsersGroupsParams, session *models.Principal) middleware.Responder {
|
||||
err := getAddUsersListToGroupsResponse(session, params)
|
||||
if err != nil {
|
||||
return userApi.NewBulkUpdateUsersGroupsDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
|
||||
return userApi.NewBulkUpdateUsersGroupsOK()
|
||||
})
|
||||
api.BucketListUsersWithAccessToBucketHandler = bucketApi.ListUsersWithAccessToBucketHandlerFunc(func(params bucketApi.ListUsersWithAccessToBucketParams, session *models.Principal) middleware.Responder {
|
||||
response, err := getListUsersWithAccessToBucketResponse(session, params)
|
||||
if err != nil {
|
||||
return bucketApi.NewListUsersWithAccessToBucketDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return bucketApi.NewListUsersWithAccessToBucketOK().WithPayload(response)
|
||||
})
|
||||
// Change User Password
|
||||
api.AccountChangeUserPasswordHandler = accountApi.ChangeUserPasswordHandlerFunc(func(params accountApi.ChangeUserPasswordParams, session *models.Principal) middleware.Responder {
|
||||
err := getChangeUserPasswordResponse(session, params)
|
||||
if err != nil {
|
||||
return accountApi.NewChangeUserPasswordDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return accountApi.NewChangeUserPasswordCreated()
|
||||
})
|
||||
// Check number of Service Accounts for listed users
|
||||
api.UserCheckUserServiceAccountsHandler = userApi.CheckUserServiceAccountsHandlerFunc(func(params userApi.CheckUserServiceAccountsParams, session *models.Principal) middleware.Responder {
|
||||
userSAList, err := getCheckUserSAResponse(session, params)
|
||||
if err != nil {
|
||||
return userApi.NewCheckUserServiceAccountsDefault(err.Code).WithPayload(err.APIError)
|
||||
}
|
||||
return userApi.NewCheckUserServiceAccountsOK().WithPayload(userSAList)
|
||||
})
|
||||
}
|
||||
|
||||
func listUsers(ctx context.Context, client MinioAdmin) ([]*models.User, error) {
|
||||
// Get list of all users in the MinIO
|
||||
// This call requires explicit authentication, no anonymous requests are
|
||||
// allowed for listing users.
|
||||
userMap, err := client.listUsers(ctx)
|
||||
if err != nil {
|
||||
return []*models.User{}, err
|
||||
}
|
||||
|
||||
var users []*models.User
|
||||
for accessKey, user := range userMap {
|
||||
userElem := &models.User{
|
||||
AccessKey: accessKey,
|
||||
Status: string(user.Status),
|
||||
Policy: strings.Split(user.PolicyName, ","),
|
||||
MemberOf: user.MemberOf,
|
||||
}
|
||||
users = append(users, userElem)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// getListUsersResponse performs listUsers() and serializes it to the handler's output
|
||||
func getListUsersResponse(session *models.Principal, params userApi.ListUsersParams) (*models.ListUsersResponse, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
users, err := listUsers(ctx, adminClient)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// serialize output
|
||||
listUsersResponse := &models.ListUsersResponse{
|
||||
Users: users,
|
||||
}
|
||||
return listUsersResponse, nil
|
||||
}
|
||||
|
||||
// addUser invokes adding a users on `MinioAdmin` and builds the response `models.User`
|
||||
func addUser(ctx context.Context, client MinioAdmin, accessKey, secretKey *string, groups []string, policies []string) (*models.User, error) {
|
||||
// Calls into MinIO to add a new user if there's an errors return it
|
||||
if err := client.addUser(ctx, *accessKey, *secretKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// set groups for the newly created user
|
||||
var userWithGroups *models.User
|
||||
if len(groups) > 0 {
|
||||
var errUG error
|
||||
userWithGroups, errUG = updateUserGroups(ctx, client, *accessKey, groups)
|
||||
|
||||
if errUG != nil {
|
||||
return nil, errUG
|
||||
}
|
||||
}
|
||||
// set policies for the newly created user
|
||||
if len(policies) > 0 {
|
||||
policyString := strings.Join(policies, ",")
|
||||
if err := SetPolicy(ctx, client, policyString, *accessKey, "user"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
memberOf := []string{}
|
||||
status := "enabled"
|
||||
if userWithGroups != nil {
|
||||
memberOf = userWithGroups.MemberOf
|
||||
status = userWithGroups.Status
|
||||
}
|
||||
|
||||
userRet := &models.User{
|
||||
AccessKey: *accessKey,
|
||||
MemberOf: memberOf,
|
||||
Policy: policies,
|
||||
Status: status,
|
||||
}
|
||||
return userRet, nil
|
||||
}
|
||||
|
||||
func getUserAddResponse(session *models.Principal, params userApi.AddUserParams) (*models.User, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
var userExists bool
|
||||
|
||||
_, err = adminClient.getUserInfo(ctx, *params.Body.AccessKey)
|
||||
userExists = err == nil
|
||||
|
||||
if userExists {
|
||||
return nil, ErrorWithContext(ctx, ErrNonUniqueAccessKey)
|
||||
}
|
||||
user, err := addUser(
|
||||
ctx,
|
||||
adminClient,
|
||||
params.Body.AccessKey,
|
||||
params.Body.SecretKey,
|
||||
params.Body.Groups,
|
||||
params.Body.Policies,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// removeUser invokes removing an user on `MinioAdmin`, then we return the response from API
|
||||
func removeUser(ctx context.Context, client MinioAdmin, accessKey string) error {
|
||||
return client.removeUser(ctx, accessKey)
|
||||
}
|
||||
|
||||
func getRemoveUserResponse(session *models.Principal, params userApi.RemoveUserParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
if session.AccountAccessKey == params.Name {
|
||||
return ErrorWithContext(ctx, ErrAvoidSelfAccountDelete)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
if err := removeUser(ctx, adminClient, params.Name); err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getUserInfo calls MinIO server get the User Information
|
||||
func getUserInfo(ctx context.Context, client MinioAdmin, accessKey string) (*madmin.UserInfo, error) {
|
||||
userInfo, err := client.getUserInfo(ctx, accessKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
||||
func getUserInfoResponse(session *models.Principal, params userApi.GetUserInfoParams) (*models.User, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
user, err := getUserInfo(ctx, adminClient, params.Name)
|
||||
if err != nil {
|
||||
// User doesn't exist, return 404
|
||||
if madmin.ToErrorResponse(err).Code == "XMinioAdminNoSuchUser" {
|
||||
errorCode := 404
|
||||
errorMessage := "User doesn't exist"
|
||||
return nil, &CodedAPIError{Code: errorCode, APIError: &models.APIError{Message: errorMessage, DetailedMessage: err.Error()}}
|
||||
}
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
var policies []string
|
||||
if user.PolicyName == "" {
|
||||
policies = []string{}
|
||||
} else {
|
||||
policies = strings.Split(user.PolicyName, ",")
|
||||
}
|
||||
|
||||
hasPolicy := true
|
||||
|
||||
if len(policies) == 0 {
|
||||
hasPolicy = false
|
||||
for i := 0; i < len(user.MemberOf); i++ {
|
||||
group, err := adminClient.getGroupDescription(ctx, user.MemberOf[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if group.Policy != "" {
|
||||
hasPolicy = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userInformation := &models.User{
|
||||
AccessKey: params.Name,
|
||||
MemberOf: user.MemberOf,
|
||||
Policy: policies,
|
||||
Status: string(user.Status),
|
||||
HasPolicy: hasPolicy,
|
||||
}
|
||||
|
||||
return userInformation, nil
|
||||
}
|
||||
|
||||
// updateUserGroups invokes getUserInfo() to get the old groups from the user,
|
||||
// then we merge the list with the new groups list to have a shorter iteration between groups and we do a comparison between the current and old groups.
|
||||
// We delete or update the groups according the location in each list and send the user with the new groups from `MinioAdmin` to the client
|
||||
func updateUserGroups(ctx context.Context, client MinioAdmin, user string, groupsToAssign []string) (*models.User, error) {
|
||||
parallelUserUpdate := func(groupName string, originGroups []string) chan error {
|
||||
chProcess := make(chan error)
|
||||
|
||||
go func() error {
|
||||
defer close(chProcess)
|
||||
|
||||
// Compare if groupName is in the arrays
|
||||
isGroupPersistent := IsElementInArray(groupsToAssign, groupName)
|
||||
isInOriginGroups := IsElementInArray(originGroups, groupName)
|
||||
|
||||
if isGroupPersistent && isInOriginGroups { // Group is already assigned and doesn't need to be updated
|
||||
chProcess <- nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
isRemove := false // User is added by default
|
||||
|
||||
// User is deleted from the group
|
||||
if !isGroupPersistent {
|
||||
isRemove = true
|
||||
}
|
||||
|
||||
userToAddRemove := []string{user}
|
||||
|
||||
updateReturn := updateGroupMembers(ctx, client, groupName, userToAddRemove, isRemove)
|
||||
|
||||
chProcess <- updateReturn
|
||||
|
||||
return updateReturn
|
||||
}()
|
||||
|
||||
return chProcess
|
||||
}
|
||||
|
||||
userInfoOr, err := getUserInfo(ctx, client, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
memberOf := userInfoOr.MemberOf
|
||||
mergedGroupArray := UniqueKeys(append(memberOf, groupsToAssign...))
|
||||
|
||||
var listOfUpdates []chan error
|
||||
|
||||
// Each group must be updated individually because there is no way to update all the groups at once for a user,
|
||||
// we are using the same logic as 'mc admin group add' command
|
||||
for _, groupN := range mergedGroupArray {
|
||||
proc := parallelUserUpdate(groupN, memberOf)
|
||||
listOfUpdates = append(listOfUpdates, proc)
|
||||
}
|
||||
|
||||
channelHasError := false
|
||||
|
||||
for _, chanRet := range listOfUpdates {
|
||||
locError := <-chanRet
|
||||
|
||||
if locError != nil {
|
||||
channelHasError = true
|
||||
}
|
||||
}
|
||||
|
||||
if channelHasError {
|
||||
errRt := errors.New(500, "there was an error updating the groups")
|
||||
return nil, errRt
|
||||
}
|
||||
|
||||
userInfo, err := getUserInfo(ctx, client, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policies := strings.Split(userInfo.PolicyName, ",")
|
||||
|
||||
userReturn := &models.User{
|
||||
AccessKey: user,
|
||||
MemberOf: userInfo.MemberOf,
|
||||
Policy: policies,
|
||||
Status: string(userInfo.Status),
|
||||
}
|
||||
|
||||
return userReturn, nil
|
||||
}
|
||||
|
||||
func getUpdateUserGroupsResponse(session *models.Principal, params userApi.UpdateUserGroupsParams) (*models.User, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
user, err := updateUserGroups(ctx, adminClient, params.Name, params.Body.Groups)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// setUserStatus invokes setUserStatus from madmin to update user status
|
||||
func setUserStatus(ctx context.Context, client MinioAdmin, user string, status string) error {
|
||||
var setStatus madmin.AccountStatus
|
||||
switch status {
|
||||
case "enabled":
|
||||
setStatus = madmin.AccountEnabled
|
||||
case "disabled":
|
||||
setStatus = madmin.AccountDisabled
|
||||
default:
|
||||
return errors.New(500, "status not valid")
|
||||
}
|
||||
|
||||
return client.setUserStatus(ctx, user, setStatus)
|
||||
}
|
||||
|
||||
func getUpdateUserResponse(session *models.Principal, params userApi.UpdateUserInfoParams) (*models.User, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
status := *params.Body.Status
|
||||
groups := params.Body.Groups
|
||||
|
||||
if err := setUserStatus(ctx, adminClient, params.Name, status); err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
userElem, errUG := updateUserGroups(ctx, adminClient, params.Name, groups)
|
||||
|
||||
if errUG != nil {
|
||||
return nil, ErrorWithContext(ctx, errUG)
|
||||
}
|
||||
return userElem, nil
|
||||
}
|
||||
|
||||
// addUsersListToGroups iterates over the user list & assigns the requested groups to each user.
|
||||
func addUsersListToGroups(ctx context.Context, client MinioAdmin, usersToUpdate []string, groupsToAssign []string) error {
|
||||
// We update each group with the complete usersList
|
||||
parallelGroupsUpdate := func(groupToAssign string) chan error {
|
||||
groupProcess := make(chan error)
|
||||
|
||||
go func() {
|
||||
defer close(groupProcess)
|
||||
// We add the users array to the group.
|
||||
err := updateGroupMembers(ctx, client, groupToAssign, usersToUpdate, false)
|
||||
|
||||
groupProcess <- err
|
||||
}()
|
||||
return groupProcess
|
||||
}
|
||||
|
||||
var groupsUpdateList []chan error
|
||||
|
||||
// We get each group name & add users accordingly
|
||||
for _, groupName := range groupsToAssign {
|
||||
// We update the group
|
||||
proc := parallelGroupsUpdate(groupName)
|
||||
groupsUpdateList = append(groupsUpdateList, proc)
|
||||
}
|
||||
|
||||
errorsList := []string{} // We get the errors list because we want to have all errors at once.
|
||||
for _, err := range groupsUpdateList {
|
||||
errorFromUpdate := <-err // We store the errors to avoid Data Race
|
||||
if errorFromUpdate != nil {
|
||||
// If there is an errors, we store the errors strings so we can join them after we receive all errors
|
||||
errorsList = append(errorsList, errorFromUpdate.Error()) // We wait until all the channels have been closed.
|
||||
}
|
||||
}
|
||||
|
||||
// If there are errors, we throw the final errors with the errors inside
|
||||
if len(errorsList) > 0 {
|
||||
errGen := fmt.Errorf("error in users-groups assignation: %q", strings.Join(errorsList, ","))
|
||||
return errGen
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAddUsersListToGroupsResponse(session *models.Principal, params userApi.BulkUpdateUsersGroupsParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
usersList := params.Body.Users
|
||||
groupsList := params.Body.Groups
|
||||
|
||||
if err := addUsersListToGroups(ctx, adminClient, usersList, groupsList); err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getListUsersWithAccessToBucketResponse(session *models.Principal, params bucketApi.ListUsersWithAccessToBucketParams) ([]string, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
list, err := listUsersWithAccessToBucket(ctx, adminClient, params.Bucket)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func policyAllowsAndMatchesBucket(policy *iampolicy.Policy, bucket string) int {
|
||||
policyStatements := policy.Statements
|
||||
for i := 0; i < len(policyStatements); i++ {
|
||||
resources := policyStatements[i].Resources
|
||||
effect := policyStatements[i].Effect
|
||||
if resources.Match(bucket, map[string][]string{}) {
|
||||
if effect.IsValid() {
|
||||
if effect.IsAllowed(true) {
|
||||
return Allow
|
||||
}
|
||||
return Deny
|
||||
}
|
||||
}
|
||||
}
|
||||
return Unknown
|
||||
}
|
||||
|
||||
func listUsersWithAccessToBucket(ctx context.Context, adminClient MinioAdmin, bucket string) ([]string, error) {
|
||||
users, err := adminClient.listUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var retval []string
|
||||
akHasAccess := make(map[string]struct{})
|
||||
akIsDenied := make(map[string]struct{})
|
||||
for k, v := range users {
|
||||
for _, policyName := range strings.Split(v.PolicyName, ",") {
|
||||
policyName = strings.TrimSpace(policyName)
|
||||
if policyName == "" {
|
||||
continue
|
||||
}
|
||||
policy, err := adminClient.getPolicy(ctx, policyName)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("unable to fetch policy %s: %v", policyName, err))
|
||||
continue
|
||||
}
|
||||
if _, ok := akIsDenied[k]; !ok {
|
||||
switch policyAllowsAndMatchesBucket(policy, bucket) {
|
||||
case Allow:
|
||||
if _, ok := akHasAccess[k]; !ok {
|
||||
akHasAccess[k] = struct{}{}
|
||||
}
|
||||
case Deny:
|
||||
akIsDenied[k] = struct{}{}
|
||||
delete(akHasAccess, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groups, err := adminClient.listGroups(ctx)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("unable to list groups: %v", err))
|
||||
return retval, nil
|
||||
}
|
||||
|
||||
for _, groupName := range groups {
|
||||
info, err := groupInfo(ctx, adminClient, groupName)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("unable to fetch group info %s: %v", groupName, err))
|
||||
continue
|
||||
}
|
||||
policy, err := adminClient.getPolicy(ctx, info.Policy)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, fmt.Errorf("unable to fetch group policy %s: %v", info.Policy, err))
|
||||
continue
|
||||
}
|
||||
for _, member := range info.Members {
|
||||
if _, ok := akIsDenied[member]; !ok {
|
||||
switch policyAllowsAndMatchesBucket(policy, bucket) {
|
||||
case Allow:
|
||||
if _, ok := akHasAccess[member]; !ok {
|
||||
akHasAccess[member] = struct{}{}
|
||||
}
|
||||
case Deny:
|
||||
akIsDenied[member] = struct{}{}
|
||||
delete(akHasAccess, member)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for k := range akHasAccess {
|
||||
retval = append(retval, k)
|
||||
}
|
||||
sort.Strings(retval)
|
||||
return retval, nil
|
||||
}
|
||||
|
||||
// changeUserPassword changes password of selectedUser to newSecretKey
|
||||
func changeUserPassword(ctx context.Context, client MinioAdmin, selectedUser string, newSecretKey string) error {
|
||||
return client.changePassword(ctx, selectedUser, newSecretKey)
|
||||
}
|
||||
|
||||
// getChangeUserPasswordResponse will change the password of selctedUser to newSecretKey
|
||||
func getChangeUserPasswordResponse(session *models.Principal, params accountApi.ChangeUserPasswordParams) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
// params will contain selectedUser and newSecretKey credentials for the user
|
||||
user := *params.Body.SelectedUser
|
||||
newSecretKey := *params.Body.NewSecretKey
|
||||
|
||||
// changes password of user to newSecretKey
|
||||
if err := changeUserPassword(ctx, adminClient, user, newSecretKey); err != nil {
|
||||
return ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCheckUserSAResponse(session *models.Principal, params userApi.CheckUserServiceAccountsParams) (*models.UserServiceAccountSummary, *CodedAPIError) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
var userServiceAccountList []*models.UserServiceAccountItem
|
||||
hasSA := false
|
||||
for _, user := range params.SelectedUsers {
|
||||
listServAccs, err := adminClient.listServiceAccounts(ctx, user)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
numSAs := int64(len(listServAccs.Accounts))
|
||||
if numSAs > 0 {
|
||||
hasSA = true
|
||||
}
|
||||
userAccountItem := &models.UserServiceAccountItem{
|
||||
UserName: user,
|
||||
NumSAs: numSAs,
|
||||
}
|
||||
userServiceAccountList = append(userServiceAccountList, userAccountItem)
|
||||
}
|
||||
|
||||
userAccountList := &models.UserServiceAccountSummary{
|
||||
UserServiceAccountList: userServiceAccountList,
|
||||
HasSA: hasSA,
|
||||
}
|
||||
|
||||
return userAccountList, nil
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package restapi
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -24,47 +24,16 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
iampolicy "github.com/minio/pkg/v3/policy"
|
||||
asrt "github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// assigning mock at runtime instead of compile time
|
||||
var minioListUsersMock func() (map[string]madmin.UserInfo, error)
|
||||
var minioAddUserMock func(accessKey, secreyKey string) error
|
||||
var minioRemoveUserMock func(accessKey string) error
|
||||
var minioGetUserInfoMock func(accessKey string) (madmin.UserInfo, error)
|
||||
var minioSetUserStatusMock func(accessKey string, status madmin.AccountStatus) error
|
||||
|
||||
// mock function of listUsers()
|
||||
func (ac adminClientMock) listUsers(ctx context.Context) (map[string]madmin.UserInfo, error) {
|
||||
return minioListUsersMock()
|
||||
}
|
||||
|
||||
// mock function of addUser()
|
||||
func (ac adminClientMock) addUser(ctx context.Context, accessKey, secretKey string) error {
|
||||
return minioAddUserMock(accessKey, secretKey)
|
||||
}
|
||||
|
||||
// mock function of removeUser()
|
||||
func (ac adminClientMock) removeUser(ctx context.Context, accessKey string) error {
|
||||
return minioRemoveUserMock(accessKey)
|
||||
}
|
||||
|
||||
//mock function of getUserInfo()
|
||||
func (ac adminClientMock) getUserInfo(ctx context.Context, accessKey string) (madmin.UserInfo, error) {
|
||||
return minioGetUserInfoMock(accessKey)
|
||||
}
|
||||
|
||||
//mock function of setUserStatus()
|
||||
func (ac adminClientMock) setUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) error {
|
||||
return minioSetUserStatusMock(accessKey, status)
|
||||
}
|
||||
|
||||
func TestListUsers(t *testing.T) {
|
||||
assert := asrt.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
ctx := context.Background()
|
||||
adminClient := AdminClientMock{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Test-1 : listUsers() Get response from minio client with two users and return the same number on listUsers()
|
||||
// mock minIO client
|
||||
mockUserMap := map[string]madmin.UserInfo{
|
||||
@@ -116,8 +85,9 @@ func TestListUsers(t *testing.T) {
|
||||
|
||||
func TestAddUser(t *testing.T) {
|
||||
assert := asrt.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
ctx := context.Background()
|
||||
adminClient := AdminClientMock{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Test-1: valid case of adding a user with a proper access key
|
||||
accessKey := "ABCDEFGHI"
|
||||
secretKey := "ABCDEFGHIABCDEFGHI"
|
||||
@@ -132,15 +102,15 @@ func TestAddUser(t *testing.T) {
|
||||
}
|
||||
|
||||
// mock function response from addUser() return no error
|
||||
minioAddUserMock = func(accessKey, secretKey string) error {
|
||||
minioAddUserMock = func(_, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
minioGetUserInfoMock = func(accessKey string) (madmin.UserInfo, error) {
|
||||
minioGetUserInfoMock = func(_ string) (madmin.UserInfo, error) {
|
||||
return *mockResponse, nil
|
||||
}
|
||||
|
||||
minioUpdateGroupMembersMock = func(remove madmin.GroupAddRemove) error {
|
||||
minioUpdateGroupMembersMock = func(_ madmin.GroupAddRemove) error {
|
||||
return nil
|
||||
}
|
||||
// Test-1: Add a user
|
||||
@@ -165,7 +135,7 @@ func TestAddUser(t *testing.T) {
|
||||
accessKey = "AB"
|
||||
secretKey = "ABCDEFGHIABCDEFGHI"
|
||||
// mock function response from addUser() return no error
|
||||
minioAddUserMock = func(accessKey, secretKey string) error {
|
||||
minioAddUserMock = func(_, _ string) error {
|
||||
return errors.New("error")
|
||||
}
|
||||
|
||||
@@ -180,7 +150,7 @@ func TestAddUser(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test-4: add groups function returns an error
|
||||
minioUpdateGroupMembersMock = func(remove madmin.GroupAddRemove) error {
|
||||
minioUpdateGroupMembersMock = func(_ madmin.GroupAddRemove) error {
|
||||
return errors.New("error")
|
||||
}
|
||||
|
||||
@@ -198,13 +168,14 @@ func TestAddUser(t *testing.T) {
|
||||
func TestRemoveUser(t *testing.T) {
|
||||
assert := asrt.New(t)
|
||||
// mock minIO client
|
||||
adminClient := adminClientMock{}
|
||||
ctx := context.Background()
|
||||
adminClient := AdminClientMock{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
function := "removeUser()"
|
||||
|
||||
// Test-1: removeUser() delete a user
|
||||
// mock function response from removeUser(accessKey)
|
||||
minioRemoveUserMock = func(accessKey string) error {
|
||||
minioRemoveUserMock = func(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -214,7 +185,7 @@ func TestRemoveUser(t *testing.T) {
|
||||
|
||||
// Test-2: removeUser() make sure errors are handled correctly when error on DeleteUser()
|
||||
// mock function response from removeUser(accessKey)
|
||||
minioRemoveUserMock = func(accessKey string) error {
|
||||
minioRemoveUserMock = func(_ string) error {
|
||||
return errors.New("error")
|
||||
}
|
||||
|
||||
@@ -226,8 +197,9 @@ func TestRemoveUser(t *testing.T) {
|
||||
func TestUserGroups(t *testing.T) {
|
||||
assert := asrt.New(t)
|
||||
// mock minIO client
|
||||
adminClient := adminClientMock{}
|
||||
ctx := context.Background()
|
||||
adminClient := AdminClientMock{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
function := "updateUserGroups()"
|
||||
mockUserGroups := []string{"group1", "group2", "group3"}
|
||||
@@ -248,11 +220,11 @@ func TestUserGroups(t *testing.T) {
|
||||
// Test-1: updateUserGroups() updates the groups for a user
|
||||
// mock function response from updateUserGroups(accessKey, groupsToAssign)
|
||||
|
||||
minioGetUserInfoMock = func(accessKey string) (madmin.UserInfo, error) {
|
||||
minioGetUserInfoMock = func(_ string) (madmin.UserInfo, error) {
|
||||
return *mockResponse, nil
|
||||
}
|
||||
|
||||
minioUpdateGroupMembersMock = func(remove madmin.GroupAddRemove) error {
|
||||
minioUpdateGroupMembersMock = func(_ madmin.GroupAddRemove) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -263,7 +235,7 @@ func TestUserGroups(t *testing.T) {
|
||||
// Test-2: updateUserGroups() make sure errors are handled correctly when error on UpdateGroupMembersMock()
|
||||
// mock function response from removeUser(accessKey)
|
||||
|
||||
minioUpdateGroupMembersMock = func(remove madmin.GroupAddRemove) error {
|
||||
minioUpdateGroupMembersMock = func(_ madmin.GroupAddRemove) error {
|
||||
return errors.New("error")
|
||||
}
|
||||
|
||||
@@ -272,11 +244,11 @@ func TestUserGroups(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test-3: updateUserGroups() make sure we return the correct error when getUserInfo returns error
|
||||
minioGetUserInfoMock = func(accessKey string) (madmin.UserInfo, error) {
|
||||
minioGetUserInfoMock = func(_ string) (madmin.UserInfo, error) {
|
||||
return *mockEmptyResponse, errors.New("error getting user ")
|
||||
}
|
||||
|
||||
minioUpdateGroupMembersMock = func(remove madmin.GroupAddRemove) error {
|
||||
minioUpdateGroupMembersMock = func(_ madmin.GroupAddRemove) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -287,8 +259,9 @@ func TestUserGroups(t *testing.T) {
|
||||
|
||||
func TestGetUserInfo(t *testing.T) {
|
||||
assert := asrt.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
ctx := context.Background()
|
||||
adminClient := AdminClientMock{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Test-1 : getUserInfo() get user info
|
||||
userName := "userNameTest"
|
||||
@@ -306,7 +279,7 @@ func TestGetUserInfo(t *testing.T) {
|
||||
}
|
||||
|
||||
// mock function response from getUserInfo()
|
||||
minioGetUserInfoMock = func(username string) (madmin.UserInfo, error) {
|
||||
minioGetUserInfoMock = func(_ string) (madmin.UserInfo, error) {
|
||||
return *mockResponse, nil
|
||||
}
|
||||
function := "getUserInfo()"
|
||||
@@ -321,7 +294,7 @@ func TestGetUserInfo(t *testing.T) {
|
||||
assert.Equal(mockResponse.Status, info.Status)
|
||||
|
||||
// Test-2 : getUserInfo() Return error and see that the error is handled correctly and returned
|
||||
minioGetUserInfoMock = func(username string) (madmin.UserInfo, error) {
|
||||
minioGetUserInfoMock = func(_ string) (madmin.UserInfo, error) {
|
||||
return *emptyMockResponse, errors.New("error")
|
||||
}
|
||||
_, err = getUserInfo(ctx, adminClient, userName)
|
||||
@@ -332,14 +305,15 @@ func TestGetUserInfo(t *testing.T) {
|
||||
|
||||
func TestSetUserStatus(t *testing.T) {
|
||||
assert := asrt.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
adminClient := AdminClientMock{}
|
||||
function := "setUserStatus()"
|
||||
userName := "userName123"
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Test-1: setUserStatus() update valid disabled status
|
||||
expectedStatus := "disabled"
|
||||
minioSetUserStatusMock = func(accessKey string, status madmin.AccountStatus) error {
|
||||
minioSetUserStatusMock = func(_ string, _ madmin.AccountStatus) error {
|
||||
return nil
|
||||
}
|
||||
if err := setUserStatus(ctx, adminClient, userName, expectedStatus); err != nil {
|
||||
@@ -347,7 +321,7 @@ func TestSetUserStatus(t *testing.T) {
|
||||
}
|
||||
// Test-2: setUserStatus() update valid enabled status
|
||||
expectedStatus = "enabled"
|
||||
minioSetUserStatusMock = func(accessKey string, status madmin.AccountStatus) error {
|
||||
minioSetUserStatusMock = func(_ string, _ madmin.AccountStatus) error {
|
||||
return nil
|
||||
}
|
||||
if err := setUserStatus(ctx, adminClient, userName, expectedStatus); err != nil {
|
||||
@@ -355,7 +329,7 @@ func TestSetUserStatus(t *testing.T) {
|
||||
}
|
||||
// Test-3: setUserStatus() update invalid status, should send error
|
||||
expectedStatus = "invalid"
|
||||
minioSetUserStatusMock = func(accessKey string, status madmin.AccountStatus) error {
|
||||
minioSetUserStatusMock = func(_ string, _ madmin.AccountStatus) error {
|
||||
return nil
|
||||
}
|
||||
if err := setUserStatus(ctx, adminClient, userName, expectedStatus); assert.Error(err) {
|
||||
@@ -363,7 +337,7 @@ func TestSetUserStatus(t *testing.T) {
|
||||
}
|
||||
// Test-4: setUserStatus() handler error correctly
|
||||
expectedStatus = "enabled"
|
||||
minioSetUserStatusMock = func(accessKey string, status madmin.AccountStatus) error {
|
||||
minioSetUserStatusMock = func(_ string, _ madmin.AccountStatus) error {
|
||||
return errors.New("error")
|
||||
}
|
||||
if err := setUserStatus(ctx, adminClient, userName, expectedStatus); assert.Error(err) {
|
||||
@@ -374,8 +348,9 @@ func TestSetUserStatus(t *testing.T) {
|
||||
func TestUserGroupsBulk(t *testing.T) {
|
||||
assert := asrt.New(t)
|
||||
// mock minIO client
|
||||
adminClient := adminClientMock{}
|
||||
ctx := context.Background()
|
||||
adminClient := AdminClientMock{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
function := "updateUserGroups()"
|
||||
mockUserGroups := []string{"group1", "group2", "group3"}
|
||||
@@ -383,7 +358,7 @@ func TestUserGroupsBulk(t *testing.T) {
|
||||
|
||||
// Test-1: addUsersListToGroups() updates the groups for a users list
|
||||
// mock function response from updateUserGroups(accessKey, groupsToAssign)
|
||||
minioUpdateGroupMembersMock = func(remove madmin.GroupAddRemove) error {
|
||||
minioUpdateGroupMembersMock = func(_ madmin.GroupAddRemove) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -393,7 +368,7 @@ func TestUserGroupsBulk(t *testing.T) {
|
||||
|
||||
// Test-2: addUsersListToGroups() make sure errors are handled correctly when error on updateGroupMembers()
|
||||
// mock function response from removeUser(accessKey)
|
||||
minioUpdateGroupMembersMock = func(remove madmin.GroupAddRemove) error {
|
||||
minioUpdateGroupMembersMock = func(_ madmin.GroupAddRemove) error {
|
||||
return errors.New("error")
|
||||
}
|
||||
|
||||
@@ -404,14 +379,17 @@ func TestUserGroupsBulk(t *testing.T) {
|
||||
|
||||
func TestListUsersWithAccessToBucket(t *testing.T) {
|
||||
assert := asrt.New(t)
|
||||
ctx := context.Background()
|
||||
adminClient := adminClientMock{}
|
||||
user1 := madmin.UserInfo{SecretKey: "testtest",
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
adminClient := AdminClientMock{}
|
||||
user1 := madmin.UserInfo{
|
||||
SecretKey: "testtest",
|
||||
PolicyName: "consoleAdmin,testPolicy,redundantPolicy",
|
||||
Status: "enabled",
|
||||
MemberOf: []string{"group1"},
|
||||
}
|
||||
user2 := madmin.UserInfo{SecretKey: "testtest",
|
||||
user2 := madmin.UserInfo{
|
||||
SecretKey: "testtest",
|
||||
PolicyName: "testPolicy, otherPolicy",
|
||||
Status: "enabled",
|
||||
MemberOf: []string{"group1"},
|
||||
@@ -517,7 +495,7 @@ func TestListUsersWithAccessToBucket(t *testing.T) {
|
||||
}
|
||||
return mockResponse, nil
|
||||
}
|
||||
return nil, ErrorGeneric
|
||||
return nil, ErrDefault
|
||||
}
|
||||
type args struct {
|
||||
bucket string
|
||||
@@ -549,10 +527,9 @@ func TestListUsersWithAccessToBucket(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
got, _ := listUsersWithAccessToBucket(ctx, adminClient, tt.args.bucket)
|
||||
assert.Equal(got, tt.want)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
692
api/client-admin.go
Normal file
692
api/client-admin.go
Normal file
@@ -0,0 +1,692 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/pkg"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
iampolicy "github.com/minio/pkg/v3/policy"
|
||||
)
|
||||
|
||||
const globalAppName = "MinIO Console"
|
||||
|
||||
// MinioAdmin interface with all functions to be implemented
|
||||
// by mock when testing, it should include all MinioAdmin respective api calls
|
||||
// that are used within this project.
|
||||
type MinioAdmin interface {
|
||||
listUsers(ctx context.Context) (map[string]madmin.UserInfo, error)
|
||||
addUser(ctx context.Context, acessKey, SecretKey string) error
|
||||
removeUser(ctx context.Context, accessKey string) error
|
||||
getUserInfo(ctx context.Context, accessKey string) (madmin.UserInfo, error)
|
||||
setUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) error
|
||||
listGroups(ctx context.Context) ([]string, error)
|
||||
updateGroupMembers(ctx context.Context, greq madmin.GroupAddRemove) error
|
||||
getGroupDescription(ctx context.Context, group string) (*madmin.GroupDesc, error)
|
||||
setGroupStatus(ctx context.Context, group string, status madmin.GroupStatus) error
|
||||
listPolicies(ctx context.Context) (map[string]*iampolicy.Policy, error)
|
||||
getPolicy(ctx context.Context, name string) (*iampolicy.Policy, error)
|
||||
removePolicy(ctx context.Context, name string) error
|
||||
addPolicy(ctx context.Context, name string, policy *iampolicy.Policy) error
|
||||
setPolicy(ctx context.Context, policyName, entityName string, isGroup bool) error
|
||||
getConfigKV(ctx context.Context, key string) ([]byte, error)
|
||||
helpConfigKV(ctx context.Context, subSys, key string, envOnly bool) (madmin.Help, error)
|
||||
helpConfigKVGlobal(ctx context.Context, envOnly bool) (madmin.Help, error)
|
||||
setConfigKV(ctx context.Context, kv string) (restart bool, err error)
|
||||
delConfigKV(ctx context.Context, kv string) (err error)
|
||||
serviceRestart(ctx context.Context) error
|
||||
serverInfo(ctx context.Context) (madmin.InfoMessage, error)
|
||||
startProfiling(ctx context.Context, profiler madmin.ProfilerType) ([]madmin.StartProfilingResult, error)
|
||||
stopProfiling(ctx context.Context) (io.ReadCloser, error)
|
||||
serviceTrace(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo
|
||||
getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo
|
||||
AccountInfo(ctx context.Context) (madmin.AccountInfo, error)
|
||||
heal(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
|
||||
forceStart, forceStop bool) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error)
|
||||
// Service Accounts
|
||||
addServiceAccount(ctx context.Context, policy string, user string, accessKey string, secretKey string, name string, description string, expiry *time.Time, comment string) (madmin.Credentials, error)
|
||||
listServiceAccounts(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error)
|
||||
deleteServiceAccount(ctx context.Context, serviceAccount string) error
|
||||
infoServiceAccount(ctx context.Context, serviceAccount string) (madmin.InfoServiceAccountResp, error)
|
||||
updateServiceAccount(ctx context.Context, serviceAccount string, opts madmin.UpdateServiceAccountReq) error
|
||||
// Remote Buckets
|
||||
listRemoteBuckets(ctx context.Context, bucket, arnType string) (targets []madmin.BucketTarget, err error)
|
||||
getRemoteBucket(ctx context.Context, bucket, arnType string) (targets *madmin.BucketTarget, err error)
|
||||
removeRemoteBucket(ctx context.Context, bucket, arn string) error
|
||||
addRemoteBucket(ctx context.Context, bucket string, target *madmin.BucketTarget) (string, error)
|
||||
// Account password management
|
||||
changePassword(ctx context.Context, accessKey, secretKey string) error
|
||||
serverHealthInfo(ctx context.Context, deadline time.Duration) (interface{}, string, error)
|
||||
// List Tiers
|
||||
listTiers(ctx context.Context) ([]*madmin.TierConfig, error)
|
||||
// Tier Info
|
||||
tierStats(ctx context.Context) ([]madmin.TierInfo, error)
|
||||
// Add Tier
|
||||
addTier(ctx context.Context, tier *madmin.TierConfig) error
|
||||
// Edit Tier Credentials
|
||||
editTierCreds(ctx context.Context, tierName string, creds madmin.TierCreds) error
|
||||
// verify Tier status
|
||||
verifyTierStatus(ctx context.Context, tierName string) error
|
||||
// remove empty Tier
|
||||
removeTier(ctx context.Context, tierName string) error
|
||||
// Speedtest
|
||||
speedtest(ctx context.Context, opts madmin.SpeedtestOpts) (chan madmin.SpeedTestResult, error)
|
||||
// Site Relication
|
||||
getSiteReplicationInfo(ctx context.Context) (*madmin.SiteReplicationInfo, error)
|
||||
addSiteReplicationInfo(ctx context.Context, sites []madmin.PeerSite, opts madmin.SRAddOptions) (*madmin.ReplicateAddStatus, error)
|
||||
editSiteReplicationInfo(ctx context.Context, site madmin.PeerInfo, opts madmin.SREditOptions) (*madmin.ReplicateEditStatus, error)
|
||||
deleteSiteReplicationInfo(ctx context.Context, removeReq madmin.SRRemoveReq) (*madmin.ReplicateRemoveStatus, error)
|
||||
|
||||
// Replication status
|
||||
getSiteReplicationStatus(ctx context.Context, params madmin.SRStatusOptions) (*madmin.SRStatusInfo, error)
|
||||
|
||||
// KMS
|
||||
kmsStatus(ctx context.Context) (madmin.KMSStatus, error)
|
||||
kmsMetrics(ctx context.Context) (*madmin.KMSMetrics, error)
|
||||
kmsAPIs(ctx context.Context) ([]madmin.KMSAPI, error)
|
||||
kmsVersion(ctx context.Context) (*madmin.KMSVersion, error)
|
||||
createKey(ctx context.Context, key string) error
|
||||
listKeys(ctx context.Context, pattern string) ([]madmin.KMSKeyInfo, error)
|
||||
keyStatus(ctx context.Context, key string) (*madmin.KMSKeyStatus, error)
|
||||
|
||||
// IDP
|
||||
addOrUpdateIDPConfig(ctx context.Context, idpType, cfgName, cfgData string, update bool) (restart bool, err error)
|
||||
listIDPConfig(ctx context.Context, idpType string) ([]madmin.IDPListItem, error)
|
||||
deleteIDPConfig(ctx context.Context, idpType, cfgName string) (restart bool, err error)
|
||||
getIDPConfig(ctx context.Context, cfgType, cfgName string) (c madmin.IDPConfig, err error)
|
||||
|
||||
// LDAP
|
||||
getLDAPPolicyEntities(ctx context.Context, query madmin.PolicyEntitiesQuery) (madmin.PolicyEntitiesResult, error)
|
||||
}
|
||||
|
||||
// Interface implementation
|
||||
//
|
||||
// Define the structure of a minIO Client and define the functions that are actually used
|
||||
// from minIO api.
|
||||
type AdminClient struct {
|
||||
Client *madmin.AdminClient
|
||||
}
|
||||
|
||||
func (ac AdminClient) changePassword(ctx context.Context, accessKey, secretKey string) error {
|
||||
return ac.Client.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
|
||||
}
|
||||
|
||||
// implements madmin.ListUsers()
|
||||
func (ac AdminClient) listUsers(ctx context.Context) (map[string]madmin.UserInfo, error) {
|
||||
return ac.Client.ListUsers(ctx)
|
||||
}
|
||||
|
||||
// implements madmin.AddUser()
|
||||
func (ac AdminClient) addUser(ctx context.Context, accessKey, secretKey string) error {
|
||||
return ac.Client.AddUser(ctx, accessKey, secretKey)
|
||||
}
|
||||
|
||||
// implements madmin.RemoveUser()
|
||||
func (ac AdminClient) removeUser(ctx context.Context, accessKey string) error {
|
||||
return ac.Client.RemoveUser(ctx, accessKey)
|
||||
}
|
||||
|
||||
// implements madmin.GetUserInfo()
|
||||
func (ac AdminClient) getUserInfo(ctx context.Context, accessKey string) (madmin.UserInfo, error) {
|
||||
return ac.Client.GetUserInfo(ctx, accessKey)
|
||||
}
|
||||
|
||||
// implements madmin.SetUserStatus()
|
||||
func (ac AdminClient) setUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) error {
|
||||
return ac.Client.SetUserStatus(ctx, accessKey, status)
|
||||
}
|
||||
|
||||
// implements madmin.ListGroups()
|
||||
func (ac AdminClient) listGroups(ctx context.Context) ([]string, error) {
|
||||
return ac.Client.ListGroups(ctx)
|
||||
}
|
||||
|
||||
// implements madmin.UpdateGroupMembers()
|
||||
func (ac AdminClient) updateGroupMembers(ctx context.Context, greq madmin.GroupAddRemove) error {
|
||||
return ac.Client.UpdateGroupMembers(ctx, greq)
|
||||
}
|
||||
|
||||
// implements madmin.GetGroupDescription(group)
|
||||
func (ac AdminClient) getGroupDescription(ctx context.Context, group string) (*madmin.GroupDesc, error) {
|
||||
return ac.Client.GetGroupDescription(ctx, group)
|
||||
}
|
||||
|
||||
// implements madmin.SetGroupStatus(group, status)
|
||||
func (ac AdminClient) setGroupStatus(ctx context.Context, group string, status madmin.GroupStatus) error {
|
||||
return ac.Client.SetGroupStatus(ctx, group, status)
|
||||
}
|
||||
|
||||
// implements madmin.ListCannedPolicies()
|
||||
func (ac AdminClient) listPolicies(ctx context.Context) (map[string]*iampolicy.Policy, error) {
|
||||
policyMap, err := ac.Client.ListCannedPolicies(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policies := make(map[string]*iampolicy.Policy, len(policyMap))
|
||||
for k, v := range policyMap {
|
||||
p, err := iampolicy.ParseConfig(bytes.NewReader(v))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policies[k] = p
|
||||
}
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
// implements madmin.ListCannedPolicies()
|
||||
func (ac AdminClient) getPolicy(ctx context.Context, name string) (*iampolicy.Policy, error) {
|
||||
info, err := ac.Client.InfoCannedPolicyV2(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return iampolicy.ParseConfig(bytes.NewReader(info.Policy))
|
||||
}
|
||||
|
||||
// implements madmin.RemoveCannedPolicy()
|
||||
func (ac AdminClient) removePolicy(ctx context.Context, name string) error {
|
||||
return ac.Client.RemoveCannedPolicy(ctx, name)
|
||||
}
|
||||
|
||||
// implements madmin.AddCannedPolicy()
|
||||
func (ac AdminClient) addPolicy(ctx context.Context, name string, policy *iampolicy.Policy) error {
|
||||
buf, err := json.Marshal(policy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ac.Client.AddCannedPolicy(ctx, name, buf)
|
||||
}
|
||||
|
||||
// implements madmin.SetPolicy()
|
||||
func (ac AdminClient) setPolicy(ctx context.Context, policyName, entityName string, isGroup bool) error {
|
||||
// nolint:staticcheck // ignore SA1019
|
||||
return ac.Client.SetPolicy(ctx, policyName, entityName, isGroup)
|
||||
}
|
||||
|
||||
// implements madmin.GetConfigKV()
|
||||
func (ac AdminClient) getConfigKV(ctx context.Context, key string) ([]byte, error) {
|
||||
return ac.Client.GetConfigKV(ctx, key)
|
||||
}
|
||||
|
||||
// implements madmin.HelpConfigKV()
|
||||
func (ac AdminClient) helpConfigKV(ctx context.Context, subSys, key string, envOnly bool) (madmin.Help, error) {
|
||||
return ac.Client.HelpConfigKV(ctx, subSys, key, envOnly)
|
||||
}
|
||||
|
||||
// implements madmin.helpConfigKVGlobal()
|
||||
func (ac AdminClient) helpConfigKVGlobal(ctx context.Context, envOnly bool) (madmin.Help, error) {
|
||||
return ac.Client.HelpConfigKV(ctx, "", "", envOnly)
|
||||
}
|
||||
|
||||
// implements madmin.SetConfigKV()
|
||||
func (ac AdminClient) setConfigKV(ctx context.Context, kv string) (restart bool, err error) {
|
||||
return ac.Client.SetConfigKV(ctx, kv)
|
||||
}
|
||||
|
||||
// implements madmin.DelConfigKV()
|
||||
func (ac AdminClient) delConfigKV(ctx context.Context, kv string) (err error) {
|
||||
_, err = ac.Client.DelConfigKV(ctx, kv)
|
||||
return err
|
||||
}
|
||||
|
||||
// implements madmin.ServiceRestart()
|
||||
func (ac AdminClient) serviceRestart(ctx context.Context) (err error) {
|
||||
return ac.Client.ServiceRestart(ctx)
|
||||
}
|
||||
|
||||
// implements madmin.ServerInfo()
|
||||
func (ac AdminClient) serverInfo(ctx context.Context) (madmin.InfoMessage, error) {
|
||||
return ac.Client.ServerInfo(ctx)
|
||||
}
|
||||
|
||||
// implements madmin.StartProfiling()
|
||||
func (ac AdminClient) startProfiling(ctx context.Context, profiler madmin.ProfilerType) ([]madmin.StartProfilingResult, error) {
|
||||
return ac.Client.StartProfiling(ctx, profiler)
|
||||
}
|
||||
|
||||
// implements madmin.DownloadProfilingData()
|
||||
func (ac AdminClient) stopProfiling(ctx context.Context) (io.ReadCloser, error) {
|
||||
return ac.Client.DownloadProfilingData(ctx)
|
||||
}
|
||||
|
||||
// implements madmin.ServiceTrace()
|
||||
func (ac AdminClient) serviceTrace(ctx context.Context, threshold int64, _, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo {
|
||||
thresholdT := time.Duration(threshold)
|
||||
|
||||
tracingOptions := madmin.ServiceTraceOpts{
|
||||
S3: true,
|
||||
OnlyErrors: errTrace,
|
||||
Internal: internal,
|
||||
Storage: storage,
|
||||
OS: os,
|
||||
Threshold: thresholdT,
|
||||
}
|
||||
|
||||
return ac.Client.ServiceTrace(ctx, tracingOptions)
|
||||
}
|
||||
|
||||
// implements madmin.GetLogs()
|
||||
func (ac AdminClient) getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
|
||||
return ac.Client.GetLogs(ctx, node, lineCnt, logKind)
|
||||
}
|
||||
|
||||
// implements madmin.AddServiceAccount()
|
||||
func (ac AdminClient) addServiceAccount(ctx context.Context, policy string, user string, accessKey string, secretKey string, name string, description string, expiry *time.Time, comment string) (madmin.Credentials, error) {
|
||||
return ac.Client.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
|
||||
Policy: []byte(policy),
|
||||
TargetUser: user,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
Name: name,
|
||||
Description: description,
|
||||
Expiration: expiry,
|
||||
Comment: comment,
|
||||
})
|
||||
}
|
||||
|
||||
// implements madmin.ListServiceAccounts()
|
||||
func (ac AdminClient) listServiceAccounts(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error) {
|
||||
return ac.Client.ListServiceAccounts(ctx, user)
|
||||
}
|
||||
|
||||
// implements madmin.DeleteServiceAccount()
|
||||
func (ac AdminClient) deleteServiceAccount(ctx context.Context, serviceAccount string) error {
|
||||
return ac.Client.DeleteServiceAccount(ctx, serviceAccount)
|
||||
}
|
||||
|
||||
// implements madmin.InfoServiceAccount()
|
||||
func (ac AdminClient) infoServiceAccount(ctx context.Context, serviceAccount string) (madmin.InfoServiceAccountResp, error) {
|
||||
return ac.Client.InfoServiceAccount(ctx, serviceAccount)
|
||||
}
|
||||
|
||||
// implements madmin.UpdateServiceAccount()
|
||||
func (ac AdminClient) updateServiceAccount(ctx context.Context, serviceAccount string, opts madmin.UpdateServiceAccountReq) error {
|
||||
return ac.Client.UpdateServiceAccount(ctx, serviceAccount, opts)
|
||||
}
|
||||
|
||||
// AccountInfo implements madmin.AccountInfo()
|
||||
func (ac AdminClient) AccountInfo(ctx context.Context) (madmin.AccountInfo, error) {
|
||||
return ac.Client.AccountInfo(ctx, madmin.AccountOpts{})
|
||||
}
|
||||
|
||||
func (ac AdminClient) heal(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
|
||||
forceStart, forceStop bool,
|
||||
) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error) {
|
||||
return ac.Client.Heal(ctx, bucket, prefix, healOpts, clientToken, forceStart, forceStop)
|
||||
}
|
||||
|
||||
// listRemoteBuckets - return a list of remote buckets
|
||||
func (ac AdminClient) listRemoteBuckets(ctx context.Context, bucket, arnType string) (targets []madmin.BucketTarget, err error) {
|
||||
return ac.Client.ListRemoteTargets(ctx, bucket, arnType)
|
||||
}
|
||||
|
||||
// getRemoteBucket - gets remote bucked based on a given bucket name
|
||||
func (ac AdminClient) getRemoteBucket(ctx context.Context, bucket, arnType string) (*madmin.BucketTarget, error) {
|
||||
targets, err := ac.Client.ListRemoteTargets(ctx, bucket, arnType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(targets) > 0 {
|
||||
return &targets[0], nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// removeRemoteBucket removes a remote target associated with particular ARN for this bucket
|
||||
func (ac AdminClient) removeRemoteBucket(ctx context.Context, bucket, arn string) error {
|
||||
return ac.Client.RemoveRemoteTarget(ctx, bucket, arn)
|
||||
}
|
||||
|
||||
// addRemoteBucket sets up a remote target for this bucket
|
||||
func (ac AdminClient) addRemoteBucket(ctx context.Context, bucket string, target *madmin.BucketTarget) (string, error) {
|
||||
return ac.Client.SetRemoteTarget(ctx, bucket, target)
|
||||
}
|
||||
|
||||
func (ac AdminClient) setBucketQuota(ctx context.Context, bucket string, quota *madmin.BucketQuota) error {
|
||||
return ac.Client.SetBucketQuota(ctx, bucket, quota)
|
||||
}
|
||||
|
||||
func (ac AdminClient) getBucketQuota(ctx context.Context, bucket string) (madmin.BucketQuota, error) {
|
||||
return ac.Client.GetBucketQuota(ctx, bucket)
|
||||
}
|
||||
|
||||
// serverHealthInfo implements mc.ServerHealthInfo - Connect to a minio server and call Health Info Management API
|
||||
func (ac AdminClient) serverHealthInfo(ctx context.Context, deadline time.Duration) (interface{}, string, error) {
|
||||
info := madmin.HealthInfo{}
|
||||
var healthInfo interface{}
|
||||
var version string
|
||||
var tryCount int
|
||||
for info.Version == "" && tryCount < 10 {
|
||||
var resp *http.Response
|
||||
var err error
|
||||
resp, version, err = ac.Client.ServerHealthInfo(ctx, madmin.HealthDataTypesList, deadline, "")
|
||||
if err != nil {
|
||||
return nil, version, err
|
||||
}
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
for {
|
||||
if err = decoder.Decode(&info); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
tryCount++
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
if info.Version == "" {
|
||||
return nil, "", ErrHealthReportFail
|
||||
}
|
||||
healthInfo = info
|
||||
|
||||
return healthInfo, version, nil
|
||||
}
|
||||
|
||||
// implements madmin.listTiers()
|
||||
func (ac AdminClient) listTiers(ctx context.Context) ([]*madmin.TierConfig, error) {
|
||||
return ac.Client.ListTiers(ctx)
|
||||
}
|
||||
|
||||
// implements madmin.tierStats()
|
||||
func (ac AdminClient) tierStats(ctx context.Context) ([]madmin.TierInfo, error) {
|
||||
return ac.Client.TierStats(ctx)
|
||||
}
|
||||
|
||||
// implements madmin.AddTier()
|
||||
func (ac AdminClient) addTier(ctx context.Context, cfg *madmin.TierConfig) error {
|
||||
return ac.Client.AddTier(ctx, cfg)
|
||||
}
|
||||
|
||||
// implements madmin.Inspect()
|
||||
func (ac AdminClient) inspect(ctx context.Context, insOpts madmin.InspectOptions) ([]byte, io.ReadCloser, error) {
|
||||
return ac.Client.Inspect(ctx, insOpts)
|
||||
}
|
||||
|
||||
// implements madmin.EditTier()
|
||||
func (ac AdminClient) editTierCreds(ctx context.Context, tierName string, creds madmin.TierCreds) error {
|
||||
return ac.Client.EditTier(ctx, tierName, creds)
|
||||
}
|
||||
|
||||
// implements madmin.VerifyTier()
|
||||
func (ac AdminClient) verifyTierStatus(ctx context.Context, tierName string) error {
|
||||
return ac.Client.VerifyTier(ctx, tierName)
|
||||
}
|
||||
|
||||
// implements madmin.RemoveTier()
|
||||
func (ac AdminClient) removeTier(ctx context.Context, tierName string) error {
|
||||
return ac.Client.RemoveTier(ctx, tierName)
|
||||
}
|
||||
|
||||
func NewMinioAdminClient(ctx context.Context, sessionClaims *models.Principal) (*madmin.AdminClient, error) {
|
||||
clientIP := utils.ClientIPFromContext(ctx)
|
||||
adminClient, err := newAdminFromClaims(sessionClaims, clientIP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adminClient.SetAppInfo(globalAppName, pkg.Version)
|
||||
return adminClient, nil
|
||||
}
|
||||
|
||||
// newAdminFromClaims creates a minio admin from Decrypted claims using Assume role credentials
|
||||
func newAdminFromClaims(claims *models.Principal, clientIP string) (*madmin.AdminClient, error) {
|
||||
tlsEnabled := getMinIOEndpointIsSecure()
|
||||
endpoint := getMinIOEndpoint()
|
||||
|
||||
adminClient, err := madmin.NewWithOptions(endpoint, &madmin.Options{
|
||||
Creds: credentials.NewStaticV4(claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken),
|
||||
Secure: tlsEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adminClient.SetAppInfo(globalAppName, pkg.Version)
|
||||
adminClient.SetCustomTransport(PrepareSTSClientTransport(clientIP))
|
||||
return adminClient, nil
|
||||
}
|
||||
|
||||
// newAdminFromCreds Creates a minio client using custom credentials for connecting to a remote host
|
||||
func newAdminFromCreds(accessKey, secretKey, endpoint string, tlsEnabled bool) (*madmin.AdminClient, error) {
|
||||
minioClient, err := madmin.NewWithOptions(endpoint, &madmin.Options{
|
||||
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
|
||||
Secure: tlsEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
minioClient.SetAppInfo(globalAppName, pkg.Version)
|
||||
return minioClient, nil
|
||||
}
|
||||
|
||||
// isLocalAddress returns true if the url contains an IPv4/IPv6 hostname
|
||||
// that points to the local machine - FQDN are not supported
|
||||
func isLocalIPEndpoint(endpoint string) bool {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return isLocalIPAddress(u.Hostname())
|
||||
}
|
||||
|
||||
// isLocalAddress returns true if the url contains an IPv4/IPv6 hostname
|
||||
// that points to the local machine - FQDN are not supported
|
||||
func isLocalIPAddress(ipAddr string) bool {
|
||||
if ipAddr == "" {
|
||||
return false
|
||||
}
|
||||
if ipAddr == "localhost" {
|
||||
return true
|
||||
}
|
||||
ip := net.ParseIP(ipAddr)
|
||||
return ip != nil && ip.IsLoopback()
|
||||
}
|
||||
|
||||
// GetConsoleHTTPClient caches different http clients depending on the target endpoint while taking
|
||||
// in consideration CA certs stored in ${HOME}/.console/certs/CAs and ${HOME}/.minio/certs/CAs
|
||||
// If the target endpoint points to a loopback device, skip the TLS verification.
|
||||
func GetConsoleHTTPClient(clientIP string) *http.Client {
|
||||
return PrepareConsoleHTTPClient(clientIP)
|
||||
}
|
||||
|
||||
var (
|
||||
// De-facto standard header keys.
|
||||
xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
|
||||
xRealIP = http.CanonicalHeaderKey("X-Real-IP")
|
||||
)
|
||||
|
||||
var (
|
||||
// RFC7239 defines a new "Forwarded: " header designed to replace the
|
||||
// existing use of X-Forwarded-* headers.
|
||||
// e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43
|
||||
forwarded = http.CanonicalHeaderKey("Forwarded")
|
||||
// Allows for a sub-match of the first value after 'for=' to the next
|
||||
// comma, semi-colon or space. The match is case-insensitive.
|
||||
forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|,| )]+)(.*)`)
|
||||
)
|
||||
|
||||
// getSourceIPFromHeaders retrieves the IP from the X-Forwarded-For, X-Real-IP
|
||||
// and RFC7239 Forwarded headers (in that order)
|
||||
func getSourceIPFromHeaders(r *http.Request) string {
|
||||
var addr string
|
||||
|
||||
if fwd := r.Header.Get(xForwardedFor); fwd != "" {
|
||||
// Only grab the first (client) address. Note that '192.168.0.1,
|
||||
// 10.1.1.1' is a valid key for X-Forwarded-For where addresses after
|
||||
// the first may represent forwarding proxies earlier in the chain.
|
||||
s := strings.Index(fwd, ", ")
|
||||
if s == -1 {
|
||||
s = len(fwd)
|
||||
}
|
||||
addr = fwd[:s]
|
||||
} else if fwd := r.Header.Get(xRealIP); fwd != "" {
|
||||
// X-Real-IP should only contain one IP address (the client making the
|
||||
// request).
|
||||
addr = fwd
|
||||
} else if fwd := r.Header.Get(forwarded); fwd != "" {
|
||||
// match should contain at least two elements if the protocol was
|
||||
// specified in the Forwarded header. The first element will always be
|
||||
// the 'for=' capture, which we ignore. In the case of multiple IP
|
||||
// addresses (for=8.8.8.8, 8.8.4.4, 172.16.1.20 is valid) we only
|
||||
// extract the first, which should be the client IP.
|
||||
if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 {
|
||||
// IPv6 addresses in Forwarded headers are quoted-strings. We strip
|
||||
// these quotes.
|
||||
addr = strings.Trim(match[1], `"`)
|
||||
}
|
||||
}
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
// getClientIP retrieves the IP from the request headers
|
||||
// and falls back to r.RemoteAddr when necessary.
|
||||
// however returns without bracketing.
|
||||
func getClientIP(r *http.Request) string {
|
||||
addr := getSourceIPFromHeaders(r)
|
||||
if addr == "" {
|
||||
addr = r.RemoteAddr
|
||||
}
|
||||
|
||||
// Default to remote address if headers not set.
|
||||
raddr, _, _ := net.SplitHostPort(addr)
|
||||
if raddr == "" {
|
||||
return addr
|
||||
}
|
||||
return raddr
|
||||
}
|
||||
|
||||
func (ac AdminClient) speedtest(ctx context.Context, opts madmin.SpeedtestOpts) (chan madmin.SpeedTestResult, error) {
|
||||
return ac.Client.Speedtest(ctx, opts)
|
||||
}
|
||||
|
||||
// Site Replication
|
||||
func (ac AdminClient) getSiteReplicationInfo(ctx context.Context) (*madmin.SiteReplicationInfo, error) {
|
||||
res, err := ac.Client.SiteReplicationInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &madmin.SiteReplicationInfo{
|
||||
Enabled: res.Enabled,
|
||||
Name: res.Name,
|
||||
Sites: res.Sites,
|
||||
ServiceAccountAccessKey: res.ServiceAccountAccessKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ac AdminClient) addSiteReplicationInfo(ctx context.Context, sites []madmin.PeerSite, opts madmin.SRAddOptions) (*madmin.ReplicateAddStatus, error) {
|
||||
res, err := ac.Client.SiteReplicationAdd(ctx, sites, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &madmin.ReplicateAddStatus{
|
||||
Success: res.Success,
|
||||
Status: res.Status,
|
||||
ErrDetail: res.ErrDetail,
|
||||
InitialSyncErrorMessage: res.InitialSyncErrorMessage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ac AdminClient) editSiteReplicationInfo(ctx context.Context, site madmin.PeerInfo, opts madmin.SREditOptions) (*madmin.ReplicateEditStatus, error) {
|
||||
res, err := ac.Client.SiteReplicationEdit(ctx, site, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &madmin.ReplicateEditStatus{
|
||||
Success: res.Success,
|
||||
Status: res.Status,
|
||||
ErrDetail: res.ErrDetail,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ac AdminClient) deleteSiteReplicationInfo(ctx context.Context, removeReq madmin.SRRemoveReq) (*madmin.ReplicateRemoveStatus, error) {
|
||||
res, err := ac.Client.SiteReplicationRemove(ctx, removeReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &madmin.ReplicateRemoveStatus{
|
||||
Status: res.Status,
|
||||
ErrDetail: res.ErrDetail,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ac AdminClient) getSiteReplicationStatus(ctx context.Context, params madmin.SRStatusOptions) (*madmin.SRStatusInfo, error) {
|
||||
res, err := ac.Client.SRStatusInfo(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (ac AdminClient) kmsStatus(ctx context.Context) (madmin.KMSStatus, error) {
|
||||
return ac.Client.KMSStatus(ctx)
|
||||
}
|
||||
|
||||
func (ac AdminClient) kmsMetrics(ctx context.Context) (*madmin.KMSMetrics, error) {
|
||||
return ac.Client.KMSMetrics(ctx)
|
||||
}
|
||||
|
||||
func (ac AdminClient) kmsAPIs(ctx context.Context) ([]madmin.KMSAPI, error) {
|
||||
return ac.Client.KMSAPIs(ctx)
|
||||
}
|
||||
|
||||
func (ac AdminClient) kmsVersion(ctx context.Context) (*madmin.KMSVersion, error) {
|
||||
return ac.Client.KMSVersion(ctx)
|
||||
}
|
||||
|
||||
func (ac AdminClient) createKey(ctx context.Context, key string) error {
|
||||
return ac.Client.CreateKey(ctx, key)
|
||||
}
|
||||
|
||||
func (ac AdminClient) listKeys(ctx context.Context, pattern string) ([]madmin.KMSKeyInfo, error) {
|
||||
return ac.Client.ListKeys(ctx, pattern)
|
||||
}
|
||||
|
||||
func (ac AdminClient) keyStatus(ctx context.Context, key string) (*madmin.KMSKeyStatus, error) {
|
||||
return ac.Client.GetKeyStatus(ctx, key)
|
||||
}
|
||||
|
||||
func (ac AdminClient) addOrUpdateIDPConfig(ctx context.Context, idpType, cfgName, cfgData string, update bool) (restart bool, err error) {
|
||||
return ac.Client.AddOrUpdateIDPConfig(ctx, idpType, cfgName, cfgData, update)
|
||||
}
|
||||
|
||||
func (ac AdminClient) listIDPConfig(ctx context.Context, idpType string) ([]madmin.IDPListItem, error) {
|
||||
return ac.Client.ListIDPConfig(ctx, idpType)
|
||||
}
|
||||
|
||||
func (ac AdminClient) deleteIDPConfig(ctx context.Context, idpType, cfgName string) (restart bool, err error) {
|
||||
return ac.Client.DeleteIDPConfig(ctx, idpType, cfgName)
|
||||
}
|
||||
|
||||
func (ac AdminClient) getIDPConfig(ctx context.Context, idpType, cfgName string) (c madmin.IDPConfig, err error) {
|
||||
return ac.Client.GetIDPConfig(ctx, idpType, cfgName)
|
||||
}
|
||||
|
||||
func (ac AdminClient) getLDAPPolicyEntities(ctx context.Context, query madmin.PolicyEntitiesQuery) (madmin.PolicyEntitiesResult, error) {
|
||||
return ac.Client.GetLDAPPolicyEntities(ctx, query)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// This file is part of MinIO Orchestrator
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -14,21 +14,20 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package restapi
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/v7/pkg/replication"
|
||||
"github.com/minio/minio-go/v7/pkg/sse"
|
||||
|
||||
"errors"
|
||||
xnet "github.com/minio/pkg/v3/net"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/pkg"
|
||||
@@ -229,11 +228,11 @@ type MCClient interface {
|
||||
addNotificationConfig(ctx context.Context, arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error
|
||||
removeNotificationConfig(ctx context.Context, arn string, event string, prefix string, suffix string) *probe.Error
|
||||
watch(ctx context.Context, options mc.WatchOptions) (*mc.WatchObject, *probe.Error)
|
||||
remove(ctx context.Context, isIncomplete, isRemoveBucket, isBypass bool, contentCh <-chan *mc.ClientContent) <-chan *probe.Error
|
||||
remove(ctx context.Context, isIncomplete, isRemoveBucket, isBypass, forceDelete bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult
|
||||
list(ctx context.Context, opts mc.ListOptions) <-chan *mc.ClientContent
|
||||
get(ctx context.Context, opts mc.GetOptions) (io.ReadCloser, *probe.Error)
|
||||
shareDownload(ctx context.Context, versionID string, expires time.Duration) (string, *probe.Error)
|
||||
setVersioning(ctx context.Context, status string) *probe.Error
|
||||
setVersioning(ctx context.Context, status string, excludePrefix []string, excludeFolders bool) *probe.Error
|
||||
}
|
||||
|
||||
// Interface implementation
|
||||
@@ -262,12 +261,16 @@ func (c mcClient) setReplication(ctx context.Context, cfg *replication.Config, o
|
||||
return c.client.SetReplication(ctx, cfg, opts)
|
||||
}
|
||||
|
||||
func (c mcClient) setVersioning(ctx context.Context, status string) *probe.Error {
|
||||
return c.client.SetVersion(ctx, status)
|
||||
func (c mcClient) deleteAllReplicationRules(ctx context.Context) *probe.Error {
|
||||
return c.client.RemoveReplication(ctx)
|
||||
}
|
||||
|
||||
func (c mcClient) remove(ctx context.Context, isIncomplete, isRemoveBucket, isBypass bool, contentCh <-chan *mc.ClientContent) <-chan *probe.Error {
|
||||
return c.client.Remove(ctx, isIncomplete, isRemoveBucket, isBypass, contentCh)
|
||||
func (c mcClient) setVersioning(ctx context.Context, status string, excludePrefix []string, excludeFolders bool) *probe.Error {
|
||||
return c.client.SetVersion(ctx, status, excludePrefix, excludeFolders)
|
||||
}
|
||||
|
||||
func (c mcClient) remove(ctx context.Context, isIncomplete, isRemoveBucket, isBypass, forceDelete bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult {
|
||||
return c.client.Remove(ctx, isIncomplete, isRemoveBucket, isBypass, forceDelete, contentCh)
|
||||
}
|
||||
|
||||
func (c mcClient) list(ctx context.Context, opts mc.ListOptions) <-chan *mc.ClientContent {
|
||||
@@ -275,7 +278,8 @@ func (c mcClient) list(ctx context.Context, opts mc.ListOptions) <-chan *mc.Clie
|
||||
}
|
||||
|
||||
func (c mcClient) get(ctx context.Context, opts mc.GetOptions) (io.ReadCloser, *probe.Error) {
|
||||
return c.client.Get(ctx, opts)
|
||||
rd, _, err := c.client.Get(ctx, opts)
|
||||
return rd, err
|
||||
}
|
||||
|
||||
func (c mcClient) shareDownload(ctx context.Context, versionID string, expires time.Duration) (string, *probe.Error) {
|
||||
@@ -326,37 +330,68 @@ func (s consoleSTSAssumeRole) IsExpired() bool {
|
||||
return s.stsAssumeRole.IsExpired()
|
||||
}
|
||||
|
||||
func NewConsoleCredentials(accessKey, secretKey, location string) (*credentials.Credentials, error) {
|
||||
func stsCredentials(minioURL, accessKey, secretKey, location, clientIP string) (*credentials.Credentials, error) {
|
||||
if accessKey == "" || secretKey == "" {
|
||||
return nil, errors.New("credentials endpoint, access and secret key are mandatory for AssumeRoleSTS")
|
||||
}
|
||||
opts := credentials.STSAssumeRoleOptions{
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
Location: location,
|
||||
DurationSeconds: int(xjwt.GetConsoleSTSDuration().Seconds()),
|
||||
}
|
||||
stsAssumeRole := &credentials.STSAssumeRole{
|
||||
Client: GetConsoleHTTPClient(clientIP),
|
||||
STSEndpoint: minioURL,
|
||||
Options: opts,
|
||||
}
|
||||
consoleSTSWrapper := consoleSTSAssumeRole{stsAssumeRole: stsAssumeRole}
|
||||
return credentials.New(consoleSTSWrapper), nil
|
||||
}
|
||||
|
||||
func NewConsoleCredentials(accessKey, secretKey, location, clientIP string) (*credentials.Credentials, error) {
|
||||
minioURL := getMinIOServer()
|
||||
|
||||
// Future authentication methods can be added under this switch statement
|
||||
switch {
|
||||
// LDAP authentication for Console
|
||||
case ldap.GetLDAPEnabled():
|
||||
{
|
||||
creds, err := auth.GetCredentialsFromLDAP(GetConsoleHTTPClient(), getMinIOServer(), accessKey, secretKey)
|
||||
creds, err := auth.GetCredentialsFromLDAP(GetConsoleHTTPClient(clientIP), minioURL, accessKey, secretKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We verify if LDAP credentials are correct and no error is returned
|
||||
_, err = creds.Get()
|
||||
|
||||
if err != nil && strings.Contains(strings.ToLower(err.Error()), "not found") {
|
||||
// We try to use STS Credentials in case LDAP credentials are incorrect.
|
||||
stsCreds, errSTS := stsCredentials(minioURL, accessKey, secretKey, location, clientIP)
|
||||
|
||||
// If there is an error with STS too, then we return the original LDAP error
|
||||
if errSTS != nil {
|
||||
LogError("error in STS credentials for LDAP case: %v ", errSTS)
|
||||
|
||||
// We return LDAP result
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
_, err := stsCreds.Get()
|
||||
// There is an error with STS credentials, We return the result of LDAP as STS is not a priority in this case.
|
||||
if err != nil {
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
return stsCreds, nil
|
||||
}
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
// default authentication for Console is via STS (Security Token Service) against MinIO
|
||||
default:
|
||||
{
|
||||
if accessKey == "" || secretKey == "" {
|
||||
return nil, errors.New("credentials endpoint, access and secret key are mandatory for AssumeRoleSTS")
|
||||
}
|
||||
opts := credentials.STSAssumeRoleOptions{
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
Location: location,
|
||||
DurationSeconds: int(xjwt.GetConsoleSTSDuration().Seconds()),
|
||||
}
|
||||
stsAssumeRole := &credentials.STSAssumeRole{
|
||||
Client: GetConsoleHTTPClient(),
|
||||
STSEndpoint: getMinIOServer(),
|
||||
Options: opts,
|
||||
}
|
||||
consoleSTSWrapper := consoleSTSAssumeRole{stsAssumeRole: stsAssumeRole}
|
||||
return credentials.New(consoleSTSWrapper), nil
|
||||
return stsCredentials(minioURL, accessKey, secretKey, location, clientIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -364,45 +399,49 @@ func NewConsoleCredentials(accessKey, secretKey, location string) (*credentials.
|
||||
// getConsoleCredentialsFromSession returns the *consoleCredentials.Login associated to the
|
||||
// provided session token, this is useful for running the Expire() or IsExpired() operations
|
||||
func getConsoleCredentialsFromSession(claims *models.Principal) *credentials.Credentials {
|
||||
if claims == nil {
|
||||
return credentials.NewStaticV4("", "", "")
|
||||
}
|
||||
return credentials.NewStaticV4(claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken)
|
||||
}
|
||||
|
||||
// newMinioClient creates a new MinIO client based on the ConsoleCredentials extracted
|
||||
// from the provided session token
|
||||
func newMinioClient(claims *models.Principal) (*minio.Client, error) {
|
||||
func newMinioClient(claims *models.Principal, clientIP string) (*minio.Client, error) {
|
||||
creds := getConsoleCredentialsFromSession(claims)
|
||||
minioClient, err := minio.New(getMinIOEndpoint(), &minio.Options{
|
||||
endpoint := getMinIOEndpoint()
|
||||
secure := getMinIOEndpointIsSecure()
|
||||
minioClient, err := minio.New(endpoint, &minio.Options{
|
||||
Creds: creds,
|
||||
Secure: getMinIOEndpointIsSecure(),
|
||||
Transport: GetConsoleHTTPClient().Transport,
|
||||
Secure: secure,
|
||||
Transport: GetConsoleHTTPClient(clientIP).Transport,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// set user-agent to differentiate Console UI requests for auditing.
|
||||
minioClient.SetAppInfo("MinIO Console", pkg.Version)
|
||||
return minioClient, nil
|
||||
}
|
||||
|
||||
// computeObjectURLWithoutEncode returns a MinIO url containing the object filename without encoding
|
||||
func computeObjectURLWithoutEncode(bucketName, prefix string) (string, error) {
|
||||
endpoint := getMinIOServer()
|
||||
u, err := url.Parse(endpoint)
|
||||
u, err := xnet.ParseHTTPURL(getMinIOServer())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("the provided endpoint is invalid")
|
||||
return "", fmt.Errorf("the provided endpoint: '%s' is invalid", getMinIOServer())
|
||||
}
|
||||
objectURL := fmt.Sprintf("%s:%s", u.Hostname(), u.Port())
|
||||
var p string
|
||||
if strings.TrimSpace(bucketName) != "" {
|
||||
objectURL = path.Join(objectURL, bucketName)
|
||||
p = path.Join(p, bucketName)
|
||||
}
|
||||
if strings.TrimSpace(prefix) != "" {
|
||||
objectURL = pathJoinFinalSlash(objectURL, prefix)
|
||||
p = pathJoinFinalSlash(p, prefix)
|
||||
}
|
||||
|
||||
objectURL = fmt.Sprintf("%s://%s", u.Scheme, objectURL)
|
||||
return objectURL, nil
|
||||
return u.String() + "/" + p, nil
|
||||
}
|
||||
|
||||
// newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket
|
||||
func newS3BucketClient(claims *models.Principal, bucketName string, prefix string) (*mc.S3Client, error) {
|
||||
func newS3BucketClient(claims *models.Principal, bucketName string, prefix string, clientIP string) (*mc.S3Client, error) {
|
||||
if claims == nil {
|
||||
return nil, fmt.Errorf("the provided credentials are invalid")
|
||||
}
|
||||
@@ -411,7 +450,7 @@ func newS3BucketClient(claims *models.Principal, bucketName string, prefix strin
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the provided endpoint is invalid")
|
||||
}
|
||||
s3Config := newS3Config(objectURL, claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken, false)
|
||||
s3Config := newS3Config(objectURL, claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken, clientIP)
|
||||
client, pErr := mc.S3New(s3Config)
|
||||
if pErr != nil {
|
||||
return nil, pErr.Cause
|
||||
@@ -433,24 +472,24 @@ func pathJoinFinalSlash(elem ...string) string {
|
||||
return path.Join(elem...)
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
// newS3Config simply creates a new Config struct using the passed
|
||||
// parameters.
|
||||
func newS3Config(endpoint, accessKey, secretKey, sessionToken string, insecure bool) *mc.Config {
|
||||
func newS3Config(endpoint, accessKey, secretKey, sessionToken string, clientIP string) *mc.Config {
|
||||
// We have a valid alias and hostConfig. We populate the/
|
||||
// consoleCredentials from the match found in the config file.
|
||||
s3Config := new(mc.Config)
|
||||
|
||||
s3Config.AppName = globalAppName
|
||||
s3Config.AppVersion = pkg.Version
|
||||
s3Config.Debug = false
|
||||
s3Config.Insecure = insecure
|
||||
|
||||
s3Config.HostURL = endpoint
|
||||
s3Config.AccessKey = accessKey
|
||||
s3Config.SecretKey = secretKey
|
||||
s3Config.SessionToken = sessionToken
|
||||
s3Config.Signature = "S3v4"
|
||||
s3Config.Transport = prepareSTSClientTransport(insecure)
|
||||
|
||||
return s3Config
|
||||
return &mc.Config{
|
||||
HostURL: endpoint,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
SessionToken: sessionToken,
|
||||
Signature: "S3v4",
|
||||
AppName: globalAppName,
|
||||
AppVersion: pkg.Version,
|
||||
Insecure: isLocalIPEndpoint(endpoint),
|
||||
Transport: &ConsoleTransport{
|
||||
ClientIP: clientIP,
|
||||
Transport: GlobalTransport,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// This file is part of MinIO Orchestrator
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2024 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package restapi
|
||||
package api
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -76,14 +76,15 @@ func Test_computeObjectURLWithoutEncode(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
got, err := computeObjectURLWithoutEncode(tt.args.bucketName, tt.args.prefix)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("computeObjectURLWithoutEncode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
t.Errorf("computeObjectURLWithoutEncode() errors = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("computeObjectURLWithoutEncode() got = %v, want %v", got, tt.want)
|
||||
if err == nil {
|
||||
if got != tt.want {
|
||||
t.Errorf("computeObjectURLWithoutEncode() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
309
api/config.go
Normal file
309
api/config.go
Normal file
@@ -0,0 +1,309 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/pkg/auth/idp/oauth2"
|
||||
xcerts "github.com/minio/pkg/v3/certs"
|
||||
"github.com/minio/pkg/v3/env"
|
||||
xnet "github.com/minio/pkg/v3/net"
|
||||
)
|
||||
|
||||
var (
|
||||
// Port console default port
|
||||
Port = "9090"
|
||||
|
||||
// Hostname console hostname
|
||||
// avoid listening on 0.0.0.0 by default
|
||||
// instead listen on all IPv4 and IPv6
|
||||
// - Hostname should be empty.
|
||||
Hostname = ""
|
||||
|
||||
// TLSPort console tls port
|
||||
TLSPort = "9443"
|
||||
|
||||
// TLSRedirect console tls redirect rule
|
||||
TLSRedirect = "on"
|
||||
|
||||
ConsoleResourceName = "console-ui"
|
||||
)
|
||||
|
||||
var (
|
||||
// GlobalRootCAs is CA root certificates, a nil value means system certs pool will be used
|
||||
GlobalRootCAs *x509.CertPool
|
||||
// GlobalPublicCerts has certificates Console will use to serve clients
|
||||
GlobalPublicCerts []*x509.Certificate
|
||||
// GlobalTLSCertsManager custom TLS Manager for SNI support
|
||||
GlobalTLSCertsManager *xcerts.Manager
|
||||
// GlobalTransport is common transport used for all HTTP calls, this is set via
|
||||
// MinIO server to be the correct transport, however we still define some defaults
|
||||
// here just in case.
|
||||
GlobalTransport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
KeepAlive: 15 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 1024,
|
||||
MaxIdleConnsPerHost: 1024,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 10 * time.Second,
|
||||
DisableCompression: true, // Set to avoid auto-decompression
|
||||
TLSClientConfig: &tls.Config{
|
||||
// Can't use SSLv3 because of POODLE and BEAST
|
||||
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
|
||||
// Can't use TLSv1.1 because of RC4 cipher usage
|
||||
MinVersion: tls.VersionTLS12,
|
||||
// Console runs in the same pod/node as MinIO this is acceptable.
|
||||
InsecureSkipVerify: true,
|
||||
RootCAs: GlobalRootCAs,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// MinIOConfig represents application configuration passed in from the MinIO
|
||||
// server to the console.
|
||||
type MinIOConfig struct {
|
||||
OpenIDProviders oauth2.OpenIDPCfg
|
||||
}
|
||||
|
||||
// GlobalMinIOConfig is the global application configuration passed in from the
|
||||
// MinIO server.
|
||||
var GlobalMinIOConfig MinIOConfig
|
||||
|
||||
func getMinIOServer() string {
|
||||
return strings.TrimSpace(env.Get(ConsoleMinIOServer, "http://localhost:9000"))
|
||||
}
|
||||
|
||||
func getSubnetProxy() string {
|
||||
return strings.TrimSpace(env.Get(ConsoleSubnetProxy, ""))
|
||||
}
|
||||
|
||||
func GetMinIORegion() string {
|
||||
return strings.TrimSpace(env.Get(ConsoleMinIORegion, ""))
|
||||
}
|
||||
|
||||
func getMinIOEndpoint() string {
|
||||
u, err := xnet.ParseHTTPURL(getMinIOServer())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u.Host
|
||||
}
|
||||
|
||||
func getMinIOEndpointIsSecure() bool {
|
||||
u, err := xnet.ParseHTTPURL(getMinIOServer())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u.Scheme == "https"
|
||||
}
|
||||
|
||||
// GetHostname gets console hostname set on env variable,
|
||||
// default one or defined on run command
|
||||
func GetHostname() string {
|
||||
return strings.ToLower(env.Get(ConsoleHostname, Hostname))
|
||||
}
|
||||
|
||||
// GetPort gets console por set on env variable
|
||||
// or default one
|
||||
func GetPort() int {
|
||||
port, err := strconv.Atoi(env.Get(ConsolePort, Port))
|
||||
if err != nil {
|
||||
port = 9090
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
// GetTLSPort gets console tls port set on env variable
|
||||
// or default one
|
||||
func GetTLSPort() int {
|
||||
port, err := strconv.Atoi(env.Get(ConsoleTLSPort, TLSPort))
|
||||
if err != nil {
|
||||
port = 9443
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
// If GetTLSRedirect is set to true, then only allow HTTPS requests. Default is true.
|
||||
func GetTLSRedirect() string {
|
||||
return strings.ToLower(env.Get(ConsoleSecureTLSRedirect, TLSRedirect))
|
||||
}
|
||||
|
||||
// Get secure middleware env variable configurations
|
||||
func GetSecureAllowedHosts() []string {
|
||||
allowedHosts := env.Get(ConsoleSecureAllowedHosts, "")
|
||||
if allowedHosts != "" {
|
||||
return strings.Split(allowedHosts, ",")
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// AllowedHostsAreRegex determines, if the provided AllowedHosts slice contains valid regular expressions. Default is false.
|
||||
func GetSecureAllowedHostsAreRegex() bool {
|
||||
return strings.ToLower(env.Get(ConsoleSecureAllowedHostsAreRegex, "off")) == "on"
|
||||
}
|
||||
|
||||
// If FrameDeny is set to true, adds the X-Frame-Options header with the value of `DENY`. Default is true.
|
||||
func GetSecureFrameDeny() bool {
|
||||
return strings.ToLower(env.Get(ConsoleSecureFrameDeny, "on")) == "on"
|
||||
}
|
||||
|
||||
// If ContentTypeNosniff is true, adds the X-Content-Type-Options header with the value `nosniff`. Default is true.
|
||||
func GetSecureContentTypeNonSniff() bool {
|
||||
return strings.ToLower(env.Get(ConsoleSecureContentTypeNoSniff, "on")) == "on"
|
||||
}
|
||||
|
||||
// If BrowserXssFilter is true, adds the X-XSS-Protection header with the value `1; mode=block`. Default is true.
|
||||
func GetSecureBrowserXSSFilter() bool {
|
||||
return strings.ToLower(env.Get(ConsoleSecureBrowserXSSFilter, "on")) == "on"
|
||||
}
|
||||
|
||||
// ContentSecurityPolicy allows the Content-Security-Policy header value to be set with a custom value. Default is "".
|
||||
// Passing a template string will replace `$NONCE` with a dynamic nonce value of 16 bytes for each request which can be
|
||||
// later retrieved using the Nonce function.
|
||||
func GetSecureContentSecurityPolicy() string {
|
||||
return env.Get(ConsoleSecureContentSecurityPolicy, "")
|
||||
}
|
||||
|
||||
// ContentSecurityPolicyReportOnly allows the Content-Security-Policy-Report-Only header value to be set with a custom value. Default is "".
|
||||
func GetSecureContentSecurityPolicyReportOnly() string {
|
||||
return env.Get(ConsoleSecureContentSecurityPolicyReportOnly, "")
|
||||
}
|
||||
|
||||
// HostsProxyHeaders is a set of header keys that may hold a proxied hostname value for the request.
|
||||
func GetSecureHostsProxyHeaders() []string {
|
||||
allowedHosts := env.Get(ConsoleSecureHostsProxyHeaders, "")
|
||||
if allowedHosts != "" {
|
||||
return strings.Split(allowedHosts, ",")
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// TLSHost is the host name that is used to redirect HTTP requests to HTTPS. Default is "", which indicates to use the same host.
|
||||
func GetSecureTLSHost() string {
|
||||
tlsHost := env.Get(ConsoleSecureTLSHost, "")
|
||||
if tlsHost == "" && Hostname != "" {
|
||||
return net.JoinHostPort(Hostname, TLSPort)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// STSSeconds is the max-age of the Strict-Transport-Security header. Default is 0, which would NOT include the header.
|
||||
func GetSecureSTSSeconds() int64 {
|
||||
seconds, err := strconv.Atoi(env.Get(ConsoleSecureSTSSeconds, "0"))
|
||||
if err != nil {
|
||||
seconds = 0
|
||||
}
|
||||
return int64(seconds)
|
||||
}
|
||||
|
||||
// If STSIncludeSubdomains is set to true, the `includeSubdomains` will be appended to the Strict-Transport-Security header. Default is false.
|
||||
func GetSecureSTSIncludeSubdomains() bool {
|
||||
return strings.ToLower(env.Get(ConsoleSecureSTSIncludeSubdomains, "off")) == "on"
|
||||
}
|
||||
|
||||
// If STSPreload is set to true, the `preload` flag will be appended to the Strict-Transport-Security header. Default is false.
|
||||
func GetSecureSTSPreload() bool {
|
||||
return strings.ToLower(env.Get(ConsoleSecureSTSPreload, "off")) == "on"
|
||||
}
|
||||
|
||||
// If TLSTemporaryRedirect is true, the a 302 will be used while redirecting. Default is false (301).
|
||||
func GetSecureTLSTemporaryRedirect() bool {
|
||||
return strings.ToLower(env.Get(ConsoleSecureTLSTemporaryRedirect, "off")) == "on"
|
||||
}
|
||||
|
||||
// STS header is only included when the connection is HTTPS.
|
||||
func GetSecureForceSTSHeader() bool {
|
||||
return strings.ToLower(env.Get(ConsoleSecureForceSTSHeader, "off")) == "on"
|
||||
}
|
||||
|
||||
// ReferrerPolicy allows the Referrer-Policy header with the value to be set with a custom value. Default is "".
|
||||
func GetSecureReferrerPolicy() string {
|
||||
return env.Get(ConsoleSecureReferrerPolicy, "")
|
||||
}
|
||||
|
||||
// FeaturePolicy allows the Feature-Policy header with the value to be set with a custom value. Default is "".
|
||||
func GetSecureFeaturePolicy() string {
|
||||
return env.Get(ConsoleSecureFeaturePolicy, "")
|
||||
}
|
||||
|
||||
func getLogSearchAPIToken() string {
|
||||
if v := env.Get(ConsoleLogQueryAuthToken, ""); v != "" {
|
||||
return v
|
||||
}
|
||||
return env.Get(LogSearchQueryAuthToken, "")
|
||||
}
|
||||
|
||||
func getLogSearchURL() string {
|
||||
return env.Get(ConsoleLogQueryURL, "")
|
||||
}
|
||||
|
||||
func getPrometheusURL() string {
|
||||
return env.Get(PrometheusURL, "")
|
||||
}
|
||||
|
||||
func getPrometheusAuthToken() string {
|
||||
return env.Get(PrometheusAuthToken, "")
|
||||
}
|
||||
|
||||
func getPrometheusJobID() string {
|
||||
return env.Get(PrometheusJobID, "minio-job")
|
||||
}
|
||||
|
||||
func getPrometheusExtraLabels() string {
|
||||
return env.Get(PrometheusExtraLabels, "")
|
||||
}
|
||||
|
||||
func getMaxConcurrentUploadsLimit() int64 {
|
||||
cu, err := strconv.ParseInt(env.Get(ConsoleMaxConcurrentUploads, "10"), 10, 64)
|
||||
if err != nil {
|
||||
return 10
|
||||
}
|
||||
|
||||
return cu
|
||||
}
|
||||
|
||||
func getMaxConcurrentDownloadsLimit() int64 {
|
||||
cu, err := strconv.ParseInt(env.Get(ConsoleMaxConcurrentDownloads, "20"), 10, 64)
|
||||
if err != nil {
|
||||
return 20
|
||||
}
|
||||
|
||||
return cu
|
||||
}
|
||||
|
||||
func getConsoleDevMode() bool {
|
||||
return strings.ToLower(env.Get(ConsoleDevMode, "off")) == "on"
|
||||
}
|
||||
|
||||
func getConsoleAnimatedLogin() bool {
|
||||
return strings.ToLower(env.Get(ConsoleAnimatedLogin, "on")) == "on"
|
||||
}
|
||||
|
||||
func getConsoleBrowserRedirectURL() string {
|
||||
return env.Get(ConsoleBrowserRedirectURL, "")
|
||||
}
|
||||
393
api/config_test.go
Normal file
393
api/config_test.go
Normal file
@@ -0,0 +1,393 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetHostname(t *testing.T) {
|
||||
os.Setenv(ConsoleHostname, "x")
|
||||
defer os.Unsetenv(ConsoleHostname)
|
||||
assert.Equalf(t, "x", GetHostname(), "GetHostname()")
|
||||
}
|
||||
|
||||
func TestGetPort(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "valid port",
|
||||
args: args{
|
||||
env: "9091",
|
||||
},
|
||||
want: 9091,
|
||||
},
|
||||
{
|
||||
name: "invalid port",
|
||||
args: args{
|
||||
env: "duck",
|
||||
},
|
||||
want: 9090,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsolePort, tt.args.env)
|
||||
assert.Equalf(t, tt.want, GetPort(), "GetPort()")
|
||||
os.Unsetenv(ConsolePort)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTLSPort(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "valid port",
|
||||
args: args{
|
||||
env: "9444",
|
||||
},
|
||||
want: 9444,
|
||||
},
|
||||
{
|
||||
name: "invalid port",
|
||||
args: args{
|
||||
env: "duck",
|
||||
},
|
||||
want: 9443,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleTLSPort, tt.args.env)
|
||||
assert.Equalf(t, tt.want, GetTLSPort(), "GetTLSPort()")
|
||||
os.Unsetenv(ConsoleTLSPort)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecureAllowedHosts(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "valid hosts",
|
||||
args: args{
|
||||
env: "host1,host2",
|
||||
},
|
||||
want: []string{"host1", "host2"},
|
||||
},
|
||||
{
|
||||
name: "empty hosts",
|
||||
args: args{
|
||||
env: "",
|
||||
},
|
||||
want: []string{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleSecureAllowedHosts, tt.args.env)
|
||||
assert.Equalf(t, tt.want, GetSecureAllowedHosts(), "GetSecureAllowedHosts()")
|
||||
os.Unsetenv(ConsoleSecureAllowedHosts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecureHostsProxyHeaders(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "valid headers",
|
||||
args: args{
|
||||
env: "header1,header2",
|
||||
},
|
||||
want: []string{"header1", "header2"},
|
||||
},
|
||||
{
|
||||
name: "empty headers",
|
||||
args: args{
|
||||
env: "",
|
||||
},
|
||||
want: []string{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleSecureHostsProxyHeaders, tt.args.env)
|
||||
assert.Equalf(t, tt.want, GetSecureHostsProxyHeaders(), "GetSecureHostsProxyHeaders()")
|
||||
os.Unsetenv(ConsoleSecureHostsProxyHeaders)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecureSTSSeconds(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int64
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
env: "1",
|
||||
},
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
args: args{
|
||||
env: "duck",
|
||||
},
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleSecureSTSSeconds, tt.args.env)
|
||||
assert.Equalf(t, tt.want, GetSecureSTSSeconds(), "GetSecureSTSSeconds()")
|
||||
os.Unsetenv(ConsoleSecureSTSSeconds)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getLogSearchAPIToken(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "env set",
|
||||
args: args{
|
||||
env: "value",
|
||||
},
|
||||
want: "value",
|
||||
},
|
||||
{
|
||||
name: "env not set",
|
||||
args: args{
|
||||
env: "",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleLogQueryAuthToken, tt.args.env)
|
||||
assert.Equalf(t, tt.want, getLogSearchAPIToken(), "getLogSearchAPIToken()")
|
||||
os.Setenv(ConsoleLogQueryAuthToken, tt.args.env)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getPrometheusURL(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "env set",
|
||||
args: args{
|
||||
env: "value",
|
||||
},
|
||||
want: "value",
|
||||
},
|
||||
{
|
||||
name: "env not set",
|
||||
args: args{
|
||||
env: "",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(PrometheusURL, tt.args.env)
|
||||
assert.Equalf(t, tt.want, getPrometheusURL(), "getPrometheusURL()")
|
||||
os.Setenv(PrometheusURL, tt.args.env)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getPrometheusJobID(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "env set",
|
||||
args: args{
|
||||
env: "value",
|
||||
},
|
||||
want: "value",
|
||||
},
|
||||
{
|
||||
name: "env not set",
|
||||
args: args{
|
||||
env: "",
|
||||
},
|
||||
want: "minio-job",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(PrometheusJobID, tt.args.env)
|
||||
assert.Equalf(t, tt.want, getPrometheusJobID(), "getPrometheusJobID()")
|
||||
os.Setenv(PrometheusJobID, tt.args.env)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getMaxConcurrentUploadsLimit(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int64
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
env: "1",
|
||||
},
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
args: args{
|
||||
env: "duck",
|
||||
},
|
||||
want: 10,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleMaxConcurrentUploads, tt.args.env)
|
||||
assert.Equalf(t, tt.want, getMaxConcurrentUploadsLimit(), "getMaxConcurrentUploadsLimit()")
|
||||
os.Unsetenv(ConsoleMaxConcurrentUploads)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getMaxConcurrentDownloadsLimit(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int64
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
env: "1",
|
||||
},
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
args: args{
|
||||
env: "duck",
|
||||
},
|
||||
want: 20,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleMaxConcurrentDownloads, tt.args.env)
|
||||
assert.Equalf(t, tt.want, getMaxConcurrentDownloadsLimit(), "getMaxConcurrentDownloadsLimit()")
|
||||
os.Unsetenv(ConsoleMaxConcurrentDownloads)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getConsoleDevMode(t *testing.T) {
|
||||
type args struct {
|
||||
env string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "value set",
|
||||
args: args{
|
||||
env: "on",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "value not set",
|
||||
args: args{
|
||||
env: "",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
os.Setenv(ConsoleDevMode, tt.args.env)
|
||||
assert.Equalf(t, tt.want, getConsoleDevMode(), "getConsoleDevMode()")
|
||||
os.Unsetenv(ConsoleDevMode)
|
||||
})
|
||||
}
|
||||
}
|
||||
525
api/configure_console.go
Normal file
525
api/configure_console.go
Normal file
@@ -0,0 +1,525 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// This file is safe to edit. Once it exists it will not be overwritten
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/minio/console/pkg/logger"
|
||||
"github.com/minio/console/pkg/utils"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
|
||||
"github.com/klauspost/compress/gzhttp"
|
||||
|
||||
portal_ui "github.com/minio/console/web-app"
|
||||
"github.com/minio/pkg/v3/env"
|
||||
"github.com/minio/pkg/v3/mimedb"
|
||||
xnet "github.com/minio/pkg/v3/net"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/minio/console/api/operations"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/pkg/auth"
|
||||
"github.com/unrolled/secure"
|
||||
)
|
||||
|
||||
//go:generate swagger generate server --target ../../console --name Console --spec ../swagger.yml
|
||||
|
||||
var additionalServerFlags = struct {
|
||||
CertsDir string `long:"certs-dir" description:"path to certs directory" env:"CONSOLE_CERTS_DIR"`
|
||||
}{}
|
||||
|
||||
const (
|
||||
SubPath = "CONSOLE_SUBPATH"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgSubPath = "/"
|
||||
subPathOnce sync.Once
|
||||
)
|
||||
|
||||
func configureFlags(api *operations.ConsoleAPI) {
|
||||
api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{
|
||||
{
|
||||
ShortDescription: "additional server flags",
|
||||
Options: &additionalServerFlags,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func configureAPI(api *operations.ConsoleAPI) http.Handler {
|
||||
// Applies when the "x-token" header is set
|
||||
api.KeyAuth = func(token string, _ []string) (*models.Principal, error) {
|
||||
// we are validating the session token by decrypting the claims inside, if the operation succeed that means the jwt
|
||||
// was generated and signed by us in the first place
|
||||
if token == "Anonymous" {
|
||||
return &models.Principal{}, nil
|
||||
}
|
||||
claims, err := auth.ParseClaimsFromToken(token)
|
||||
if err != nil {
|
||||
api.Logger("Unable to validate the session token %s: %v", token, err)
|
||||
return nil, errors.New(401, "incorrect api key auth")
|
||||
}
|
||||
return &models.Principal{
|
||||
STSAccessKeyID: claims.STSAccessKeyID,
|
||||
STSSecretAccessKey: claims.STSSecretAccessKey,
|
||||
STSSessionToken: claims.STSSessionToken,
|
||||
AccountAccessKey: claims.AccountAccessKey,
|
||||
Hm: claims.HideMenu,
|
||||
Ob: claims.ObjectBrowser,
|
||||
CustomStyleOb: claims.CustomStyleOB,
|
||||
}, nil
|
||||
}
|
||||
api.AnonymousAuth = func(_ string) (*models.Principal, error) {
|
||||
return &models.Principal{}, nil
|
||||
}
|
||||
|
||||
// Register login handlers
|
||||
registerLoginHandlers(api)
|
||||
// Register logout handlers
|
||||
registerLogoutHandlers(api)
|
||||
// Register bucket handlers
|
||||
registerBucketsHandlers(api)
|
||||
// Register all users handlers
|
||||
registerUsersHandlers(api)
|
||||
// Register groups handlers
|
||||
registerGroupsHandlers(api)
|
||||
// Register policies handlers
|
||||
registersPoliciesHandler(api)
|
||||
// Register configurations handlers
|
||||
registerConfigHandlers(api)
|
||||
// Register bucket events handlers
|
||||
registerBucketEventsHandlers(api)
|
||||
// Register bucket lifecycle handlers
|
||||
registerBucketsLifecycleHandlers(api)
|
||||
// Register service handlers
|
||||
registerServiceHandlers(api)
|
||||
// Register session handlers
|
||||
registerSessionHandlers(api)
|
||||
// Register admin info handlers
|
||||
registerAdminInfoHandlers(api)
|
||||
// Register admin arns handlers
|
||||
registerAdminArnsHandlers(api)
|
||||
// Register admin notification endpoints handlers
|
||||
registerAdminNotificationEndpointsHandlers(api)
|
||||
// Register admin Service Account Handlers
|
||||
registerServiceAccountsHandlers(api)
|
||||
// Register admin remote buckets
|
||||
registerAdminBucketRemoteHandlers(api)
|
||||
// Register admin log search
|
||||
registerLogSearchHandlers(api)
|
||||
// Register admin subnet handlers
|
||||
registerSubnetHandlers(api)
|
||||
// Register admin KMS handlers
|
||||
registerKMSHandlers(api)
|
||||
// Register admin IDP handlers
|
||||
registerIDPHandlers(api)
|
||||
// Register Account handlers
|
||||
registerAdminTiersHandlers(api)
|
||||
// Register Inspect Handler
|
||||
registerInspectHandler(api)
|
||||
// Register nodes handlers
|
||||
registerNodesHandler(api)
|
||||
|
||||
registerSiteReplicationHandler(api)
|
||||
registerSiteReplicationStatusHandler(api)
|
||||
// Register Support Handler
|
||||
registerSupportHandlers(api)
|
||||
|
||||
// Operator Console
|
||||
|
||||
// Register Object's Handlers
|
||||
registerObjectsHandlers(api)
|
||||
// Register Bucket Quota's Handlers
|
||||
registerBucketQuotaHandlers(api)
|
||||
// Register Account handlers
|
||||
registerAccountHandlers(api)
|
||||
|
||||
registerReleasesHandlers(api)
|
||||
|
||||
registerPublicObjectsHandlers(api)
|
||||
|
||||
api.PreServerShutdown = func() {}
|
||||
|
||||
api.ServerShutdown = func() {}
|
||||
|
||||
// do an initial subnet plan caching
|
||||
fetchLicensePlan()
|
||||
|
||||
return setupGlobalMiddleware(api.Serve(setupMiddlewares))
|
||||
}
|
||||
|
||||
// The TLS configuration before HTTPS server starts.
|
||||
func configureTLS(tlsConfig *tls.Config) {
|
||||
tlsConfig.RootCAs = GlobalRootCAs
|
||||
tlsConfig.GetCertificate = GlobalTLSCertsManager.GetCertificate
|
||||
}
|
||||
|
||||
// The middleware configuration is for the handler executors. These do not apply to the swagger.json document.
|
||||
// The middleware executes after routing but before authentication, binding and validation
|
||||
func setupMiddlewares(handler http.Handler) http.Handler {
|
||||
return handler
|
||||
}
|
||||
|
||||
func ContextMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestID := uuid.NewString()
|
||||
ctx := context.WithValue(r.Context(), utils.ContextRequestID, requestID)
|
||||
ctx = context.WithValue(ctx, utils.ContextRequestUserAgent, r.UserAgent())
|
||||
ctx = context.WithValue(ctx, utils.ContextRequestHost, r.Host)
|
||||
ctx = context.WithValue(ctx, utils.ContextRequestRemoteAddr, r.RemoteAddr)
|
||||
ctx = context.WithValue(ctx, utils.ContextClientIP, getClientIP(r))
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func AuditLogMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rw := logger.NewResponseWriter(w)
|
||||
next.ServeHTTP(rw, r)
|
||||
if strings.HasPrefix(r.URL.Path, "/ws") || strings.HasPrefix(r.URL.Path, "/api") {
|
||||
logger.AuditLog(r.Context(), rw, r, map[string]interface{}{}, "Authorization", "Cookie", "Set-Cookie")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document.
|
||||
// So this is a good place to plug in a panic handling middleware, logger and metrics
|
||||
func setupGlobalMiddleware(handler http.Handler) http.Handler {
|
||||
gnext := gzhttp.GzipHandler(handler)
|
||||
// if audit-log is enabled console will log all incoming request
|
||||
next := AuditLogMiddleware(gnext)
|
||||
// serve static files
|
||||
next = FileServerMiddleware(next)
|
||||
// add information to request context
|
||||
next = ContextMiddleware(next)
|
||||
// handle cookie or authorization header for session
|
||||
next = AuthenticationMiddleware(next)
|
||||
|
||||
sslHostFn := secure.SSLHostFunc(func(host string) string {
|
||||
xhost, err := xnet.ParseHost(host)
|
||||
if err != nil {
|
||||
return host
|
||||
}
|
||||
return net.JoinHostPort(xhost.Name, TLSPort)
|
||||
})
|
||||
|
||||
// Secure middleware, this middleware wrap all the previous handlers and add
|
||||
// HTTP security headers
|
||||
secureOptions := secure.Options{
|
||||
AllowedHosts: GetSecureAllowedHosts(),
|
||||
AllowedHostsAreRegex: GetSecureAllowedHostsAreRegex(),
|
||||
HostsProxyHeaders: GetSecureHostsProxyHeaders(),
|
||||
SSLRedirect: GetTLSRedirect() == "on" && len(GlobalPublicCerts) > 0,
|
||||
SSLHostFunc: &sslHostFn,
|
||||
SSLHost: GetSecureTLSHost(),
|
||||
STSSeconds: GetSecureSTSSeconds(),
|
||||
STSIncludeSubdomains: GetSecureSTSIncludeSubdomains(),
|
||||
STSPreload: GetSecureSTSPreload(),
|
||||
SSLTemporaryRedirect: false,
|
||||
ForceSTSHeader: GetSecureForceSTSHeader(),
|
||||
FrameDeny: GetSecureFrameDeny(),
|
||||
ContentTypeNosniff: GetSecureContentTypeNonSniff(),
|
||||
BrowserXssFilter: GetSecureBrowserXSSFilter(),
|
||||
ContentSecurityPolicy: GetSecureContentSecurityPolicy(),
|
||||
ContentSecurityPolicyReportOnly: GetSecureContentSecurityPolicyReportOnly(),
|
||||
ReferrerPolicy: GetSecureReferrerPolicy(),
|
||||
FeaturePolicy: GetSecureFeaturePolicy(),
|
||||
IsDevelopment: false,
|
||||
}
|
||||
secureMiddleware := secure.New(secureOptions)
|
||||
next = secureMiddleware.Handler(next)
|
||||
return RejectS3Middleware(next)
|
||||
}
|
||||
|
||||
const apiRequestErr = `<?xml version="1.0" encoding="UTF-8"?><Error><Code>InvalidArgument</Code><Message>S3 API Requests must be made to API port.</Message><RequestId>0</RequestId></Error>`
|
||||
|
||||
// RejectS3Middleware will reject requests that have AWS S3 specific headers.
|
||||
func RejectS3Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if len(r.Header.Get("X-Amz-Content-Sha256")) > 0 ||
|
||||
len(r.Header.Get("X-Amz-Date")) > 0 ||
|
||||
strings.HasPrefix(r.Header.Get("Authorization"), "AWS4-HMAC-SHA256") ||
|
||||
r.URL.Query().Get("AWSAccessKeyId") != "" {
|
||||
|
||||
w.Header().Set("Location", getMinIOServer())
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(apiRequestErr))
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func AuthenticationMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token, err := auth.GetTokenFromRequest(r)
|
||||
if err != nil && err != auth.ErrNoAuthToken {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
sessionToken, _ := auth.DecryptToken(token)
|
||||
// All handlers handle appropriately to return errors
|
||||
// based on their swagger rules, we do not need to
|
||||
// additionally return error here, let the next ServeHTTPs
|
||||
// handle it appropriately.
|
||||
if len(sessionToken) > 0 {
|
||||
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", string(sessionToken)))
|
||||
} else {
|
||||
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "Anonymous"))
|
||||
}
|
||||
ctx := r.Context()
|
||||
claims, _ := auth.ParseClaimsFromToken(string(sessionToken))
|
||||
if claims != nil {
|
||||
// save user session id context
|
||||
ctx = context.WithValue(r.Context(), utils.ContextRequestUserID, claims.STSSessionToken)
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// FileServerMiddleware serves files from the static folder
|
||||
func FileServerMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Server", globalAppName) // do not add version information
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/ws"):
|
||||
serveWS(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api"):
|
||||
next.ServeHTTP(w, r)
|
||||
default:
|
||||
buildFs, err := fs.Sub(portal_ui.GetStaticAssets(), "build")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
wrapHandlerSinglePageApplication(requestBounce(http.FileServer(http.FS(buildFs)))).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type notFoundRedirectRespWr struct {
|
||||
http.ResponseWriter // We embed http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (w *notFoundRedirectRespWr) WriteHeader(status int) {
|
||||
w.status = status // Store the status for our own use
|
||||
if status != http.StatusNotFound {
|
||||
w.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *notFoundRedirectRespWr) Write(p []byte) (int, error) {
|
||||
if w.status != http.StatusNotFound {
|
||||
return w.ResponseWriter.Write(p)
|
||||
}
|
||||
return len(p), nil // Lie that we successfully wrote it
|
||||
}
|
||||
|
||||
// handleSPA handles the serving of the React Single Page Application
|
||||
func handleSPA(w http.ResponseWriter, r *http.Request) {
|
||||
basePath := "/"
|
||||
// For SPA mode we will replace root base with a sub path if configured unless we received cp=y and cpb=/NEW/BASE
|
||||
if v := r.URL.Query().Get("cp"); v == "y" {
|
||||
if base := r.URL.Query().Get("cpb"); base != "" {
|
||||
// make sure the subpath has a trailing slash
|
||||
if !strings.HasSuffix(base, "/") {
|
||||
base = fmt.Sprintf("%s/", base)
|
||||
}
|
||||
basePath = base
|
||||
}
|
||||
}
|
||||
|
||||
indexPage, err := portal_ui.GetStaticAssets().Open("build/index.html")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sts := r.URL.Query().Get("sts")
|
||||
stsAccessKey := r.URL.Query().Get("sts_a")
|
||||
stsSecretKey := r.URL.Query().Get("sts_s")
|
||||
overridenStyles := r.URL.Query().Get("ov_st")
|
||||
|
||||
// if these three parameters are present we are being asked to issue a session with these values
|
||||
if sts != "" && stsAccessKey != "" && stsSecretKey != "" {
|
||||
creds := credentials.NewStaticV4(stsAccessKey, stsSecretKey, sts)
|
||||
consoleCreds := &ConsoleCredentials{
|
||||
ConsoleCredentials: creds,
|
||||
AccountAccessKey: stsAccessKey,
|
||||
}
|
||||
sf := &auth.SessionFeatures{}
|
||||
sf.HideMenu = true
|
||||
sf.ObjectBrowser = true
|
||||
|
||||
if overridenStyles != "" {
|
||||
err := ValidateEncodedStyles(overridenStyles)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sf.CustomStyleOB = overridenStyles
|
||||
}
|
||||
|
||||
sessionID, err := login(consoleCreds, sf)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cookie := NewSessionCookieForConsole(*sessionID)
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
// Allow us to be iframed
|
||||
w.Header().Del("X-Frame-Options")
|
||||
}
|
||||
|
||||
indexPageBytes, err := io.ReadAll(indexPage)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// if we have a seeded basePath. This should override CONSOLE_SUBPATH every time, thus the `if else`
|
||||
if basePath != "/" {
|
||||
indexPageBytes = replaceBaseInIndex(indexPageBytes, basePath)
|
||||
// if we have a custom subpath replace it in
|
||||
} else if getSubPath() != "/" {
|
||||
indexPageBytes = replaceBaseInIndex(indexPageBytes, getSubPath())
|
||||
}
|
||||
indexPageBytes = replaceLicense(indexPageBytes)
|
||||
|
||||
mimeType := mimedb.TypeByExtension(filepath.Ext(r.URL.Path))
|
||||
|
||||
if mimeType == "application/octet-stream" {
|
||||
mimeType = "text/html"
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", mimeType)
|
||||
http.ServeContent(w, r, "index.html", time.Now(), bytes.NewReader(indexPageBytes))
|
||||
}
|
||||
|
||||
// wrapHandlerSinglePageApplication handles a http.FileServer returning a 404 and overrides it with index.html
|
||||
func wrapHandlerSinglePageApplication(h http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
handleSPA(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", mimedb.TypeByExtension(filepath.Ext(r.URL.Path)))
|
||||
nfw := ¬FoundRedirectRespWr{ResponseWriter: w}
|
||||
h.ServeHTTP(nfw, r)
|
||||
if nfw.status == http.StatusNotFound {
|
||||
handleSPA(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type nullWriter struct{}
|
||||
|
||||
func (lw nullWriter) Write(b []byte) (int, error) {
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// As soon as server is initialized but not run yet, this function will be called.
|
||||
// If you need to modify a config, store server instance to stop it individually later, this is the place.
|
||||
// This function can be called multiple times, depending on the number of serving schemes.
|
||||
// scheme value will be set accordingly: "http", "https" or "unix"
|
||||
func configureServer(s *http.Server, _, _ string) {
|
||||
// Turn-off random logger by Go net/http
|
||||
s.ErrorLog = log.New(&nullWriter{}, "", 0)
|
||||
}
|
||||
|
||||
func getSubPath() string {
|
||||
subPathOnce.Do(func() {
|
||||
cfgSubPath = parseSubPath(env.Get(SubPath, ""))
|
||||
})
|
||||
return cfgSubPath
|
||||
}
|
||||
|
||||
func parseSubPath(v string) string {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return SlashSeparator
|
||||
}
|
||||
// Replace all unnecessary `\` to `/`
|
||||
// also add pro-actively at the end.
|
||||
subPath := path.Clean(filepath.ToSlash(v))
|
||||
if !strings.HasPrefix(subPath, SlashSeparator) {
|
||||
subPath = SlashSeparator + subPath
|
||||
}
|
||||
if !strings.HasSuffix(subPath, SlashSeparator) {
|
||||
subPath += SlashSeparator
|
||||
}
|
||||
return subPath
|
||||
}
|
||||
|
||||
func replaceBaseInIndex(indexPageBytes []byte, basePath string) []byte {
|
||||
if basePath != "" {
|
||||
validBasePath := regexp.MustCompile(`^[0-9a-zA-Z\/-]+$`)
|
||||
if !validBasePath.MatchString(basePath) {
|
||||
return indexPageBytes
|
||||
}
|
||||
indexPageStr := string(indexPageBytes)
|
||||
newBase := fmt.Sprintf("<base href=\"%s\"/>", basePath)
|
||||
indexPageStr = strings.Replace(indexPageStr, "<base href=\"/\"/>", newBase, 1)
|
||||
indexPageBytes = []byte(indexPageStr)
|
||||
|
||||
}
|
||||
return indexPageBytes
|
||||
}
|
||||
|
||||
func replaceLicense(indexPageBytes []byte) []byte {
|
||||
indexPageStr := string(indexPageBytes)
|
||||
newPlan := fmt.Sprintf("<meta name=\"minio-license\" content=\"%s\" />", InstanceLicensePlan.String())
|
||||
indexPageStr = strings.Replace(indexPageStr, "<meta name=\"minio-license\" content=\"agpl\"/>", newPlan, 1)
|
||||
indexPageBytes = []byte(indexPageStr)
|
||||
return indexPageBytes
|
||||
}
|
||||
|
||||
func requestBounce(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
125
api/configure_console_test.go
Normal file
125
api/configure_console_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_parseSubPath(t *testing.T) {
|
||||
type args struct {
|
||||
v string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
args: args{
|
||||
v: "",
|
||||
},
|
||||
want: "/",
|
||||
},
|
||||
{
|
||||
name: "Slash",
|
||||
args: args{
|
||||
v: "/",
|
||||
},
|
||||
want: "/",
|
||||
},
|
||||
{
|
||||
name: "Double Slash",
|
||||
args: args{
|
||||
v: "//",
|
||||
},
|
||||
want: "/",
|
||||
},
|
||||
{
|
||||
name: "No slashes",
|
||||
args: args{
|
||||
v: "route",
|
||||
},
|
||||
want: "/route/",
|
||||
},
|
||||
{
|
||||
name: "No trailing slashes",
|
||||
args: args{
|
||||
v: "/route",
|
||||
},
|
||||
want: "/route/",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
assert.Equalf(t, tt.want, parseSubPath(tt.args.v), "parseSubPath(%v)", tt.args.v)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getSubPath(t *testing.T) {
|
||||
type args struct {
|
||||
envValue string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
args: args{
|
||||
envValue: "",
|
||||
},
|
||||
want: "/",
|
||||
},
|
||||
{
|
||||
name: "Slash",
|
||||
args: args{
|
||||
envValue: "/",
|
||||
},
|
||||
want: "/",
|
||||
},
|
||||
{
|
||||
name: "Valid Value",
|
||||
args: args{
|
||||
envValue: "/subpath/",
|
||||
},
|
||||
want: "/subpath/",
|
||||
},
|
||||
{
|
||||
name: "No starting slash",
|
||||
args: args{
|
||||
envValue: "subpath/",
|
||||
},
|
||||
want: "/subpath/",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
t.Setenv(SubPath, tt.args.envValue)
|
||||
defer os.Unsetenv(SubPath)
|
||||
subPathOnce = sync.Once{}
|
||||
assert.Equalf(t, tt.want, getSubPath(), "getSubPath()")
|
||||
})
|
||||
}
|
||||
}
|
||||
63
api/consts.go
Normal file
63
api/consts.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
// list of all console environment constants
|
||||
const (
|
||||
// Constants for common configuration
|
||||
ConsoleMinIOServer = "CONSOLE_MINIO_SERVER"
|
||||
ConsoleSubnetProxy = "CONSOLE_SUBNET_PROXY"
|
||||
ConsoleMinIORegion = "CONSOLE_MINIO_REGION"
|
||||
ConsoleHostname = "CONSOLE_HOSTNAME"
|
||||
ConsolePort = "CONSOLE_PORT"
|
||||
ConsoleTLSPort = "CONSOLE_TLS_PORT"
|
||||
|
||||
// Constants for Secure middleware
|
||||
ConsoleSecureAllowedHosts = "CONSOLE_SECURE_ALLOWED_HOSTS"
|
||||
ConsoleSecureAllowedHostsAreRegex = "CONSOLE_SECURE_ALLOWED_HOSTS_ARE_REGEX"
|
||||
ConsoleSecureFrameDeny = "CONSOLE_SECURE_FRAME_DENY"
|
||||
ConsoleSecureContentTypeNoSniff = "CONSOLE_SECURE_CONTENT_TYPE_NO_SNIFF"
|
||||
ConsoleSecureBrowserXSSFilter = "CONSOLE_SECURE_BROWSER_XSS_FILTER"
|
||||
ConsoleSecureContentSecurityPolicy = "CONSOLE_SECURE_CONTENT_SECURITY_POLICY"
|
||||
ConsoleSecureContentSecurityPolicyReportOnly = "CONSOLE_SECURE_CONTENT_SECURITY_POLICY_REPORT_ONLY"
|
||||
ConsoleSecureHostsProxyHeaders = "CONSOLE_SECURE_HOSTS_PROXY_HEADERS"
|
||||
ConsoleSecureSTSSeconds = "CONSOLE_SECURE_STS_SECONDS"
|
||||
ConsoleSecureSTSIncludeSubdomains = "CONSOLE_SECURE_STS_INCLUDE_SUB_DOMAINS"
|
||||
ConsoleSecureSTSPreload = "CONSOLE_SECURE_STS_PRELOAD"
|
||||
ConsoleSecureTLSRedirect = "CONSOLE_SECURE_TLS_REDIRECT"
|
||||
ConsoleSecureTLSHost = "CONSOLE_SECURE_TLS_HOST"
|
||||
ConsoleSecureTLSTemporaryRedirect = "CONSOLE_SECURE_TLS_TEMPORARY_REDIRECT"
|
||||
ConsoleSecureForceSTSHeader = "CONSOLE_SECURE_FORCE_STS_HEADER"
|
||||
ConsoleSecurePublicKey = "CONSOLE_SECURE_PUBLIC_KEY"
|
||||
ConsoleSecureReferrerPolicy = "CONSOLE_SECURE_REFERRER_POLICY"
|
||||
ConsoleSecureFeaturePolicy = "CONSOLE_SECURE_FEATURE_POLICY"
|
||||
ConsoleSecureExpectCTHeader = "CONSOLE_SECURE_EXPECT_CT_HEADER"
|
||||
PrometheusURL = "CONSOLE_PROMETHEUS_URL"
|
||||
PrometheusAuthToken = "CONSOLE_PROMETHEUS_AUTH_TOKEN"
|
||||
PrometheusJobID = "CONSOLE_PROMETHEUS_JOB_ID"
|
||||
PrometheusExtraLabels = "CONSOLE_PROMETHEUS_EXTRA_LABELS"
|
||||
ConsoleLogQueryURL = "CONSOLE_LOG_QUERY_URL"
|
||||
ConsoleLogQueryAuthToken = "CONSOLE_LOG_QUERY_AUTH_TOKEN"
|
||||
ConsoleMaxConcurrentUploads = "CONSOLE_MAX_CONCURRENT_UPLOADS"
|
||||
ConsoleMaxConcurrentDownloads = "CONSOLE_MAX_CONCURRENT_DOWNLOADS"
|
||||
ConsoleDevMode = "CONSOLE_DEV_MODE"
|
||||
ConsoleAnimatedLogin = "CONSOLE_ANIMATED_LOGIN"
|
||||
ConsoleBrowserRedirectURL = "CONSOLE_BROWSER_REDIRECT_URL"
|
||||
LogSearchQueryAuthToken = "LOGSEARCH_QUERY_AUTH_TOKEN"
|
||||
SlashSeparator = "/"
|
||||
LocalAddress = "127.0.0.1"
|
||||
)
|
||||
556
api/custom-server.go
Normal file
556
api/custom-server.go
Normal file
@@ -0,0 +1,556 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/runtime/flagext"
|
||||
"github.com/go-openapi/swag"
|
||||
flags "github.com/jessevdk/go-flags"
|
||||
"golang.org/x/net/netutil"
|
||||
|
||||
"github.com/minio/console/api/operations"
|
||||
)
|
||||
|
||||
const (
|
||||
schemeHTTP = "http"
|
||||
schemeHTTPS = "https"
|
||||
schemeUnix = "unix"
|
||||
)
|
||||
|
||||
var defaultSchemes []string
|
||||
|
||||
func init() {
|
||||
defaultSchemes = []string{
|
||||
schemeHTTP,
|
||||
}
|
||||
}
|
||||
|
||||
// NewServer creates a new api console server but does not configure it
|
||||
func NewServer(api *operations.ConsoleAPI) *Server {
|
||||
s := new(Server)
|
||||
|
||||
s.shutdown = make(chan struct{})
|
||||
s.api = api
|
||||
s.interrupt = make(chan os.Signal, 1)
|
||||
return s
|
||||
}
|
||||
|
||||
// ConfigureAPI configures the API and handlers.
|
||||
func (s *Server) ConfigureAPI() {
|
||||
if s.api != nil {
|
||||
s.handler = configureAPI(s.api)
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigureFlags configures the additional flags defined by the handlers. Needs to be called before the parser.Parse
|
||||
func (s *Server) ConfigureFlags() {
|
||||
if s.api != nil {
|
||||
configureFlags(s.api)
|
||||
}
|
||||
}
|
||||
|
||||
// Server for the console API
|
||||
type Server struct {
|
||||
EnabledListeners []string `long:"scheme" description:"the listeners to enable, this can be repeated and defaults to the schemes in the swagger spec"`
|
||||
CleanupTimeout time.Duration `long:"cleanup-timeout" description:"grace period for which to wait before killing idle connections" default:"10s"`
|
||||
GracefulTimeout time.Duration `long:"graceful-timeout" description:"grace period for which to wait before shutting down the server" default:"15s"`
|
||||
MaxHeaderSize flagext.ByteSize `long:"max-header-size" description:"controls the maximum number of bytes the server will read parsing the request header's keys and values, including the request line. It does not limit the size of the request body." default:"1MiB"`
|
||||
|
||||
SocketPath flags.Filename `long:"socket-path" description:"the unix socket to listen on" default:"/var/run/console.sock"`
|
||||
domainSocketL net.Listener
|
||||
|
||||
Host string `long:"host" description:"the IP to listen on" default:"localhost" env:"HOST"`
|
||||
Port int `long:"port" description:"the port to listen on for insecure connections, defaults to a random value" env:"PORT"`
|
||||
ListenLimit int `long:"listen-limit" description:"limit the number of outstanding requests"`
|
||||
KeepAlive time.Duration `long:"keep-alive" description:"sets the TCP keep-alive timeouts on accepted connections. It prunes dead TCP connections ( e.g. closing laptop mid-download)" default:"3m"`
|
||||
ReadTimeout time.Duration `long:"read-timeout" description:"maximum duration before timing out read of the request" default:"30s"`
|
||||
WriteTimeout time.Duration `long:"write-timeout" description:"maximum duration before timing out write of the response" default:"60s"`
|
||||
httpServerL []net.Listener
|
||||
|
||||
TLSHost string `long:"tls-host" description:"the IP to listen on for tls, when not specified it's the same as --host" env:"TLS_HOST"`
|
||||
TLSPort int `long:"tls-port" description:"the port to listen on for secure connections, defaults to a random value" env:"TLS_PORT"`
|
||||
TLSCertificate flags.Filename `long:"tls-certificate" description:"the certificate to use for secure connections" env:"TLS_CERTIFICATE"`
|
||||
TLSCertificateKey flags.Filename `long:"tls-key" description:"the private key to use for secure connections" env:"TLS_PRIVATE_KEY"`
|
||||
TLSCACertificate flags.Filename `long:"tls-ca" description:"the certificate authority file to be used with mutual tls auth" env:"TLS_CA_CERTIFICATE"`
|
||||
TLSListenLimit int `long:"tls-listen-limit" description:"limit the number of outstanding requests"`
|
||||
TLSKeepAlive time.Duration `long:"tls-keep-alive" description:"sets the TCP keep-alive timeouts on accepted connections. It prunes dead TCP connections ( e.g. closing laptop mid-download)"`
|
||||
TLSReadTimeout time.Duration `long:"tls-read-timeout" description:"maximum duration before timing out read of the request"`
|
||||
TLSWriteTimeout time.Duration `long:"tls-write-timeout" description:"maximum duration before timing out write of the response"`
|
||||
httpsServerL []net.Listener
|
||||
|
||||
api *operations.ConsoleAPI
|
||||
handler http.Handler
|
||||
hasListeners bool
|
||||
shutdown chan struct{}
|
||||
shuttingDown int32
|
||||
interrupted bool
|
||||
interrupt chan os.Signal
|
||||
}
|
||||
|
||||
// Logf logs message either via defined user logger or via system one if no user logger is defined.
|
||||
func (s *Server) Logf(f string, args ...interface{}) {
|
||||
if s.api != nil && s.api.Logger != nil {
|
||||
s.api.Logger(f, args...)
|
||||
} else {
|
||||
log.Printf(f, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Fatalf logs message either via defined user logger or via system one if no user logger is defined.
|
||||
// Exits with non-zero status after printing
|
||||
func (s *Server) Fatalf(f string, args ...interface{}) {
|
||||
if s.api != nil && s.api.Logger != nil {
|
||||
s.api.Logger(f, args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Fatalf(f, args...)
|
||||
}
|
||||
|
||||
// SetAPI configures the server with the specified API. Needs to be called before Serve
|
||||
func (s *Server) SetAPI(api *operations.ConsoleAPI) {
|
||||
if api == nil {
|
||||
s.api = nil
|
||||
s.handler = nil
|
||||
return
|
||||
}
|
||||
|
||||
s.api = api
|
||||
s.handler = configureAPI(api)
|
||||
}
|
||||
|
||||
func (s *Server) hasScheme(scheme string) bool {
|
||||
schemes := s.EnabledListeners
|
||||
if len(schemes) == 0 {
|
||||
schemes = defaultSchemes
|
||||
}
|
||||
|
||||
for _, v := range schemes {
|
||||
if v == scheme {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Serve the api
|
||||
func (s *Server) Serve() (err error) {
|
||||
if !s.hasListeners {
|
||||
if err = s.Listen(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// set default handler, if none is set
|
||||
if s.handler == nil {
|
||||
if s.api == nil {
|
||||
return errors.New("can't create the default handler, as no api is set")
|
||||
}
|
||||
|
||||
s.SetHandler(s.api.Serve(nil))
|
||||
}
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
once := new(sync.Once)
|
||||
signalNotify(s.interrupt)
|
||||
go handleInterrupt(once, s)
|
||||
|
||||
servers := []*http.Server{}
|
||||
|
||||
if s.hasScheme(schemeUnix) {
|
||||
domainSocket := new(http.Server)
|
||||
domainSocket.MaxHeaderBytes = int(s.MaxHeaderSize)
|
||||
domainSocket.Handler = s.handler
|
||||
if int64(s.CleanupTimeout) > 0 {
|
||||
domainSocket.IdleTimeout = s.CleanupTimeout
|
||||
}
|
||||
|
||||
configureServer(domainSocket, "unix", string(s.SocketPath))
|
||||
|
||||
servers = append(servers, domainSocket)
|
||||
wg.Add(1)
|
||||
s.Logf("Serving console at unix://%s", s.SocketPath)
|
||||
go func(l net.Listener) {
|
||||
defer wg.Done()
|
||||
if err := domainSocket.Serve(l); err != nil && err != http.ErrServerClosed {
|
||||
s.Fatalf("%v", err)
|
||||
}
|
||||
s.Logf("Stopped serving console at unix://%s", s.SocketPath)
|
||||
}(s.domainSocketL)
|
||||
}
|
||||
|
||||
if s.hasScheme(schemeHTTP) {
|
||||
httpServer := new(http.Server)
|
||||
httpServer.MaxHeaderBytes = int(s.MaxHeaderSize)
|
||||
httpServer.ReadTimeout = s.ReadTimeout
|
||||
httpServer.WriteTimeout = s.WriteTimeout
|
||||
httpServer.SetKeepAlivesEnabled(int64(s.KeepAlive) > 0)
|
||||
if s.ListenLimit > 0 {
|
||||
for i := range s.httpServerL {
|
||||
s.httpServerL[i] = netutil.LimitListener(s.httpServerL[i], s.ListenLimit)
|
||||
}
|
||||
}
|
||||
|
||||
if int64(s.CleanupTimeout) > 0 {
|
||||
httpServer.IdleTimeout = s.CleanupTimeout
|
||||
}
|
||||
|
||||
httpServer.Handler = s.handler
|
||||
|
||||
configureServer(httpServer, "http", s.httpServerL[0].Addr().String())
|
||||
|
||||
servers = append(servers, httpServer)
|
||||
s.Logf("Serving console at http://%s", s.httpServerL[0].Addr())
|
||||
for i := range s.httpServerL {
|
||||
wg.Add(1)
|
||||
go func(l net.Listener) {
|
||||
defer wg.Done()
|
||||
if err := httpServer.Serve(l); err != nil && err != http.ErrServerClosed {
|
||||
s.Fatalf("%v", err)
|
||||
}
|
||||
s.Logf("Stopped serving console at http://%s", l.Addr())
|
||||
}(s.httpServerL[i])
|
||||
}
|
||||
}
|
||||
|
||||
if s.hasScheme(schemeHTTPS) {
|
||||
httpsServer := new(http.Server)
|
||||
httpsServer.MaxHeaderBytes = int(s.MaxHeaderSize)
|
||||
httpsServer.ReadTimeout = s.TLSReadTimeout
|
||||
httpsServer.WriteTimeout = s.TLSWriteTimeout
|
||||
httpsServer.SetKeepAlivesEnabled(int64(s.TLSKeepAlive) > 0)
|
||||
if s.TLSListenLimit > 0 {
|
||||
for i := range s.httpsServerL {
|
||||
s.httpsServerL[i] = netutil.LimitListener(s.httpsServerL[i], s.TLSListenLimit)
|
||||
}
|
||||
}
|
||||
if int64(s.CleanupTimeout) > 0 {
|
||||
httpsServer.IdleTimeout = s.CleanupTimeout
|
||||
}
|
||||
httpsServer.Handler = s.handler
|
||||
|
||||
// Inspired by https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go
|
||||
httpsServer.TLSConfig = &tls.Config{
|
||||
// Causes servers to use Go's default ciphersuite preferences,
|
||||
// which are tuned to avoid attacks. Does nothing on clients.
|
||||
PreferServerCipherSuites: true,
|
||||
// Only use curves which have assembly implementations
|
||||
// https://github.com/golang/go/tree/master/src/crypto/elliptic
|
||||
CurvePreferences: []tls.CurveID{tls.CurveP256},
|
||||
// Use modern tls mode https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
// https://www.owasp.org/index.php/Transport_Layer_Protection_Cheat_Sheet#Rule_-_Only_Support_Strong_Protocols
|
||||
MinVersion: tls.VersionTLS12,
|
||||
// These ciphersuites support Forward Secrecy: https://en.wikipedia.org/wiki/Forward_secrecy
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
},
|
||||
}
|
||||
|
||||
// build standard config from server options
|
||||
if s.TLSCertificate != "" && s.TLSCertificateKey != "" {
|
||||
httpsServer.TLSConfig.Certificates = make([]tls.Certificate, 1)
|
||||
httpsServer.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(string(s.TLSCertificate), string(s.TLSCertificateKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if s.TLSCACertificate != "" {
|
||||
// include specified CA certificate
|
||||
caCert, caCertErr := os.ReadFile(string(s.TLSCACertificate))
|
||||
if caCertErr != nil {
|
||||
return caCertErr
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
ok := caCertPool.AppendCertsFromPEM(caCert)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot parse CA certificate")
|
||||
}
|
||||
httpsServer.TLSConfig.ClientCAs = caCertPool
|
||||
httpsServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
|
||||
// call custom TLS configurator
|
||||
configureTLS(httpsServer.TLSConfig)
|
||||
|
||||
if len(httpsServer.TLSConfig.Certificates) == 0 && httpsServer.TLSConfig.GetCertificate == nil {
|
||||
// after standard and custom config are passed, this ends up with no certificate
|
||||
if s.TLSCertificate == "" {
|
||||
if s.TLSCertificateKey == "" {
|
||||
s.Fatalf("the required flags `--tls-certificate` and `--tls-key` were not specified")
|
||||
}
|
||||
s.Fatalf("the required flag `--tls-certificate` was not specified")
|
||||
}
|
||||
if s.TLSCertificateKey == "" {
|
||||
s.Fatalf("the required flag `--tls-key` was not specified")
|
||||
}
|
||||
// this happens with a wrong custom TLS configurator
|
||||
s.Fatalf("no certificate was configured for TLS")
|
||||
}
|
||||
|
||||
configureServer(httpsServer, "https", s.httpsServerL[0].Addr().String())
|
||||
|
||||
servers = append(servers, httpsServer)
|
||||
s.Logf("Serving console at https://%s", s.httpsServerL[0].Addr())
|
||||
for i := range s.httpsServerL {
|
||||
wg.Add(1)
|
||||
go func(l net.Listener) {
|
||||
defer wg.Done()
|
||||
if err := httpsServer.Serve(l); err != nil && err != http.ErrServerClosed {
|
||||
s.Fatalf("%v", err)
|
||||
}
|
||||
s.Logf("Stopped serving console at https://%s", l.Addr())
|
||||
}(tls.NewListener(s.httpsServerL[i], httpsServer.TLSConfig))
|
||||
}
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go s.handleShutdown(wg, &servers)
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Listen creates the listeners for the server
|
||||
func (s *Server) Listen() error {
|
||||
if s.hasListeners { // already done this
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.hasScheme(schemeHTTPS) {
|
||||
// Use http host if https host wasn't defined
|
||||
if s.TLSHost == "" {
|
||||
s.TLSHost = s.Host
|
||||
}
|
||||
// Use http listen limit if https listen limit wasn't defined
|
||||
if s.TLSListenLimit == 0 {
|
||||
s.TLSListenLimit = s.ListenLimit
|
||||
}
|
||||
// Use http tcp keep alive if https tcp keep alive wasn't defined
|
||||
if int64(s.TLSKeepAlive) == 0 {
|
||||
s.TLSKeepAlive = s.KeepAlive
|
||||
}
|
||||
// Use http read timeout if https read timeout wasn't defined
|
||||
if int64(s.TLSReadTimeout) == 0 {
|
||||
s.TLSReadTimeout = s.ReadTimeout
|
||||
}
|
||||
// Use http write timeout if https write timeout wasn't defined
|
||||
if int64(s.TLSWriteTimeout) == 0 {
|
||||
s.TLSWriteTimeout = s.WriteTimeout
|
||||
}
|
||||
}
|
||||
|
||||
if s.hasScheme(schemeUnix) {
|
||||
domSockListener, err := net.Listen("unix", string(s.SocketPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.domainSocketL = domSockListener
|
||||
}
|
||||
|
||||
lookup := func(addr string) []net.IP {
|
||||
ips, err := net.LookupIP(addr)
|
||||
if err == nil {
|
||||
return ips
|
||||
}
|
||||
return []net.IP{net.ParseIP(addr)}
|
||||
}
|
||||
|
||||
convert := func(ip net.IP) (string, string) {
|
||||
if ip == nil {
|
||||
return "", "tcp"
|
||||
}
|
||||
proto := "tcp4"
|
||||
if ip.To4() == nil {
|
||||
proto = "tcp6"
|
||||
}
|
||||
return ip.String(), proto
|
||||
}
|
||||
|
||||
if s.hasScheme(schemeHTTP) {
|
||||
for _, ip := range lookup(s.Host) {
|
||||
host, proto := convert(ip)
|
||||
listener, err := net.Listen(proto, net.JoinHostPort(host, strconv.Itoa(s.Port)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.Host == "" || s.Port == 0 {
|
||||
h, p, err := swag.SplitHostPort(listener.Addr().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Host = h
|
||||
s.Port = p
|
||||
}
|
||||
s.httpServerL = append(s.httpServerL, listener)
|
||||
}
|
||||
}
|
||||
|
||||
if s.hasScheme(schemeHTTPS) {
|
||||
for _, ip := range lookup(s.TLSHost) {
|
||||
host, proto := convert(ip)
|
||||
tlsListener, err := net.Listen(proto, net.JoinHostPort(host, strconv.Itoa(s.TLSPort)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.TLSHost == "" || s.TLSPort == 0 {
|
||||
sh, sp, err := swag.SplitHostPort(tlsListener.Addr().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.TLSHost = sh
|
||||
s.TLSPort = sp
|
||||
}
|
||||
s.httpsServerL = append(s.httpsServerL, tlsListener)
|
||||
}
|
||||
}
|
||||
|
||||
s.hasListeners = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown server and clean up resources
|
||||
func (s *Server) Shutdown() error {
|
||||
if atomic.CompareAndSwapInt32(&s.shuttingDown, 0, 1) {
|
||||
close(s.shutdown)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) handleShutdown(wg *sync.WaitGroup, serversPtr *[]*http.Server) {
|
||||
// wg.Done must occur last, after s.api.ServerShutdown()
|
||||
// (to preserve old behavior)
|
||||
defer wg.Done()
|
||||
|
||||
<-s.shutdown
|
||||
|
||||
servers := *serversPtr
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), s.GracefulTimeout)
|
||||
defer cancel()
|
||||
|
||||
// first execute the pre-shutdown hook
|
||||
s.api.PreServerShutdown()
|
||||
|
||||
shutdownChan := make(chan bool)
|
||||
for i := range servers {
|
||||
server := servers[i]
|
||||
go func() {
|
||||
var success bool
|
||||
defer func() {
|
||||
shutdownChan <- success
|
||||
}()
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
// Error from closing listeners, or context timeout:
|
||||
s.Logf("HTTP server Shutdown: %v", err)
|
||||
} else {
|
||||
success = true
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait until all listeners have successfully shut down before calling ServerShutdown
|
||||
success := true
|
||||
for range servers {
|
||||
success = success && <-shutdownChan
|
||||
}
|
||||
if success {
|
||||
s.api.ServerShutdown()
|
||||
}
|
||||
}
|
||||
|
||||
// GetHandler returns a handler useful for testing
|
||||
func (s *Server) GetHandler() http.Handler {
|
||||
return s.handler
|
||||
}
|
||||
|
||||
// SetHandler allows for setting a http handler on this server
|
||||
func (s *Server) SetHandler(handler http.Handler) {
|
||||
s.handler = handler
|
||||
}
|
||||
|
||||
// UnixListener returns the domain socket listener
|
||||
func (s *Server) UnixListener() (net.Listener, error) {
|
||||
if !s.hasListeners {
|
||||
if err := s.Listen(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return s.domainSocketL, nil
|
||||
}
|
||||
|
||||
// HTTPListener returns the http listener
|
||||
func (s *Server) HTTPListener() ([]net.Listener, error) {
|
||||
if !s.hasListeners {
|
||||
if err := s.Listen(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return s.httpServerL, nil
|
||||
}
|
||||
|
||||
// TLSListener returns the https listener
|
||||
func (s *Server) TLSListener() ([]net.Listener, error) {
|
||||
if !s.hasListeners {
|
||||
if err := s.Listen(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return s.httpsServerL, nil
|
||||
}
|
||||
|
||||
func handleInterrupt(once *sync.Once, s *Server) {
|
||||
once.Do(func() {
|
||||
for range s.interrupt {
|
||||
if s.interrupted {
|
||||
s.Logf("Server already shutting down")
|
||||
continue
|
||||
}
|
||||
s.interrupted = true
|
||||
s.Logf("Shutting down... ")
|
||||
if err := s.Shutdown(); err != nil {
|
||||
s.Logf("HTTP server Shutdown: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func signalNotify(interrupt chan<- os.Signal) {
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
}
|
||||
38
api/doc.go
Normal file
38
api/doc.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
// Package api MinIO Console Server
|
||||
//
|
||||
// Schemes:
|
||||
// http
|
||||
// ws
|
||||
// Host: localhost
|
||||
// BasePath: /api/v1
|
||||
// Version: 0.1.0
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
// - multipart/form-data
|
||||
//
|
||||
// Produces:
|
||||
// - application/zip
|
||||
// - application/octet-stream
|
||||
// - application/json
|
||||
//
|
||||
// swagger:meta
|
||||
package api
|
||||
17655
api/embedded_spec.go
Normal file
17655
api/embedded_spec.go
Normal file
File diff suppressed because it is too large
Load Diff
278
api/errors.go
Normal file
278
api/errors.go
Normal file
@@ -0,0 +1,278 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDefault = errors.New("an error occurred, please try again")
|
||||
ErrInvalidLogin = errors.New("invalid Login")
|
||||
ErrForbidden = errors.New("403 Forbidden")
|
||||
ErrBadRequest = errors.New("400 Bad Request")
|
||||
ErrFileTooLarge = errors.New("413 File too Large")
|
||||
ErrInvalidSession = errors.New("invalid session")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrGroupAlreadyExists = errors.New("error group name already in use")
|
||||
ErrInvalidErasureCodingValue = errors.New("invalid Erasure Coding Value")
|
||||
ErrBucketBodyNotInRequest = errors.New("error bucket body not in request")
|
||||
ErrBucketNameNotInRequest = errors.New("error bucket name not in request")
|
||||
ErrGroupBodyNotInRequest = errors.New("error group body not in request")
|
||||
ErrGroupNameNotInRequest = errors.New("error group name not in request")
|
||||
ErrPolicyNameNotInRequest = errors.New("error policy name not in request")
|
||||
ErrPolicyBodyNotInRequest = errors.New("error policy body not in request")
|
||||
ErrPolicyNameContainsSpace = errors.New("error policy name cannot contain spaces")
|
||||
ErrInvalidEncryptionAlgorithm = errors.New("error invalid encryption algorithm")
|
||||
ErrSSENotConfigured = errors.New("error server side encryption configuration not found")
|
||||
ErrBucketLifeCycleNotConfigured = errors.New("error bucket life cycle configuration not found")
|
||||
ErrChangePassword = errors.New("error please check your current password")
|
||||
ErrInvalidLicense = errors.New("invalid license key")
|
||||
ErrLicenseNotFound = errors.New("license not found")
|
||||
ErrAvoidSelfAccountDelete = errors.New("logged in user cannot be deleted by itself")
|
||||
ErrAccessDenied = errors.New("access denied")
|
||||
ErrOauth2Provider = errors.New("unable to contact configured identity provider")
|
||||
ErrOauth2Login = errors.New("unable to login using configured identity provider")
|
||||
ErrNonUniqueAccessKey = errors.New("access key already in use")
|
||||
ErrRemoteTierExists = errors.New("specified remote tier already exists")
|
||||
ErrRemoteTierNotFound = errors.New("specified remote tier was not found")
|
||||
ErrRemoteTierUppercase = errors.New("tier name must be in uppercase")
|
||||
ErrRemoteTierBucketNotFound = errors.New("remote tier bucket not found")
|
||||
ErrRemoteInvalidCredentials = errors.New("invalid remote tier credentials")
|
||||
ErrUnableToGetTenantUsage = errors.New("unable to get tenant usage")
|
||||
ErrTooManyNodes = errors.New("cannot request more nodes than what is available in the cluster")
|
||||
ErrTooFewNodes = errors.New("there are not enough nodes in the cluster to support this tenant")
|
||||
ErrTooFewAvailableNodes = errors.New("there is not enough available nodes to satisfy this requirement")
|
||||
ErrFewerThanFourNodes = errors.New("at least 4 nodes are required for a tenant")
|
||||
ErrUnableToGetTenantLogs = errors.New("unable to get tenant logs")
|
||||
ErrUnableToUpdateTenantCertificates = errors.New("unable to update tenant certificates")
|
||||
ErrUpdatingEncryptionConfig = errors.New("unable to update encryption configuration")
|
||||
ErrDeletingEncryptionConfig = errors.New("error disabling tenant encryption")
|
||||
ErrEncryptionConfigNotFound = errors.New("encryption configuration not found")
|
||||
ErrPolicyNotFound = errors.New("policy does not exist")
|
||||
ErrLoginNotAllowed = errors.New("login not allowed")
|
||||
ErrHealthReportFail = errors.New("failure to generate Health report")
|
||||
)
|
||||
|
||||
type CodedAPIError struct {
|
||||
Code int
|
||||
APIError *models.APIError
|
||||
}
|
||||
|
||||
// ErrorWithContext :
|
||||
func ErrorWithContext(ctx context.Context, err ...interface{}) *CodedAPIError {
|
||||
errorCode := 500
|
||||
errorMessage := ErrDefault.Error()
|
||||
var detailedMessage string
|
||||
var err1 error
|
||||
var exists bool
|
||||
if len(err) > 0 {
|
||||
if err1, exists = err[0].(error); exists {
|
||||
detailedMessage = err1.Error()
|
||||
var lastError error
|
||||
if len(err) > 1 {
|
||||
if err2, lastExists := err[1].(error); lastExists {
|
||||
lastError = err2
|
||||
}
|
||||
}
|
||||
if err1.Error() == ErrForbidden.Error() {
|
||||
errorCode = 403
|
||||
}
|
||||
if err1.Error() == ErrBadRequest.Error() {
|
||||
errorCode = 400
|
||||
}
|
||||
if err1 == ErrNotFound {
|
||||
errorCode = 404
|
||||
errorMessage = ErrNotFound.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrInvalidLogin) {
|
||||
detailedMessage = ""
|
||||
errorCode = 401
|
||||
errorMessage = ErrInvalidLogin.Error()
|
||||
}
|
||||
if strings.Contains(strings.ToLower(err1.Error()), ErrAccessDenied.Error()) {
|
||||
errorCode = 403
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
// If the last error is ErrInvalidLogin, this is a login failure
|
||||
if errors.Is(lastError, ErrInvalidLogin) {
|
||||
detailedMessage = ""
|
||||
errorCode = 401
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
if strings.Contains(err1.Error(), ErrLoginNotAllowed.Error()) {
|
||||
detailedMessage = ""
|
||||
errorCode = 400
|
||||
errorMessage = ErrLoginNotAllowed.Error()
|
||||
}
|
||||
// console invalid erasure coding value
|
||||
if errors.Is(err1, ErrInvalidErasureCodingValue) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrInvalidErasureCodingValue.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrBucketBodyNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrBucketBodyNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrBucketNameNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrBucketNameNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrGroupBodyNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrGroupBodyNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrGroupNameNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrGroupNameNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrPolicyNameNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrPolicyNameNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrPolicyBodyNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrPolicyBodyNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrPolicyNameContainsSpace) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrPolicyNameContainsSpace.Error()
|
||||
}
|
||||
// console invalid session errors
|
||||
if errors.Is(err1, ErrInvalidSession) {
|
||||
errorCode = 401
|
||||
errorMessage = ErrInvalidSession.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrGroupAlreadyExists) {
|
||||
errorCode = 400
|
||||
errorMessage = ErrGroupAlreadyExists.Error()
|
||||
}
|
||||
// Bucket life cycle not configured
|
||||
if errors.Is(err1, ErrBucketLifeCycleNotConfigured) {
|
||||
errorCode = 404
|
||||
errorMessage = ErrBucketLifeCycleNotConfigured.Error()
|
||||
}
|
||||
// Encryption not configured
|
||||
if errors.Is(err1, ErrSSENotConfigured) {
|
||||
errorCode = 404
|
||||
errorMessage = ErrSSENotConfigured.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrEncryptionConfigNotFound) {
|
||||
errorCode = 404
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
// account change password
|
||||
if errors.Is(err1, ErrChangePassword) {
|
||||
errorCode = 403
|
||||
errorMessage = ErrChangePassword.Error()
|
||||
}
|
||||
if madmin.ToErrorResponse(err1).Code == "SignatureDoesNotMatch" {
|
||||
errorCode = 403
|
||||
errorMessage = ErrChangePassword.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrLicenseNotFound) {
|
||||
errorCode = 404
|
||||
errorMessage = ErrLicenseNotFound.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrInvalidLicense) {
|
||||
errorCode = 404
|
||||
errorMessage = ErrInvalidLicense.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrAvoidSelfAccountDelete) {
|
||||
errorCode = 403
|
||||
errorMessage = ErrAvoidSelfAccountDelete.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrAccessDenied) {
|
||||
errorCode = 403
|
||||
errorMessage = ErrAccessDenied.Error()
|
||||
}
|
||||
if errors.Is(err1, ErrPolicyNotFound) {
|
||||
errorCode = 404
|
||||
errorMessage = ErrPolicyNotFound.Error()
|
||||
}
|
||||
if madmin.ToErrorResponse(err1).Code == "AccessDenied" {
|
||||
errorCode = 403
|
||||
errorMessage = ErrAccessDenied.Error()
|
||||
}
|
||||
if madmin.ToErrorResponse(err1).Code == "InvalidAccessKeyId" {
|
||||
|
||||
errorCode = 401
|
||||
errorMessage = ErrInvalidSession.Error()
|
||||
}
|
||||
// console invalid session errors
|
||||
if madmin.ToErrorResponse(err1).Code == "XMinioAdminNoSuchUser" {
|
||||
errorCode = 401
|
||||
errorMessage = ErrInvalidSession.Error()
|
||||
}
|
||||
// tiering errors
|
||||
if err1.Error() == ErrRemoteTierExists.Error() {
|
||||
errorCode = 400
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
if err1.Error() == ErrRemoteTierNotFound.Error() {
|
||||
errorCode = 400
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
|
||||
if err1.Error() == ErrRemoteTierUppercase.Error() {
|
||||
errorCode = 400
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
if err1.Error() == ErrRemoteTierBucketNotFound.Error() {
|
||||
errorCode = 400
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
if err1.Error() == ErrRemoteInvalidCredentials.Error() {
|
||||
errorCode = 403
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
if err1.Error() == ErrFileTooLarge.Error() {
|
||||
errorCode = 413
|
||||
errorMessage = err1.Error()
|
||||
}
|
||||
// bucket already exists
|
||||
if minio.ToErrorResponse(err1).Code == "BucketAlreadyOwnedByYou" {
|
||||
errorCode = 400
|
||||
errorMessage = "Bucket already exists"
|
||||
}
|
||||
|
||||
LogError("ErrorWithContext:%v", err...)
|
||||
LogIf(ctx, err1, err...)
|
||||
}
|
||||
|
||||
if len(err) > 1 && err[1] != nil {
|
||||
if err2, ok := err[1].(error); ok {
|
||||
errorMessage = err2.Error()
|
||||
}
|
||||
}
|
||||
}
|
||||
return &CodedAPIError{Code: errorCode, APIError: &models.APIError{Message: errorMessage, DetailedMessage: detailedMessage}}
|
||||
}
|
||||
|
||||
// Error receives an errors object and parse it against k8sErrors, returns the right errors code paired with a generic errors message
|
||||
func Error(err ...interface{}) *CodedAPIError {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
return ErrorWithContext(ctx, err...)
|
||||
}
|
||||
159
api/errors_test.go
Normal file
159
api/errors_test.go
Normal file
@@ -0,0 +1,159 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
type args struct {
|
||||
err []interface{}
|
||||
}
|
||||
|
||||
type testError struct {
|
||||
name string
|
||||
args args
|
||||
want *CodedAPIError
|
||||
}
|
||||
|
||||
var tests []testError
|
||||
|
||||
type expectedError struct {
|
||||
err error
|
||||
code int
|
||||
}
|
||||
|
||||
appErrors := map[string]expectedError{
|
||||
"ErrDefault": {code: 500, err: ErrDefault},
|
||||
|
||||
"ErrForbidden": {code: 403, err: ErrForbidden},
|
||||
"ErrFileTooLarge": {code: 413, err: ErrFileTooLarge},
|
||||
"ErrInvalidSession": {code: 401, err: ErrInvalidSession},
|
||||
"ErrNotFound": {code: 404, err: ErrNotFound},
|
||||
"ErrGroupAlreadyExists": {code: 400, err: ErrGroupAlreadyExists},
|
||||
"ErrInvalidErasureCodingValue": {code: 400, err: ErrInvalidErasureCodingValue},
|
||||
"ErrBucketBodyNotInRequest": {code: 400, err: ErrBucketBodyNotInRequest},
|
||||
"ErrBucketNameNotInRequest": {code: 400, err: ErrBucketNameNotInRequest},
|
||||
"ErrGroupBodyNotInRequest": {code: 400, err: ErrGroupBodyNotInRequest},
|
||||
"ErrGroupNameNotInRequest": {code: 400, err: ErrGroupNameNotInRequest},
|
||||
"ErrPolicyNameNotInRequest": {code: 400, err: ErrPolicyNameNotInRequest},
|
||||
"ErrPolicyBodyNotInRequest": {code: 400, err: ErrPolicyBodyNotInRequest},
|
||||
"ErrInvalidEncryptionAlgorithm": {code: 500, err: ErrInvalidEncryptionAlgorithm},
|
||||
"ErrSSENotConfigured": {code: 404, err: ErrSSENotConfigured},
|
||||
"ErrBucketLifeCycleNotConfigured": {code: 404, err: ErrBucketLifeCycleNotConfigured},
|
||||
"ErrChangePassword": {code: 403, err: ErrChangePassword},
|
||||
"ErrInvalidLicense": {code: 404, err: ErrInvalidLicense},
|
||||
"ErrLicenseNotFound": {code: 404, err: ErrLicenseNotFound},
|
||||
"ErrAvoidSelfAccountDelete": {code: 403, err: ErrAvoidSelfAccountDelete},
|
||||
|
||||
"ErrNonUniqueAccessKey": {code: 500, err: ErrNonUniqueAccessKey},
|
||||
"ErrRemoteTierExists": {code: 400, err: ErrRemoteTierExists},
|
||||
"ErrRemoteTierNotFound": {code: 400, err: ErrRemoteTierNotFound},
|
||||
"ErrRemoteTierUppercase": {code: 400, err: ErrRemoteTierUppercase},
|
||||
"ErrRemoteTierBucketNotFound": {code: 400, err: ErrRemoteTierBucketNotFound},
|
||||
"ErrRemoteInvalidCredentials": {code: 403, err: ErrRemoteInvalidCredentials},
|
||||
"ErrTooFewNodes": {code: 500, err: ErrTooFewNodes},
|
||||
"ErrUnableToGetTenantUsage": {code: 500, err: ErrUnableToGetTenantUsage},
|
||||
"ErrTooManyNodes": {code: 500, err: ErrTooManyNodes},
|
||||
"ErrAccessDenied": {code: 403, err: ErrAccessDenied},
|
||||
"ErrTooFewAvailableNodes": {code: 500, err: ErrTooFewAvailableNodes},
|
||||
"ErrFewerThanFourNodes": {code: 500, err: ErrFewerThanFourNodes},
|
||||
"ErrUnableToGetTenantLogs": {code: 500, err: ErrUnableToGetTenantLogs},
|
||||
"ErrUnableToUpdateTenantCertificates": {code: 500, err: ErrUnableToUpdateTenantCertificates},
|
||||
"ErrUpdatingEncryptionConfig": {code: 500, err: ErrUpdatingEncryptionConfig},
|
||||
"ErrDeletingEncryptionConfig": {code: 500, err: ErrDeletingEncryptionConfig},
|
||||
"ErrEncryptionConfigNotFound": {code: 404, err: ErrEncryptionConfigNotFound},
|
||||
}
|
||||
|
||||
for k, e := range appErrors {
|
||||
tests = append(tests, testError{
|
||||
name: fmt.Sprintf("%s error", k),
|
||||
args: args{
|
||||
err: []interface{}{e.err},
|
||||
},
|
||||
want: &CodedAPIError{
|
||||
Code: e.code,
|
||||
APIError: &models.APIError{Message: e.err.Error(), DetailedMessage: e.err.Error()},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
tests = append(tests,
|
||||
testError{
|
||||
name: "passing multiple errors but ErrInvalidLogin is last",
|
||||
args: args{
|
||||
err: []interface{}{ErrDefault, ErrInvalidLogin},
|
||||
},
|
||||
want: &CodedAPIError{
|
||||
Code: int(401),
|
||||
APIError: &models.APIError{Message: ErrDefault.Error(), DetailedMessage: ""},
|
||||
},
|
||||
})
|
||||
tests = append(tests,
|
||||
testError{
|
||||
name: "login error omits detailedMessage",
|
||||
args: args{
|
||||
err: []interface{}{ErrInvalidLogin},
|
||||
},
|
||||
want: &CodedAPIError{
|
||||
Code: int(401),
|
||||
APIError: &models.APIError{Message: ErrInvalidLogin.Error(), DetailedMessage: ""},
|
||||
},
|
||||
})
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
got := Error(tt.args.err...)
|
||||
assert.Equalf(t, tt.want.Code, got.Code, "Error(%v) Got (%v)", tt.want.Code, got.Code)
|
||||
assert.Equalf(t, tt.want.APIError.DetailedMessage, got.APIError.DetailedMessage, "Error(%s) Got (%s)", tt.want.APIError.DetailedMessage, got.APIError.DetailedMessage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorWithContext(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
err []interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *CodedAPIError
|
||||
}{
|
||||
{
|
||||
name: "default error",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
err: []interface{}{ErrDefault},
|
||||
},
|
||||
want: &CodedAPIError{
|
||||
Code: 500, APIError: &models.APIError{Message: ErrDefault.Error(), DetailedMessage: ErrDefault.Error()},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
assert.Equalf(t, tt.want, ErrorWithContext(tt.args.ctx, tt.args.err...), "ErrorWithContext(%v, %v)", tt.args.ctx, tt.args.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
81
api/license.go
Normal file
81
api/license.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/minio/pkg/v3/licverifier"
|
||||
"github.com/minio/pkg/v3/subnet"
|
||||
)
|
||||
|
||||
type SubnetPlan int
|
||||
|
||||
const (
|
||||
PlanAGPL SubnetPlan = iota
|
||||
PlanStandard
|
||||
PlanEnterprise
|
||||
PlanEnterpriseLite
|
||||
PlanEnterprisePlus
|
||||
)
|
||||
|
||||
func (sp SubnetPlan) String() string {
|
||||
switch sp {
|
||||
case PlanStandard:
|
||||
return "standard"
|
||||
case PlanEnterprise:
|
||||
return "enterprise"
|
||||
case PlanEnterpriseLite:
|
||||
return "enterprise-lite"
|
||||
case PlanEnterprisePlus:
|
||||
return "enterprise-plus"
|
||||
default:
|
||||
return "agpl"
|
||||
}
|
||||
}
|
||||
|
||||
var InstanceLicensePlan = PlanAGPL
|
||||
|
||||
func getLicenseInfo(client http.Client, license string) (*licverifier.LicenseInfo, error) {
|
||||
lv := subnet.LicenseValidator{
|
||||
Client: client,
|
||||
ExpiryGracePeriod: 0,
|
||||
}
|
||||
lv.Init(getConsoleDevMode())
|
||||
return lv.ParseLicense(license)
|
||||
}
|
||||
|
||||
func fetchLicensePlan() {
|
||||
client := GetConsoleHTTPClient("127.0.0.1")
|
||||
licenseInfo, err := getLicenseInfo(*client, os.Getenv(EnvSubnetLicense))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch licenseInfo.Plan {
|
||||
case "STANDARD":
|
||||
InstanceLicensePlan = PlanStandard
|
||||
case "ENTERPRISE":
|
||||
InstanceLicensePlan = PlanEnterprise
|
||||
case "ENTERPRISE-LITE":
|
||||
InstanceLicensePlan = PlanEnterpriseLite
|
||||
case "ENTERPRISE-PLUS":
|
||||
InstanceLicensePlan = PlanEnterprisePlus
|
||||
default:
|
||||
InstanceLicensePlan = PlanAGPL
|
||||
}
|
||||
}
|
||||
83
api/logs.go
Normal file
83
api/logs.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/minio/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
infoLog = log.New(os.Stdout, "I: ", log.LstdFlags)
|
||||
errorLog = log.New(os.Stdout, "E: ", log.LstdFlags)
|
||||
)
|
||||
|
||||
func logInfo(msg string, data ...interface{}) {
|
||||
infoLog.Printf(msg+"\n", data...)
|
||||
}
|
||||
|
||||
func logError(msg string, data ...interface{}) {
|
||||
errorLog.Printf(msg+"\n", data...)
|
||||
}
|
||||
|
||||
func logIf(_ context.Context, _ error, _ ...interface{}) {
|
||||
}
|
||||
|
||||
// globally changeable logger styles
|
||||
var (
|
||||
LogInfo = logInfo
|
||||
LogError = logError
|
||||
LogIf = logIf
|
||||
)
|
||||
|
||||
// Context captures all command line flags values
|
||||
type Context struct {
|
||||
Host string
|
||||
HTTPPort, HTTPSPort int
|
||||
TLSRedirect string
|
||||
// Legacy options, TODO: remove in future
|
||||
TLSCertificate, TLSKey, TLSca string
|
||||
}
|
||||
|
||||
// Load loads api Context from command line context.
|
||||
func (c *Context) Load(ctx *cli.Context) error {
|
||||
*c = Context{
|
||||
Host: ctx.String("host"),
|
||||
HTTPPort: ctx.Int("port"),
|
||||
HTTPSPort: ctx.Int("tls-port"),
|
||||
TLSRedirect: ctx.String("tls-redirect"),
|
||||
// Legacy options to be removed.
|
||||
TLSCertificate: ctx.String("tls-certificate"),
|
||||
TLSKey: ctx.String("tls-key"),
|
||||
TLSca: ctx.String("tls-ca"),
|
||||
}
|
||||
if c.HTTPPort > 65535 {
|
||||
return errors.New("invalid argument --port out of range - ports can range from 1-65535")
|
||||
}
|
||||
if c.HTTPSPort > 65535 {
|
||||
return errors.New("invalid argument --tls-port out of range - ports can range from 1-65535")
|
||||
}
|
||||
if c.TLSRedirect != "on" && c.TLSRedirect != "off" {
|
||||
return errors.New("invalid argument --tls-redirect only accepts either 'on' or 'off'")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
110
api/logs_test.go
Normal file
110
api/logs_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestContext_Load(t *testing.T) {
|
||||
type fields struct {
|
||||
Host string
|
||||
HTTPPort int
|
||||
HTTPSPort int
|
||||
TLSRedirect string
|
||||
TLSCertificate string
|
||||
TLSKey string
|
||||
TLSca string
|
||||
}
|
||||
type args struct {
|
||||
values map[string]string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid args",
|
||||
args: args{
|
||||
values: map[string]string{
|
||||
"tls-redirect": "on",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid args",
|
||||
args: args{
|
||||
values: map[string]string{
|
||||
"tls-redirect": "aaaa",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid port http",
|
||||
args: args{
|
||||
values: map[string]string{
|
||||
"tls-redirect": "on",
|
||||
"port": "65536",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid port https",
|
||||
args: args{
|
||||
values: map[string]string{
|
||||
"tls-redirect": "on",
|
||||
"port": "65534",
|
||||
"tls-port": "65536",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
c := &Context{}
|
||||
|
||||
fs := flag.NewFlagSet("flags", flag.ContinueOnError)
|
||||
for k, v := range tt.args.values {
|
||||
fs.String(k, v, "ok")
|
||||
}
|
||||
|
||||
ctx := cli.NewContext(nil, fs, &cli.Context{})
|
||||
|
||||
err := c.Load(ctx)
|
||||
if tt.wantErr {
|
||||
assert.NotNilf(t, err, fmt.Sprintf("Load(%v)", err))
|
||||
} else {
|
||||
assert.Nilf(t, err, fmt.Sprintf("Load(%v)", err))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_logInfo(_ *testing.T) {
|
||||
logInfo("message", nil)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package account
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
@@ -48,10 +48,10 @@ func NewAccountChangePassword(ctx *middleware.Context, handler AccountChangePass
|
||||
return &AccountChangePassword{Context: ctx, Handler: handler}
|
||||
}
|
||||
|
||||
/* AccountChangePassword swagger:route POST /account/change-password UserAPI accountChangePassword
|
||||
/*
|
||||
AccountChangePassword swagger:route POST /account/change-password Account accountChangePassword
|
||||
|
||||
Change password of currently logged in user.
|
||||
|
||||
*/
|
||||
type AccountChangePassword struct {
|
||||
Context *middleware.Context
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -17,13 +17,12 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package account
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
@@ -83,7 +82,7 @@ func (o *AccountChangePasswordParams) BindRequest(r *http.Request, route *middle
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
ctx := validate.WithOperationRequest(context.Background())
|
||||
ctx := validate.WithOperationRequest(r.Context())
|
||||
if err := body.ContextValidate(ctx, route.Formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package account
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
@@ -33,7 +33,8 @@ import (
|
||||
// AccountChangePasswordNoContentCode is the HTTP code returned for type AccountChangePasswordNoContent
|
||||
const AccountChangePasswordNoContentCode int = 204
|
||||
|
||||
/*AccountChangePasswordNoContent A successful login.
|
||||
/*
|
||||
AccountChangePasswordNoContent A successful login.
|
||||
|
||||
swagger:response accountChangePasswordNoContent
|
||||
*/
|
||||
@@ -54,7 +55,8 @@ func (o *AccountChangePasswordNoContent) WriteResponse(rw http.ResponseWriter, p
|
||||
rw.WriteHeader(204)
|
||||
}
|
||||
|
||||
/*AccountChangePasswordDefault Generic error response.
|
||||
/*
|
||||
AccountChangePasswordDefault Generic error response.
|
||||
|
||||
swagger:response accountChangePasswordDefault
|
||||
*/
|
||||
@@ -64,7 +66,7 @@ type AccountChangePasswordDefault struct {
|
||||
/*
|
||||
In: Body
|
||||
*/
|
||||
Payload *models.Error `json:"body,omitempty"`
|
||||
Payload *models.APIError `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// NewAccountChangePasswordDefault creates AccountChangePasswordDefault with default headers values
|
||||
@@ -90,13 +92,13 @@ func (o *AccountChangePasswordDefault) SetStatusCode(code int) {
|
||||
}
|
||||
|
||||
// WithPayload adds the payload to the account change password default response
|
||||
func (o *AccountChangePasswordDefault) WithPayload(payload *models.Error) *AccountChangePasswordDefault {
|
||||
func (o *AccountChangePasswordDefault) WithPayload(payload *models.APIError) *AccountChangePasswordDefault {
|
||||
o.Payload = payload
|
||||
return o
|
||||
}
|
||||
|
||||
// SetPayload sets the payload to the account change password default response
|
||||
func (o *AccountChangePasswordDefault) SetPayload(payload *models.Error) {
|
||||
func (o *AccountChangePasswordDefault) SetPayload(payload *models.APIError) {
|
||||
o.Payload = payload
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package account
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package admin_api
|
||||
package account
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
@@ -48,10 +48,10 @@ func NewChangeUserPassword(ctx *middleware.Context, handler ChangeUserPasswordHa
|
||||
return &ChangeUserPassword{Context: ctx, Handler: handler}
|
||||
}
|
||||
|
||||
/* ChangeUserPassword swagger:route POST /account/change-user-password AdminAPI changeUserPassword
|
||||
/*
|
||||
ChangeUserPassword swagger:route POST /account/change-user-password Account changeUserPassword
|
||||
|
||||
Change password of currently logged in user.
|
||||
|
||||
*/
|
||||
type ChangeUserPassword struct {
|
||||
Context *middleware.Context
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -17,13 +17,12 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package admin_api
|
||||
package account
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
@@ -83,7 +82,7 @@ func (o *ChangeUserPasswordParams) BindRequest(r *http.Request, route *middlewar
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
ctx := validate.WithOperationRequest(context.Background())
|
||||
ctx := validate.WithOperationRequest(r.Context())
|
||||
if err := body.ContextValidate(ctx, route.Formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user