Compare commits
1024 Commits
user/alexc
...
developmen
Author | SHA1 | Date |
---|---|---|
Vitaliy Filippov | b5711e9cbf | |
Vitaliy Filippov | 36dc6298d2 | |
Vitaliy Filippov | bc2d637578 | |
Vitaliy Filippov | b543695048 | |
Vitaliy Filippov | 90024d044d | |
Vitaliy Filippov | 451ab33f68 | |
Vitaliy Filippov | c86107e912 | |
Vitaliy Filippov | 0a5962f256 | |
Vitaliy Filippov | 0e292791c6 | |
Vitaliy Filippov | fc07729bd0 | |
Vitaliy Filippov | 4527dd6795 | |
Vitaliy Filippov | 05fb581023 | |
Vitaliy Filippov | 956739a04e | |
Vitaliy Filippov | 7ad0888a66 | |
Vitaliy Filippov | bf01ba4ed1 | |
Vitaliy Filippov | ab019e7e50 | |
Vitaliy Filippov | 3797695e74 | |
Vitaliy Filippov | c8084196c4 | |
bert-e | b72e918ff9 | |
bert-e | 22887f47d8 | |
bert-e | 0cd10a73f3 | |
bert-e | e139406612 | |
Maha Benzekri | d91853a38b | |
Mickael Bourgois | a7e798f909 | |
Mickael Bourgois | 3a1ba29869 | |
Mickael Bourgois | dbb9b6d787 | |
Mickael Bourgois | fce76f0934 | |
Mickael Bourgois | 0e39aaac09 | |
Mickael Bourgois | 0b14c93fac | |
Mickael Bourgois | ab2960bbf4 | |
Mickael Bourgois | 7305b112e2 | |
Mickael Bourgois | cd9e2e757b | |
Mickael Bourgois | ca0904f584 | |
Mickael Bourgois | 0dd3dd35e6 | |
bert-e | bf7e4b7e23 | |
bert-e | 92f4794727 | |
Jonathan Gramain | c6ef85e3a1 | |
Jonathan Gramain | c0fe0cfbcf | |
bert-e | 9c936f2b83 | |
bert-e | d26bac2ebc | |
Jonathan Gramain | cfb9db5178 | |
Jonathan Gramain | 2ce004751a | |
Jonathan Gramain | 539219e046 | |
Jonathan Gramain | be49e55db5 | |
bert-e | e6b240421b | |
bert-e | 81739e3ecf | |
Jonathan Gramain | c475503248 | |
bert-e | 7acbd5d2fb | |
Jonathan Gramain | 8d726322e5 | |
williamlardier | 4f7aa54886 | |
williamlardier | 0117a5b0b4 | |
williamlardier | f679831ba2 | |
williamlardier | bb162ca7d3 | |
williamlardier | 0c6dfc7b6e | |
williamlardier | d608d849df | |
williamlardier | 2cb63f58d4 | |
williamlardier | 51585712f4 | |
bert-e | 61eb24e46f | |
bert-e | a34b162782 | |
bert-e | a9e50fe046 | |
bert-e | 4150a8432e | |
Taylor McKinnon | 7e70ff9cbc | |
bert-e | 09dc45289c | |
bert-e | 47c628e0e1 | |
Nicolas Humbert | a1f4d3fe8a | |
williamlardier | 926242b077 | |
williamlardier | aa2aac5db3 | |
williamlardier | f2e2d82e51 | |
williamlardier | 88ad86b0c6 | |
bert-e | 8f25892247 | |
bert-e | 9ac207187b | |
Anurag Mittal | 624a04805f | |
Anurag Mittal | ba99933765 | |
williamlardier | 38d1ac1d2c | |
Taylor McKinnon | 4f34a34a11 | |
Taylor McKinnon | 53f2a159fa | |
Maha Benzekri | 63f6a75a86 | |
Maha Benzekri | 41acc7968e | |
williamlardier | c98c5207fc | |
williamlardier | 615ee393a4 | |
williamlardier | 97dfc699aa | |
williamlardier | 76786282d1 | |
williamlardier | a19d6524be | |
williamlardier | bbf6dfba22 | |
williamlardier | f0663fd507 | |
williamlardier | d4decbbd6c | |
williamlardier | 288b2b7b87 | |
williamlardier | ccf9b62e59 | |
williamlardier | 9fc2d552ae | |
williamlardier | d7cc4cf7d5 | |
williamlardier | 334d33ef44 | |
williamlardier | 989b0214d9 | |
williamlardier | 04d0730f97 | |
williamlardier | fbc642c022 | |
williamlardier | 104435f0b6 | |
williamlardier | a362ac202e | |
williamlardier | 1277e58150 | |
williamlardier | 7727ccf5f0 | |
williamlardier | 71860fc90c | |
williamlardier | e504b52de7 | |
Maha Benzekri | b369a47c4d | |
Maha Benzekri | b4fa81e832 | |
Maha Benzekri | 1e03d53879 | |
Maha Benzekri | 63e502d419 | |
Maha Benzekri | d2a31dc20a | |
Maha Benzekri | f24411875f | |
Maha Benzekri | 4fd7faa6a3 | |
Francois Ferrand | 118aaba702 | |
Francois Ferrand | e4442fdc52 | |
Francois Ferrand | 7fa199741f | |
Francois Ferrand | f7f95af78f | |
Francois Ferrand | 2dc053a784 | |
Francois Ferrand | cc9bb9047e | |
Francois Ferrand | b824fc0828 | |
Francois Ferrand | a2e6d91cf2 | |
Francois Ferrand | c1060853dd | |
Francois Ferrand | 227d6edd09 | |
bert-e | b4754c68ea | |
bert-e | 11aea5d93b | |
bert-e | 0c50a5952f | |
bert-e | 4a32e05855 | |
bert-e | 402ed21b14 | |
Nicolas Humbert | a22719ed47 | |
Nicolas Humbert | 41975d539d | |
Nicolas Humbert | c6724eb811 | |
Nicolas Humbert | d027006938 | |
Nicolas Humbert | 92cfd47572 | |
bert-e | 8796bf0f44 | |
bert-e | 735fcd04ef | |
Jonathan Gramain | c5522685b2 | |
Jonathan Gramain | 48df7df271 | |
Jonathan Gramain | e028eb227f | |
Nicolas Humbert | caf3146662 | |
bert-e | 1dee707eb8 | |
Jonathan Gramain | 2c8d69c20a | |
Jonathan Gramain | 0b2b6ceeb5 | |
Jonathan Gramain | f4b3f39dc6 | |
Jonathan Gramain | 84260340d0 | |
Jonathan Gramain | e531abc346 | |
Jonathan Gramain | 20f6e3089b | |
bert-e | 9dc34f2155 | |
bert-e | 08a4c3ade3 | |
Nicolas Humbert | d5c731856b | |
Nicolas Humbert | 584c94692b | |
Nicolas Humbert | a0e5257c75 | |
bert-e | 5435c14116 | |
bert-e | 38c44ea874 | |
Nicolas Humbert | 4200346dd2 | |
bert-e | 5472d0da59 | |
bert-e | cdc0bb1128 | |
Nicolas Humbert | 795f8bcf1c | |
Nicolas Humbert | 9371d8d734 | |
Nicolas Humbert | 3f31c7f3a1 | |
KillianG | 39cba3ee6c | |
KillianG | a00952712f | |
KillianG | a246e18e17 | |
KillianG | 3bb3a4d161 | |
bert-e | c6ba7f981e | |
bert-e | 69c82da878 | |
bert-e | 762ae5a0ff | |
bert-e | 89dfc794a6 | |
bert-e | 3205d117f5 | |
bert-e | 4eafae44d8 | |
bert-e | 4cab3c84f3 | |
bert-e | e3301a2db9 | |
williamlardier | 0dcc93cdbe | |
williamlardier | 2f2f91d6e8 | |
williamlardier | a28b141dfb | |
williamlardier | 46fe061895 | |
williamlardier | 34202eaa62 | |
williamlardier | 4d343fe468 | |
williamlardier | 229e641f88 | |
bert-e | 1433973e5c | |
bert-e | 201170b1ed | |
bert-e | f13985094e | |
Nicolas Humbert | 395033acd2 | |
Nicolas Humbert | 632ef26826 | |
bert-e | 242b2ec85a | |
bert-e | 3186a97113 | |
bert-e | 3861b8d317 | |
bert-e | bb278f7d7e | |
bert-e | 3b9309490d | |
Will Toozs | 0118dfabbb | |
Will Toozs | ff40dfaadf | |
Will Toozs | 9a31236da0 | |
Will Toozs | 61ebacfbf3 | |
Will Toozs | aa646ced28 | |
Will Toozs | f2ca37b5fb | |
Will Toozs | 9d74cedde8 | |
bert-e | 9c99a6980f | |
bert-e | d4e255781b | |
bert-e | f5763d012e | |
bert-e | 8fb740cf09 | |
bert-e | 55c8d89de2 | |
bert-e | 1afaaec0ac | |
bert-e | e20e458971 | |
williamlardier | 56e52de056 | |
williamlardier | d9fc4aae50 | |
williamlardier | 08de09a2ab | |
bert-e | bef9220032 | |
bert-e | de20f1efdc | |
bert-e | 4817f11f36 | |
bert-e | a6b283f5a2 | |
bert-e | 3f810a7596 | |
bert-e | b89d19c9f8 | |
Nicolas Humbert | 4dc9788629 | |
Nicolas Humbert | 65a891d6f8 | |
bert-e | 2ecca4feef | |
Nicolas Humbert | c52a3a6e44 | |
williamlardier | d82965ff78 | |
williamlardier | f488a65f15 | |
williamlardier | 40a575a717 | |
williamlardier | fea82f15ea | |
bert-e | 06dc042154 | |
bert-e | aa4643644a | |
bert-e | 89edf7e3d0 | |
Francois Ferrand | 4c7d3ae4bc | |
Francois Ferrand | 23883dae8b | |
Francois Ferrand | e616ffa374 | |
Francois Ferrand | 515c20e4cf | |
Francois Ferrand | f8eedddebf | |
Francois Ferrand | f3654e4fb8 | |
Francois Ferrand | 517fb99190 | |
Francois Ferrand | 531c83a359 | |
Francois Ferrand | b84fa851f7 | |
Francois Ferrand | 4cb1a879f7 | |
Francois Ferrand | 7ae55b20e7 | |
Francois Ferrand | d0a6fa17a5 | |
Francois Ferrand | 7275459f70 | |
Hervé Dombya | 363afcd17f | |
Frédéric Meinnel | 1cf0250ce9 | |
Frédéric Meinnel | 20d0b38d0b | |
Frédéric Meinnel | 9988a8327a | |
Frédéric Meinnel | b481d24637 | |
Frédéric Meinnel | 71625774c1 | |
Frédéric Meinnel | 9b9338f2b8 | |
Frédéric Meinnel | 601619f200 | |
Frédéric Meinnel | a92e71fd50 | |
Frédéric Meinnel | 8802ea0617 | |
Frédéric Meinnel | acc5f74787 | |
Frédéric Meinnel | e3c093f352 | |
Frédéric Meinnel | e17383a678 | |
bert-e | 43f62b847c | |
bert-e | a031905bba | |
bert-e | 13ad6881f4 | |
Mickael Bourgois | dea5173075 | |
Mickael Bourgois | b3f96198fe | |
Mickael Bourgois | 5e2dd8cccb | |
bert-e | cd2406b827 | |
bert-e | 62f707caff | |
bert-e | f01ef00a52 | |
bert-e | 30fb64e443 | |
bert-e | 054107d8fb | |
bert-e | 848bf318fe | |
bert-e | 0beb48a1fd | |
bert-e | 618d4dffc7 | |
bert-e | b5aae192f7 | |
Mickael Bourgois | 557f3dcde6 | |
Mickael Bourgois | 3291af36bb | |
Will Toozs | d274acd8ed | |
Will Toozs | e6d9e8fc35 | |
Will Toozs | b08edefad6 | |
Will Toozs | e9c353d62a | |
Will Toozs | c7c55451a1 | |
bert-e | 7bb004586d | |
bert-e | d48de67723 | |
Will Toozs | fa4dec01cb | |
Will Toozs | 4f79a9c59c | |
Will Toozs | 05c759110b | |
Will Toozs | deae294a81 | |
Will Toozs | ab587385e6 | |
Will Toozs | 6243911072 | |
Will Toozs | da804054e5 | |
Will Toozs | 493a6da773 | |
Will Toozs | 7ecdd11783 | |
Mickael Bourgois | 7e53b67c90 | |
bert-e | b141c59bb7 | |
bert-e | 0b79ecd942 | |
bert-e | 86ece5c264 | |
Mickael Bourgois | 0b79cd6af6 | |
Mickael Bourgois | a51b5e0af3 | |
bert-e | 10ca6b98fa | |
bert-e | 171925732f | |
Taylor McKinnon | 6d36f9c867 | |
Taylor McKinnon | 1a21c4f867 | |
Taylor McKinnon | 866dec1b81 | |
Mickael Bourgois | 9491e82235 | |
bert-e | 70e8b20af9 | |
bert-e | 0ec5f4fee5 | |
bert-e | 6c468a01d9 | |
bert-e | 3d2b75f344 | |
Mickael Bourgois | 5811fa5326 | |
bert-e | e600677545 | |
bert-e | 72e5da10b7 | |
Mickael Bourgois | de0e7e6449 | |
Mickael Bourgois | 97b5ed6dd3 | |
Mickael Bourgois | dad8a3ee37 | |
Mickael Bourgois | 8aca658c5c | |
bert-e | 759817c5a0 | |
bert-e | 035c7e8d7f | |
Mickael Bourgois | b8af1225d5 | |
Mickael Bourgois | 40faa5f3fa | |
Mickael Bourgois | 1fc8622614 | |
Mickael Bourgois | a0acefb4a8 | |
bert-e | de27a5b88e | |
bert-e | a4cc5e45f3 | |
bert-e | 621cb33680 | |
bert-e | b025443d21 | |
Mickael Bourgois | d502a81284 | |
bert-e | 9a8b707e82 | |
bert-e | 002dbe0019 | |
bert-e | 59e52f6df2 | |
bert-e | b52f2356ba | |
Mickael Bourgois | 60679495b6 | |
Mickael Bourgois | 9dfacd0827 | |
Mickael Bourgois | 485ef1e9bb | |
Mickael Bourgois | 5e041ca5e7 | |
Mickael Bourgois | 52137772d9 | |
Mickael Bourgois | fcf193d033 | |
Mickael Bourgois | fb61cad786 | |
Mickael Bourgois | b6367eb2b8 | |
bert-e | d803bdcadc | |
bert-e | 4f1b8f25b7 | |
bert-e | 94363482c3 | |
bert-e | 6b0a8cb9ed | |
Will Toozs | 5dbf5d965f | |
Will Toozs | ebefc4b5b0 | |
Mickael Bourgois | ac1c75e414 | |
Mickael Bourgois | fee4f3a96e | |
bert-e | e969eeaa20 | |
bert-e | 2ee78bcf6a | |
bert-e | 64273365d5 | |
bert-e | 65c6bacd34 | |
bert-e | d60d252eaf | |
bert-e | f31fe2f2bf | |
bert-e | ee47cece90 | |
Mickael Bourgois | 7a5cddacbc | |
Mickael Bourgois | baa6203b57 | |
Mickael Bourgois | 141056637b | |
Mickael Bourgois | 0f007e0489 | |
Mickael Bourgois | 2d50a76923 | |
Mickael Bourgois | 6b4f10ae56 | |
Mickael Bourgois | 23eaf89cc3 | |
Mickael Bourgois | d6a2144508 | |
Mickael Bourgois | 40dd3f37a4 | |
Mickael Bourgois | d3307654a6 | |
Mickael Bourgois | e342a90b48 | |
williamlardier | dbda5f16a6 | |
Mickael Bourgois | d4a4825668 | |
Mickael Bourgois | 83b9e9a775 | |
Maha Benzekri | 2959c950dd | |
Maha Benzekri | 462ddf7ef1 | |
Maha Benzekri | fda42e7399 | |
Maha Benzekri | edbd6caeb4 | |
Maha Benzekri | 1befaa1f28 | |
Maha Benzekri | 0cefca831d | |
Jonathan Gramain | ea7b69e313 | |
Jonathan Gramain | 8ec1c2f2db | |
Jonathan Gramain | 3af6ca5f6d | |
Jonathan Gramain | 997d71df08 | |
Jonathan Gramain | 275ebcec5c | |
Mickael Bourgois | 8b77530b2b | |
bert-e | 43f9606598 | |
bert-e | be34e5ad59 | |
Jonathan Gramain | 5bc64ede43 | |
Jonathan Gramain | 911010376e | |
Jonathan Gramain | b5ec37b38b | |
Mickael Bourgois | 3ce869cea3 | |
Mickael Bourgois | b7960784db | |
Mickael Bourgois | 5ac10cefa8 | |
Mickael Bourgois | 2dafefd77f | |
Mickael Bourgois | 36f147b441 | |
Mickael Bourgois | 8ed447ba63 | |
bert-e | bf235f3335 | |
bert-e | 569c9f4368 | |
Nicolas Humbert | 92cf03254a | |
Nicolas Humbert | c57ae9c8ea | |
Mickael Bourgois | 5bec42d051 | |
Mickael Bourgois | f427fc9b70 | |
Mickael Bourgois | 9aad4ae3ea | |
bert-e | 1a3cb8108c | |
bert-e | 042120b17e | |
bert-e | ba4593592d | |
bert-e | 6efdb627da | |
bert-e | 5306bf0b5c | |
bert-e | 5b22819c3f | |
bert-e | 126ca3560f | |
bert-e | e5b692f3db | |
bert-e | 548ae8cd12 | |
Taylor McKinnon | 80376405df | |
Taylor McKinnon | a612e5c27c | |
Taylor McKinnon | c3b7662086 | |
Taylor McKinnon | 818b1e60d1 | |
bert-e | 2a919af071 | |
bert-e | 5c300b8b6c | |
Maha Benzekri | ad3ebd3db2 | |
Maha Benzekri | 99068e7265 | |
Maha Benzekri | cd039d8133 | |
Maha Benzekri | dd3ec25d74 | |
Maha Benzekri | 717228bdfc | |
Maha Benzekri | 836fc80560 | |
Maha Benzekri | 75b293df8d | |
Maha Benzekri | a855e38998 | |
Maha Benzekri | 51d5666bec | |
Maha Benzekri | ecb74a2db3 | |
Maha Benzekri | cdcdf8eff0 | |
Maha Benzekri | dc39b37877 | |
Maha Benzekri | 4897b3c720 | |
Maha Benzekri | ffe4ea4afe | |
Maha Benzekri | a16cfad0fc | |
bert-e | 556163e3e9 | |
Maha Benzekri | 8fe9f16661 | |
Maha Benzekri | eb9ff85bd9 | |
bert-e | 52994c0177 | |
tmacro | e109b0fca7 | |
Maha Benzekri | 9940699f9d | |
Maha Benzekri | 869d554e43 | |
Maha Benzekri | 2f8b228595 | |
Maha Benzekri | 539b2c1630 | |
Maha Benzekri | 320766e7b2 | |
Maha Benzekri | 74425d03f8 | |
Maha Benzekri | 91629a0d18 | |
Maha Benzekri | e44b7ed918 | |
Maha Benzekri | 3cb29f7f8e | |
Maha Benzekri | 4f08a4dff2 | |
Maha Benzekri | 15a1aa7965 | |
Maha Benzekri | 4470ee9125 | |
Francois Ferrand | d8c12597ea | |
Francois Ferrand | c8eb9025fa | |
Francois Ferrand | 57e0f71e6a | |
Francois Ferrand | f22f920ee2 | |
Maha Benzekri | ed1bb6301d | |
Maha Benzekri | 70dfa5b11b | |
Maha Benzekri | f17e7677fa | |
Maha Benzekri | 63b00fef55 | |
Maha Benzekri | b4f0d34abd | |
Maha Benzekri | e18f83ef0d | |
Francois Ferrand | a4e6f9d034 | |
Maha Benzekri | cf94b9de6a | |
Maha Benzekri | da0492d2bb | |
Maha Benzekri | 979b9065ed | |
Maha Benzekri | d5a3923f74 | |
Maha Benzekri | 23cbbdaaed | |
Maha Benzekri | e506dea140 | |
Maha Benzekri | 78721be7f7 | |
Maha Benzekri | 02c5a46d14 | |
Maha Benzekri | b138955ef2 | |
Maha Benzekri | 7d10e5d69e | |
bert-e | bc291fe3a7 | |
bert-e | 8dc7432c51 | |
bert-e | 040fe53e53 | |
bert-e | 60e350a5cf | |
bert-e | 5de00c80f8 | |
bert-e | 6f963bdcd9 | |
bert-e | cd9024fd32 | |
Maha Benzekri | 37649bf49b | |
Maha Benzekri | abf5ea33a9 | |
Maha Benzekri | 2596f3fda8 | |
bert-e | dff7610060 | |
bert-e | 757c2537ef | |
Maha Benzekri | c445322685 | |
bert-e | 2344204746 | |
Maha Benzekri | 693ddf8d35 | |
Maha Benzekri | 6caa5cc26a | |
bert-e | 4515b2adbf | |
bert-e | 50ffdd260b | |
Taylor McKinnon | 3836848c05 | |
Taylor McKinnon | 813a1553d2 | |
Taylor McKinnon | 1238cd809c | |
bert-e | b5f22d8c68 | |
bert-e | 68ff54d49a | |
bert-e | a74b3eacf8 | |
bert-e | f00a2f2d9e | |
bert-e | 02bb60253a | |
bert-e | 3fe5579c80 | |
bert-e | 3fdd2bce21 | |
Taylor McKinnon | 44e6eb2550 | |
Taylor McKinnon | c148c770ac | |
Maha Benzekri | fa2f877825 | |
Will Toozs | 0e323fbefe | |
bert-e | c9b512174f | |
bert-e | 7b48624cf7 | |
bert-e | 55b07def2e | |
bert-e | 62ae2b2c69 | |
bert-e | fcc9468b63 | |
bert-e | efc44a620d | |
Maha Benzekri | 72342f6654 | |
Maha Benzekri | fa11e58d57 | |
bert-e | 1bc19b39d7 | |
bert-e | b5fa3a1fd3 | |
bert-e | 68a6fc659c | |
bert-e | 2624a05018 | |
bert-e | 0882bfffb9 | |
bert-e | c0fc958365 | |
bert-e | d3c74d2c16 | |
Maha Benzekri | 9001285177 | |
bert-e | bae6e8ecb3 | |
Will Toozs | e0eab954aa | |
Will Toozs | 19b4e25373 | |
Kerkesni | 07eda89a3f | |
bert-e | 27b4066ca4 | |
bert-e | 2ee5b356fa | |
bert-e | 233955a0d3 | |
bert-e | ab51522110 | |
Rahul Padigela | b1b2d2ada6 | |
bert-e | f5d3433413 | |
bert-e | 62b4b9bc25 | |
bert-e | ce4b2b5a27 | |
bert-e | 96bd67ee60 | |
bert-e | ec56c77881 | |
bert-e | d0abde3962 | |
bert-e | f08a3f434b | |
bert-e | fdc682f2db | |
bert-e | b184606dc2 | |
bert-e | 172ec4a714 | |
bert-e | ae770d0d3f | |
bert-e | 7d2613e9a3 | |
Maha Benzekri | 9ce0f2c2b6 | |
Maha Benzekri | 43b4e0c713 | |
Maha Benzekri | 2bda761518 | |
Maha Benzekri | bfc9ca68c9 | |
Maha Benzekri | 6abb0d96a9 | |
Maha Benzekri | 733f424a4b | |
Will Toozs | 8d4ff7df5f | |
Taylor McKinnon | 59b87479df | |
Taylor McKinnon | 967ab966fa | |
Taylor McKinnon | 212c7f506c | |
Taylor McKinnon | 1e9ee0ef0b | |
bert-e | 9185f16554 | |
bert-e | 2df9a57f9c | |
Nicolas Humbert | c96706ff28 | |
Nicolas Humbert | daa6f46b14 | |
Nicolas Humbert | 44315057df | |
Nicolas Humbert | 61fe64a3ac | |
bert-e | 68535f83d6 | |
bert-e | 41d63650be | |
bert-e | 4ebb5d449a | |
bert-e | 48abedc6f7 | |
bert-e | 12185f7c3b | |
bert-e | 5f82ee2d0e | |
bert-e | 7e0f9c63fe | |
bert-e | 9f5ac17357 | |
Taylor McKinnon | d72bc5c6b9 | |
Taylor McKinnon | 0e47810963 | |
Taylor McKinnon | 8d83546ee3 | |
Taylor McKinnon | fff4fd5d22 | |
Taylor McKinnon | 1016a6826d | |
bert-e | 3b36cef85f | |
Jonathan Gramain | 114b885c7f | |
Jonathan Gramain | e56d4e3744 | |
Jonathan Gramain | 15144e4adf | |
Jonathan Gramain | 3985e2a712 | |
williamlardier | 3b95c033d2 | |
williamlardier | 04091dc316 | |
williamlardier | 56023a80ed | |
bert-e | 2deaebd89a | |
bert-e | c706ccf9c6 | |
Nicolas Humbert | 4afb2476f8 | |
Nicolas Humbert | 91a7e7f24f | |
Taylor McKinnon | 2f344cde70 | |
Taylor McKinnon | ad154085ac | |
Francois Ferrand | 583ea8490f | |
bert-e | 85a9480793 | |
bert-e | be2f65b69e | |
bert-e | 1ee6d0a87d | |
bert-e | 224af9a5d2 | |
Nicolas Humbert | 9e2ad48c5c | |
Nicolas Humbert | 780971ce10 | |
bert-e | 74f05377f0 | |
bert-e | 111e14cc89 | |
Nicolas Humbert | fd6fb5a26c | |
Nicolas Humbert | 8df540dcc1 | |
Florent Monjalet | 00b20f00d1 | |
Florent Monjalet | a91d53a12c | |
Florent Monjalet | 63d2637046 | |
Maha Benzekri | 5d416ad190 | |
Maha Benzekri | ff29cda03f | |
Maha Benzekri | 5685b2e972 | |
Maha Benzekri | 4e4ea2ab84 | |
Florent Monjalet | cb8baf2dab | |
Maha Benzekri | 67e5694d26 | |
bert-e | 22f470c6eb | |
bert-e | e510473116 | |
Maha Benzekri | d046e8a294 | |
Maha Benzekri | 20a730788a | |
Maha Benzekri | 47958591ec | |
Maha Benzekri | 4195b6ae6a | |
Maha Benzekri | feefd13b68 | |
Florent Monjalet | 17a6808fe4 | |
Florent Monjalet | df646e4802 | |
Florent Monjalet | 267770d256 | |
Florent Monjalet | 1b92dc2c05 | |
Florent Monjalet | f80bb2f34b | |
Florent Monjalet | 4f89b67bb9 | |
Florent Monjalet | 8b5630923c | |
Florent Monjalet | 9ff5e376e5 | |
Florent Monjalet | a9b5a2e3a4 | |
Florent Monjalet | 7e9ec22ae3 | |
bert-e | 9d4664ae06 | |
bert-e | 662265ba2e | |
bert-e | c7da82dda7 | |
Taylor McKinnon | 960b4b2dd4 | |
Taylor McKinnon | 1e9af343b9 | |
Taylor McKinnon | 8bb7338080 | |
Taylor McKinnon | 17e4f14f9c | |
Taylor McKinnon | 014b071536 | |
Taylor McKinnon | 9130f323d4 | |
Taylor McKinnon | c09d3282dc | |
Taylor McKinnon | fb9175579f | |
bert-e | 2d45f92ae1 | |
bert-e | 48452496fa | |
bert-e | b89773eba6 | |
bert-e | c738e0924e | |
bert-e | 18bf6b8d4a | |
bert-e | 858c31a542 | |
Nicolas Humbert | 75a759de27 | |
bert-e | 19d3e0bc9d | |
bert-e | bac044dc8f | |
Taylor McKinnon | 8f77cd18c8 | |
bert-e | cb7609b173 | |
Taylor McKinnon | 2926048735 | |
Taylor McKinnon | 656ef3fcee | |
bert-e | 8c0f709014 | |
Francois Ferrand | ce92d33a5d | |
Kerkesni | 0381cce85c | |
Kerkesni | 20a08a2a4e | |
Kerkesni | ff73d8ab12 | |
Kerkesni | 1ee44bc6d3 | |
bert-e | 614e876536 | |
bert-e | b40a77d94b | |
bert-e | 3a3a73b756 | |
Nicolas Humbert | 6789959109 | |
Nicolas Humbert | bf9b53eea9 | |
Nicolas Humbert | aa04d23e68 | |
Nicolas Humbert | e08aaa7bcc | |
Nicolas Humbert | 1d9c44126a | |
Nicolas Humbert | 70a28ab620 | |
Nicolas Humbert | 550451eefa | |
Nicolas Humbert | 96befd3f28 | |
Nicolas Humbert | 75288f1b56 | |
Nicolas Humbert | 6847f2b0c4 | |
Nicolas Humbert | 050059548e | |
Nicolas Humbert | 9b2a557a05 | |
Nicolas Humbert | 7a7e2f4b91 | |
bert-e | 3f6e85590d | |
bert-e | de589a07e8 | |
bert-e | bc009945d2 | |
bert-e | 8db04f4486 | |
bert-e | 328b7bc335 | |
bert-e | 3ac30d9bab | |
bert-e | 32204fbfbf | |
bert-e | b1eda2a73a | |
bert-e | 0249ad9bcf | |
bert-e | 5a26e1a80d | |
bert-e | 507a2d4ff5 | |
bert-e | 8cdd35950b | |
bert-e | bfa366cd27 | |
Dimitrios Vasilas | d132757696 | |
Alexander Chan | 2a4be31b8a | |
bert-e | 1207a6fb70 | |
bert-e | 5883286864 | |
bert-e | b206728342 | |
bert-e | 347a7391b9 | |
Dimitrios Vasilas | 6273eebe66 | |
bert-e | 2a37e809d9 | |
bert-e | 86ce7691cd | |
bert-e | c04f663480 | |
Dimitrios Vasilas | f2493e982f | |
bert-e | e466b5e92a | |
bert-e | a4bc10f730 | |
bert-e | e826033bf0 | |
Dimitrios Vasilas | c23dad6fb8 | |
Dimitrios Vasilas | 5fcdaa5a97 | |
Dimitrios Vasilas | 9f61ef9a3b | |
Nicolas Humbert | c480301e95 | |
Nicolas Humbert | 276be285cc | |
bert-e | 897d41392a | |
bert-e | f4e3a19d61 | |
Nicolas Humbert | ee84a03d2c | |
Nicolas Humbert | 98f855f997 | |
williamlardier | 7c52fcbbb0 | |
bert-e | da52688a39 | |
bert-e | 1cb54a66f8 | |
williamlardier | 0bb61ddb5b | |
williamlardier | 68e4b0610a | |
bert-e | d9fffdad9e | |
williamlardier | 389c32f819 | |
williamlardier | c2df0bd3eb | |
williamlardier | af0436f1cd | |
williamlardier | f7593d385e | |
williamlardier | 84068b205e | |
williamlardier | 9774d31b03 | |
Kerkesni | d26b8bcfcc | |
Kerkesni | e4634621ee | |
williamlardier | 0b58b3ad2a | |
bert-e | 652bf92536 | |
bert-e | c5b1ef63ee | |
bert-e | 227de16bca | |
williamlardier | c57a6e3c57 | |
bert-e | 344ee8a014 | |
bert-e | 5d7a434306 | |
williamlardier | 852ae72a13 | |
williamlardier | 507782bd17 | |
bert-e | b7e7f65d52 | |
williamlardier | d00320a8ba | |
williamlardier | 4cf07193d9 | |
williamlardier | aef272ea3c | |
williamlardier | 31d1734d5c | |
bert-e | c5b7450a4d | |
bert-e | eb5affdced | |
bert-e | cdaf6db929 | |
bert-e | 91ada795d0 | |
bert-e | 2b420a85e0 | |
Nicolas Humbert | 18c8d4ecac | |
Nicolas Humbert | c8150c6857 | |
bert-e | 399a2a53ab | |
Alexander Chan | bbad049b5f | |
bert-e | 2a4e2e1584 | |
bert-e | 08e43f5084 | |
Nicolas Humbert | cc153c99d6 | |
Nicolas Humbert | d3f9870389 | |
Nicolas Humbert | 0fa264693d | |
bert-e | b304d05614 | |
bert-e | 751f6ce559 | |
bert-e | 0330597679 | |
Nicolas Humbert | 27cacc9552 | |
bert-e | 004bd63368 | |
bert-e | e047ae6fbb | |
Nicolas Humbert | ebca8dd05e | |
bert-e | 52535fb44b | |
Nicolas Humbert | 1ed32b2cae | |
Nicolas Humbert | 960d736962 | |
bert-e | 11098dd113 | |
Nicolas Humbert | 9cc7362fbd | |
KillianG | 32401c9a83 | |
KillianG | 5f05b676cc | |
KillianG | fd662a8c2c | |
bert-e | a843d53939 | |
Nicolas Humbert | f889100798 | |
bert-e | 5d54dd58be | |
Nicolas Humbert | 1bd0deafcf | |
Francois Ferrand | 7c788d3dbf | |
Nicolas Humbert | 50cb6a2bf1 | |
bert-e | 58f7bb2877 | |
bert-e | f899337284 | |
bert-e | b960a913ec | |
bert-e | 5436c0698e | |
bert-e | 3ff7856a94 | |
gaspardmoindrot | 57fb5f1403 | |
Francois Ferrand | ea284508d7 | |
Francois Ferrand | 0981fa42f3 | |
Francois Ferrand | 7e63064a52 | |
Francois Ferrand | 71b21e40ca | |
Francois Ferrand | ff894bb545 | |
Francois Ferrand | ae9f24e1bb | |
bert-e | 2dc01ce3ed | |
Kerkesni | 9bd9bef6c7 | |
bert-e | 3a8bbefb6c | |
bert-e | a6a5c273d5 | |
Dimitrios Vasilas | c329d9684d | |
bert-e | ec5baf1f85 | |
Dimitrios Vasilas | d844fb4fa1 | |
Kerkesni | 6479076fec | |
bert-e | c436e2657c | |
bert-e | df45f481d0 | |
Dimitrios Vasilas | 406f3f0093 | |
Dimitrios Vasilas | 6952b91539 | |
Dimitrios Vasilas | eea1ebb5ec | |
bert-e | dae5b7dc28 | |
Dimitrios Vasilas | 1d76f61d88 | |
Dimitrios Vasilas | 8abe809141 | |
Dimitrios Vasilas | 94b14a258e | |
bert-e | cd8c589eba | |
williamlardier | daec2661ae | |
Francois Ferrand | 0f266371a0 | |
Francois Ferrand | 73e56963bf | |
Nicolas Humbert | 4c189b2d9e | |
Alexander Chan | fb11d0f42e | |
Alexander Chan | fe6690da92 | |
williamlardier | 9cbd9f7be8 | |
williamlardier | c2fc8873cb | |
Francois Ferrand | bee1ae04bf | |
Francois Ferrand | eb86552a57 | |
Alexander Chan | 80fbf78d62 | |
bert-e | f5d8f2fac5 | |
bert-e | b1e13d6efa | |
Jonathan Gramain | e7ef437b27 | |
Jonathan Gramain | 55f652ecc4 | |
Jonathan Gramain | 77f56d1fa1 | |
bert-e | 36e841b542 | |
bert-e | a2404ed622 | |
williamlardier | 1d12a430a0 | |
williamlardier | bea27b4fb4 | |
williamlardier | 76405d9179 | |
Alexander Chan | 31b7f1e71c | |
Alexander Chan | 8674cac9f8 | |
KillianG | d5b666a246 | |
KillianG | 4360772971 | |
KillianG | 6e152e33d5 | |
KillianG | 94f34979a5 | |
bert-e | 4be430c313 | |
bert-e | 4b0f165b46 | |
Nicolas Humbert | 3590377554 | |
Nicolas Humbert | f7f77c6cd2 | |
bert-e | 8a08f97492 | |
bert-e | a908d09cc8 | |
Jonathan Gramain | 170a68a4f8 | |
bert-e | 448afa50e3 | |
bert-e | a0fff19611 | |
bert-e | 6ad1643ba8 | |
Jonathan Gramain | 5ce253ef62 | |
Jonathan Gramain | 72f4c36077 | |
Jonathan Gramain | e534af856f | |
bert-e | 5dd8d9057a | |
bert-e | 50b738cfff | |
bert-e | 2be3ce21c7 | |
bert-e | 70ff6fc4ee | |
bert-e | c5214d19a6 | |
bert-e | 951a98fcaf | |
Jonathan Gramain | ebb0fed48a | |
Jonathan Gramain | 5f85c14ab9 | |
bert-e | 8ca770dcb7 | |
bert-e | 7923977300 | |
Jonathan Gramain | 3597c146d7 | |
Jonathan Gramain | c81e49ba9b | |
Jonathan Gramain | e93c064b5f | |
Jonathan Gramain | 2b3774600d | |
Jonathan Gramain | a6951f2ef8 | |
Jonathan Gramain | 9fb232861f | |
Jonathan Gramain | 6cf4e291fa | |
Jonathan Gramain | 06b4320e7d | |
bert-e | 3585b8d5eb | |
bert-e | 9331c0a375 | |
bert-e | 70f368408d | |
Jonathan Gramain | a63762ae71 | |
Jonathan Gramain | f0420572c8 | |
Jonathan Gramain | b1fd915ba3 | |
Jonathan Gramain | 4285c18e44 | |
Jonathan Gramain | 71ffd004df | |
Jonathan Gramain | f674104825 | |
Jonathan Gramain | 9c9d4b3e7c | |
Jonathan Gramain | 13265a3d6e | |
Jonathan Gramain | 31c5316a7e | |
bert-e | 0a1489ee46 | |
bert-e | 71f80544ac | |
bert-e | 270080a75b | |
bert-e | 74717b2acb | |
bert-e | ef81f3e58f | |
Jonathan Gramain | aa55a87a65 | |
Xin LI | de5b4331e2 | |
Xin LI | e1a4f1ef8c | |
bert-e | 46dff0321d | |
bert-e | f3c7580510 | |
bert-e | 2145bb3ae3 | |
Xin LI | 468162c81c | |
Xin LI | 89f9139203 | |
Xin LI | 8153554a4c | |
Xin LI | fb9063bccc | |
bert-e | ddc6ea72be | |
Nicolas Humbert | f20bf1becf | |
bert-e | d31c773e77 | |
bert-e | d266ff4e9f | |
bert-e | 6ff21996f5 | |
bert-e | 15d1b3ba86 | |
bert-e | 827c752e9a | |
Jonathan Gramain | 82dc837610 | |
bert-e | 7dc2f07cb6 | |
Kerkesni | 6c22d87c55 | |
Kerkesni | 310f67d3a7 | |
Kerkesni | 49841c5e0e | |
Kerkesni | b5334baca8 | |
Kerkesni | e592671b54 | |
bert-e | 6e0b66849d | |
Nicolas Humbert | f2292f1ca3 | |
bert-e | 18a1bfd325 | |
bert-e | c2b54702f6 | |
Jonathan Gramain | 13a5e14da5 | |
Jonathan Gramain | 891913fd16 | |
bert-e | 7baa2501e6 | |
Jonathan Gramain | 8e808afec9 | |
bert-e | 2c999f4c10 | |
bert-e | b23472a754 | |
bert-e | a4999c1bfb | |
bert-e | fe0b0f8b2f | |
Jonathan Gramain | c2bee23fd1 | |
Jonathan Gramain | e87c2a4e5f | |
Jonathan Gramain | db943cd634 | |
bert-e | bf7a643d45 | |
bert-e | 874a53c767 | |
Jonathan Gramain | c7e1c6921b | |
Jonathan Gramain | 6d2d56bc1e | |
bert-e | 3f3bf0fdf0 | |
bert-e | 1922facb7b | |
Jonathan Gramain | fff03d3320 | |
Jonathan Gramain | 6e79d3f1a4 | |
bert-e | 2a44949048 | |
bert-e | 1576352613 | |
bert-e | 74978f423e | |
bert-e | 6f4cd75d6f | |
bert-e | 00906d04f5 | |
Jonathan Gramain | 270339f2bb | |
bert-e | 6660626190 | |
Nicolas Humbert | 049f52bf95 | |
williamlardier | 58fc0b7146 | |
williamlardier | 11e3d7ecb2 | |
williamlardier | 1bab851ce3 | |
bert-e | 0bc0341f33 | |
bert-e | b5af652dc8 | |
bert-e | 6c29be5137 | |
Jonathan Gramain | 2967f327ed | |
Jonathan Gramain | 0f8a56e9b5 | |
Jonathan Gramain | c1d2601237 | |
Jonathan Gramain | 885f95606c | |
bert-e | b5b0f6482b | |
Nicolas Humbert | ec9ed94555 | |
bert-e | 755f282f8e | |
Nicolas Humbert | 41cc399d85 | |
bert-e | c4dc928de2 | |
Nicolas Humbert | 6b8a2581b6 | |
Killian Gardahaut | a0087e8d77 | |
KillianG | 8e5bea56b6 | |
KillianG | 976e349036 | |
KillianG | de1c23ac1b | |
KillianG | 0b4d04a2a3 | |
KillianG | 049d396c8d | |
Naren | 5c04cbe6d1 | |
Naren | d3e538087a | |
bert-e | 7cc37c7f3d | |
Naren | 399d081d68 | |
Naren | c3fac24366 | |
Naren | 82687aa1a7 | |
Naren | 820ada48ce | |
Naren | df73cc7ebc | |
Naren | 429c62087d | |
Naren | 13fa26986d | |
bert-e | 5cb63991a8 | |
Naren | d5b336d1d9 | |
bert-e | 750223500d | |
Naren | 23ffbf77d2 | |
Naren | 6ea18bcef4 | |
Naren | c45dac7ffc | |
Naren | 878fc6819f | |
Naren | 43592f9392 | |
Naren | dbd1383c32 | |
Alexander Chan | c310cb3dd1 | |
bert-e | 7fe0e2891b | |
bert-e | 93442fed68 | |
Alexander Chan | 21612cfadd | |
bert-e | 644062f088 | |
Alexander Chan | d0eb81539e | |
bert-e | 22cda51944 | |
williamlardier | 408d0de732 | |
williamlardier | 83916c91fb | |
bert-e | 110b2a35ed | |
bert-e | 3b5f5875f3 | |
bert-e | bdaf92023f | |
bert-e | 25d1cd9601 | |
bert-e | 91c9eb6faa | |
Jonathan Gramain | 6306cf7fc7 | |
williamlardier | a8117ca037 | |
bert-e | 9145d1cf79 | |
bert-e | 0fb54c9d31 | |
bert-e | 63dc33a339 | |
bert-e | 49d46dfe04 | |
bert-e | 4bb331392e | |
bert-e | ae1b6dc3d1 | |
bert-e | 162157580f | |
bert-e | 4e4435d82e | |
Alexander Chan | b0db1f9a94 | |
Alexander Chan | 35d269c27c | |
bert-e | b1304b5f7f | |
bert-e | c355422a7e | |
bert-e | d44334ad22 | |
bert-e | 6e9c50eeba | |
bert-e | 6c7be8892c | |
bert-e | 82df91debb | |
bert-e | 6b1f8c61ec | |
bert-e | a12d44dc18 | |
bert-e | d5ec32fc5c | |
Jonathan Gramain | e16da9ab11 | |
Jonathan Gramain | d43e8d01bf | |
bert-e | 335bfabed1 | |
bert-e | 3b92eaaef2 | |
bert-e | a6fd8b2261 | |
bert-e | 00ab8d482d | |
bert-e | 29551f7edf | |
Alexander Chan | 7dd022f6cb | |
bert-e | 3398db3c0f | |
bert-e | 00a793be6e | |
bert-e | 68bb824b57 | |
Jonathan Gramain | 432680841e | |
bert-e | b2641f5c1b | |
Jonathan Gramain | 66c34e0272 | |
bert-e | 836e9fb22d | |
bert-e | 9bc7fa49ea | |
bert-e | e3087fb940 | |
Dimitrios Vasilas | 67e126320c | |
Dimitrios Vasilas | 66520571d3 | |
bert-e | ead7f5f7c2 | |
bert-e | fe636d22fc | |
bert-e | 6530e70761 | |
bert-e | 6d14bda3ed | |
bert-e | 416634cf11 | |
Dimitrios Vasilas | fd669664a6 | |
bert-e | c17059dc77 | |
bert-e | b4617f1362 | |
bert-e | 624d4708cf | |
bert-e | 95c180e9d9 | |
bert-e | 5a2b465d0f | |
Dimitrios Vasilas | 2cd10e7195 | |
bert-e | 8ace5b24a5 | |
bert-e | 4b1dcd531d | |
bert-e | 13ef509cbc | |
Dimitrios Vasilas | d4feda7bbd | |
bert-e | 39f7035dbd | |
bert-e | 7d3ab342f6 | |
Dimitrios Vasilas | af60df4caf | |
Dimitrios Vasilas | 2acd7348d4 | |
bert-e | ffafe6ecfc | |
Dimitrios Vasilas | 4301fc57e2 | |
Dimitrios Vasilas | 072d8324ca | |
Dimitrios Vasilas | 25276dae3f | |
Dimitrios Vasilas | bdeeb25d19 | |
Dimitrios Vasilas | 5dc17db9df |
|
@ -1,5 +1,8 @@
|
|||
{
|
||||
"extends": "scality",
|
||||
"plugins": [
|
||||
"mocha"
|
||||
],
|
||||
"rules": {
|
||||
"import/extensions": "off",
|
||||
"lines-around-directive": "off",
|
||||
|
@ -42,7 +45,8 @@
|
|||
"no-restricted-properties": "off",
|
||||
"new-parens": "off",
|
||||
"no-multi-spaces": "off",
|
||||
"quote-props": "off"
|
||||
"quote-props": "off",
|
||||
"mocha/no-exclusive-tests": "error",
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020
|
||||
|
|
|
@ -16,30 +16,28 @@ runs:
|
|||
run: |-
|
||||
set -exu;
|
||||
mkdir -p /tmp/artifacts/${JOB_NAME}/;
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'yarn'
|
||||
- name: install dependencies
|
||||
shell: bash
|
||||
run: yarn install --ignore-engines --frozen-lockfile --network-concurrency 1
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: |
|
||||
2.7
|
||||
3.9
|
||||
- name: Install python deps
|
||||
shell: bash
|
||||
run: pip install docker-compose
|
||||
python-version: 3.9
|
||||
- name: Setup python2 test environment
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get install -y libdigest-hmac-perl
|
||||
pip install virtualenv
|
||||
virtualenv -p $(which python2) ~/.virtualenv/py2
|
||||
source ~/.virtualenv/py2/bin/activate
|
||||
pip install 's3cmd==1.6.1'
|
||||
pip install 's3cmd==2.3.0'
|
||||
- name: fix sproxyd.conf permissions
|
||||
shell: bash
|
||||
run: sudo chown root:root .github/docker/sproxyd/conf/sproxyd0.conf
|
||||
- name: ensure fuse kernel module is loaded (for sproxyd)
|
||||
shell: bash
|
||||
run: sudo modprobe fuse
|
||||
|
|
|
@ -39,6 +39,12 @@ services:
|
|||
- MONGODB_RS=rs0
|
||||
- DEFAULT_BUCKET_KEY_FORMAT
|
||||
- METADATA_MAX_CACHED_BUCKETS
|
||||
- ENABLE_NULL_VERSION_COMPAT_MODE
|
||||
- SCUBA_HOST
|
||||
- SCUBA_PORT
|
||||
- SCUBA_HEALTHCHECK_FREQUENCY
|
||||
- S3QUOTA
|
||||
- QUOTA_ENABLE_INFLIGHTS
|
||||
env_file:
|
||||
- creds.env
|
||||
depends_on:
|
||||
|
@ -66,14 +72,21 @@ services:
|
|||
pykmip:
|
||||
network_mode: "host"
|
||||
profiles: ['pykmip']
|
||||
image: registry.scality.com/cloudserver-dev/pykmip
|
||||
image: ${PYKMIP_IMAGE:-ghcr.io/scality/cloudserver/pykmip}
|
||||
volumes:
|
||||
- /tmp/artifacts/${JOB_NAME}:/artifacts
|
||||
mongo:
|
||||
network_mode: "host"
|
||||
profiles: ['mongo', 'ceph']
|
||||
image: scality/ci-mongo:3.6.8
|
||||
image: ${MONGODB_IMAGE}
|
||||
ceph:
|
||||
network_mode: "host"
|
||||
profiles: ['ceph']
|
||||
image: ghcr.io/scality/cloudserver/ci-ceph
|
||||
sproxyd:
|
||||
network_mode: "host"
|
||||
profiles: ['sproxyd']
|
||||
image: sproxyd-standalone
|
||||
build: ./sproxyd
|
||||
user: 0:0
|
||||
privileged: yes
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
FROM mongo:5.0.21
|
||||
|
||||
ENV USER=scality \
|
||||
HOME_DIR=/home/scality \
|
||||
CONF_DIR=/conf \
|
||||
DATA_DIR=/data
|
||||
|
||||
# Set up directories and permissions
|
||||
RUN mkdir -p /data/db /data/configdb && chown -R mongodb:mongodb /data/db /data/configdb; \
|
||||
mkdir /logs; \
|
||||
adduser --uid 1000 --disabled-password --gecos --quiet --shell /bin/bash scality
|
||||
|
||||
# Set up environment variables and directories for scality user
|
||||
RUN mkdir ${CONF_DIR} && \
|
||||
chown -R ${USER} ${CONF_DIR} && \
|
||||
chown -R ${USER} ${DATA_DIR}
|
||||
|
||||
# copy the mongo config file
|
||||
COPY /conf/mongod.conf /conf/mongod.conf
|
||||
COPY /conf/mongo-run.sh /conf/mongo-run.sh
|
||||
COPY /conf/initReplicaSet /conf/initReplicaSet.js
|
||||
|
||||
EXPOSE 27017/tcp
|
||||
EXPOSE 27018
|
||||
|
||||
# Set up CMD
|
||||
ENTRYPOINT ["bash", "/conf/mongo-run.sh"]
|
||||
CMD ["bash", "/conf/mongo-run.sh"]
|
|
@ -0,0 +1,4 @@
|
|||
rs.initiate({
|
||||
_id: "rs0",
|
||||
members: [{ _id: 0, host: "127.0.0.1:27018" }]
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/bash
|
||||
set -exo pipefail
|
||||
|
||||
init_RS() {
|
||||
sleep 5
|
||||
mongo --port 27018 /conf/initReplicaSet.js
|
||||
}
|
||||
init_RS &
|
||||
|
||||
mongod --bind_ip_all --config=/conf/mongod.conf
|
|
@ -0,0 +1,15 @@
|
|||
storage:
|
||||
journal:
|
||||
enabled: true
|
||||
engine: wiredTiger
|
||||
dbPath: "/data/db"
|
||||
processManagement:
|
||||
fork: false
|
||||
net:
|
||||
port: 27018
|
||||
bindIp: 0.0.0.0
|
||||
replication:
|
||||
replSetName: "rs0"
|
||||
enableMajorityReadConcern: true
|
||||
security:
|
||||
authorization: disabled
|
|
@ -0,0 +1,3 @@
|
|||
FROM ghcr.io/scality/federation/sproxyd:7.10.6.8
|
||||
ADD ./conf/supervisord.conf ./conf/nginx.conf ./conf/fastcgi_params ./conf/sproxyd0.conf /conf/
|
||||
RUN chown root:root /conf/sproxyd0.conf
|
|
@ -0,0 +1,26 @@
|
|||
fastcgi_param QUERY_STRING $query_string;
|
||||
fastcgi_param REQUEST_METHOD $request_method;
|
||||
fastcgi_param CONTENT_TYPE $content_type;
|
||||
fastcgi_param CONTENT_LENGTH $content_length;
|
||||
|
||||
#fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||
fastcgi_param SCRIPT_NAME /var/www;
|
||||
fastcgi_param PATH_INFO $document_uri;
|
||||
|
||||
fastcgi_param REQUEST_URI $request_uri;
|
||||
fastcgi_param DOCUMENT_URI $document_uri;
|
||||
fastcgi_param DOCUMENT_ROOT $document_root;
|
||||
fastcgi_param SERVER_PROTOCOL $server_protocol;
|
||||
fastcgi_param HTTPS $https if_not_empty;
|
||||
|
||||
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
|
||||
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
|
||||
|
||||
fastcgi_param REMOTE_ADDR $remote_addr;
|
||||
fastcgi_param REMOTE_PORT $remote_port;
|
||||
fastcgi_param SERVER_ADDR $server_addr;
|
||||
fastcgi_param SERVER_PORT $server_port;
|
||||
fastcgi_param SERVER_NAME $server_name;
|
||||
|
||||
# PHP only, required if PHP was built with --enable-force-cgi-redirect
|
||||
fastcgi_param REDIRECT_STATUS 200;
|
|
@ -0,0 +1,88 @@
|
|||
worker_processes 1;
|
||||
error_log /logs/error.log;
|
||||
user root root;
|
||||
events {
|
||||
worker_connections 1000;
|
||||
reuse_port on;
|
||||
multi_accept on;
|
||||
}
|
||||
worker_rlimit_nofile 20000;
|
||||
http {
|
||||
root /var/www/;
|
||||
upstream sproxyds {
|
||||
least_conn;
|
||||
keepalive 40;
|
||||
server 127.0.0.1:20000;
|
||||
}
|
||||
server {
|
||||
client_max_body_size 0;
|
||||
client_body_timeout 150;
|
||||
client_header_timeout 150;
|
||||
postpone_output 0;
|
||||
client_body_postpone_size 0;
|
||||
keepalive_requests 1100;
|
||||
keepalive_timeout 300s;
|
||||
server_tokens off;
|
||||
default_type application/octet-stream;
|
||||
gzip off;
|
||||
tcp_nodelay on;
|
||||
tcp_nopush on;
|
||||
sendfile on;
|
||||
listen 81;
|
||||
server_name localhost;
|
||||
rewrite ^/arc/(.*)$ /dc1/$1 permanent;
|
||||
location ~* ^/proxy/(.*)$ {
|
||||
rewrite ^/proxy/(.*)$ /$1 last;
|
||||
}
|
||||
allow 127.0.0.1;
|
||||
|
||||
deny all;
|
||||
set $usermd '-';
|
||||
set $sentusermd '-';
|
||||
set $elapsed_ms '-';
|
||||
set $now '-';
|
||||
log_by_lua '
|
||||
if not(ngx.var.http_x_scal_usermd == nil) and string.len(ngx.var.http_x_scal_usermd) > 2 then
|
||||
ngx.var.usermd = string.sub(ngx.decode_base64(ngx.var.http_x_scal_usermd),1,-3)
|
||||
end
|
||||
if not(ngx.var.sent_http_x_scal_usermd == nil) and string.len(ngx.var.sent_http_x_scal_usermd) > 2 then
|
||||
ngx.var.sentusermd = string.sub(ngx.decode_base64(ngx.var.sent_http_x_scal_usermd),1,-3)
|
||||
end
|
||||
local elapsed_ms = tonumber(ngx.var.request_time)
|
||||
if not ( elapsed_ms == nil) then
|
||||
elapsed_ms = elapsed_ms * 1000
|
||||
ngx.var.elapsed_ms = tostring(elapsed_ms)
|
||||
end
|
||||
local time = tonumber(ngx.var.msec) * 1000
|
||||
ngx.var.now = time
|
||||
';
|
||||
log_format irm '{ "time":"$now","connection":"$connection","request":"$connection_requests","hrtime":"$msec",'
|
||||
'"httpMethod":"$request_method","httpURL":"$uri","elapsed_ms":$elapsed_ms,'
|
||||
'"httpCode":$status,"requestLength":$request_length,"bytesSent":$bytes_sent,'
|
||||
'"contentLength":"$content_length","sentContentLength":"$sent_http_content_length",'
|
||||
'"contentType":"$content_type","s3Address":"$remote_addr",'
|
||||
'"requestUserMd":"$usermd","responseUserMd":"$sentusermd",'
|
||||
'"ringKeyVersion":"$sent_http_x_scal_version","ringStatus":"$sent_http_x_scal_ring_status",'
|
||||
'"s3Port":"$remote_port","sproxydStatus":"$upstream_status","req_id":"$http_x_scal_request_uids",'
|
||||
'"ifMatch":"$http_if_match","ifNoneMatch":"$http_if_none_match",'
|
||||
'"range":"$http_range","contentRange":"$sent_http_content_range","nginxPID":$PID,'
|
||||
'"sproxydAddress":"$upstream_addr","sproxydResponseTime_s":"$upstream_response_time" }';
|
||||
access_log /dev/stdout irm;
|
||||
error_log /dev/stdout error;
|
||||
location / {
|
||||
proxy_request_buffering off;
|
||||
fastcgi_request_buffering off;
|
||||
fastcgi_no_cache 1;
|
||||
fastcgi_cache_bypass 1;
|
||||
fastcgi_buffering off;
|
||||
fastcgi_ignore_client_abort on;
|
||||
fastcgi_keep_conn on;
|
||||
include fastcgi_params;
|
||||
fastcgi_pass sproxyds;
|
||||
fastcgi_next_upstream error timeout;
|
||||
fastcgi_send_timeout 285s;
|
||||
fastcgi_read_timeout 285s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"general": {
|
||||
"ring": "DATA",
|
||||
"port": 20000,
|
||||
"syslog_facility": "local0"
|
||||
},
|
||||
"ring_driver:0": {
|
||||
"alias": "dc1",
|
||||
"type": "local",
|
||||
"queue_path": "/tmp/ring-objs"
|
||||
},
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
[supervisord]
|
||||
nodaemon = true
|
||||
loglevel = info
|
||||
logfile = %(ENV_LOG_DIR)s/supervisord.log
|
||||
pidfile = %(ENV_SUP_RUN_DIR)s/supervisord.pid
|
||||
logfile_maxbytes = 20MB
|
||||
logfile_backups = 2
|
||||
|
||||
[unix_http_server]
|
||||
file = %(ENV_SUP_RUN_DIR)s/supervisor.sock
|
||||
|
||||
[rpcinterface:supervisor]
|
||||
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||
|
||||
[supervisorctl]
|
||||
serverurl = unix://%(ENV_SUP_RUN_DIR)s/supervisor.sock
|
||||
|
||||
[program:nginx]
|
||||
directory=%(ENV_SUP_RUN_DIR)s
|
||||
command=bash -c "/usr/sbin/nginx -c %(ENV_CONF_DIR)s/nginx.conf -g 'daemon off;'"
|
||||
stdout_logfile = %(ENV_LOG_DIR)s/%(program_name)s-%(process_num)s.log
|
||||
stderr_logfile = %(ENV_LOG_DIR)s/%(program_name)s-%(process_num)s-stderr.log
|
||||
stdout_logfile_maxbytes=100MB
|
||||
stdout_logfile_backups=7
|
||||
stderr_logfile_maxbytes=100MB
|
||||
stderr_logfile_backups=7
|
||||
autorestart=true
|
||||
autostart=true
|
||||
user=root
|
||||
|
||||
[program:sproxyd]
|
||||
directory=%(ENV_SUP_RUN_DIR)s
|
||||
process_name=%(program_name)s-%(process_num)s
|
||||
numprocs=1
|
||||
numprocs_start=0
|
||||
command=/usr/bin/sproxyd -dlw -V127 -c %(ENV_CONF_DIR)s/sproxyd%(process_num)s.conf -P /run%(process_num)s
|
||||
stdout_logfile = %(ENV_LOG_DIR)s/%(program_name)s-%(process_num)s.log
|
||||
stdout_logfile_maxbytes=100MB
|
||||
stdout_logfile_backups=7
|
||||
redirect_stderr=true
|
||||
autorestart=true
|
||||
autostart=true
|
||||
user=root
|
|
@ -1,7 +1,10 @@
|
|||
name: Test alerts
|
||||
|
||||
on:
|
||||
push
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'development/**'
|
||||
- 'q/*/**'
|
||||
|
||||
jobs:
|
||||
run-alert-tests:
|
||||
|
@ -17,13 +20,16 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Render and test ${{ matrix.tests.name }}
|
||||
uses: scality/action-prom-render-test@1.0.1
|
||||
uses: scality/action-prom-render-test@1.0.3
|
||||
with:
|
||||
alert_file_path: monitoring/alerts.yaml
|
||||
test_file_path: ${{ matrix.tests.file }}
|
||||
alert_inputs: >-
|
||||
namespace=zenko,service=artesca-data-connector-s3api-metrics,replicas=3
|
||||
alert_inputs: |
|
||||
namespace=zenko
|
||||
service=artesca-data-connector-s3api-metrics
|
||||
reportJob=artesca-data-ops-report-handler
|
||||
replicas=3
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
name: codeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [w/**, q/*]
|
||||
pull_request:
|
||||
branches: [development/*, stabilization/*, hotfix/*]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Static analysis with CodeQL
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: javascript, python, ruby
|
||||
|
||||
- name: Build and analyze
|
||||
uses: github/codeql-action/analyze@v3
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
name: dependency review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [development/*, stabilization/*, hotfix/*]
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
name: release
|
||||
run-name: release ${{ inputs.tag }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
@ -9,58 +10,69 @@ on:
|
|||
required: true
|
||||
|
||||
env:
|
||||
REGISTRY_NAME: registry.scality.com
|
||||
PROJECT_NAME: ${{ github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
build-federation-image:
|
||||
uses: scality/workflows/.github/workflows/docker-build.yaml@v1
|
||||
secrets: inherit
|
||||
with:
|
||||
push: true
|
||||
registry: registry.scality.com
|
||||
namespace: ${{ github.event.repository.name }}
|
||||
name: ${{ github.event.repository.name }}
|
||||
context: .
|
||||
file: images/svc-base/Dockerfile
|
||||
tag: ${{ github.event.inputs.tag }}-svc-base
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to GitHub Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ github.token }}
|
||||
- name: Build and push image for federation
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
file: images/svc-base/Dockerfile
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}:${{ github.event.inputs.tag }}-svc-base
|
||||
cache-from: type=gha,scope=federation
|
||||
cache-to: type=gha,mode=max,scope=federation
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildk
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY_NAME }}
|
||||
username: ${{ secrets.REGISTRY_LOGIN }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Push dashboards into the production namespace
|
||||
run: |
|
||||
oras push ${{ env.REGISTRY_NAME }}/${{ env.PROJECT_NAME }}/${{ env.PROJECT_NAME }}-dashboards:${{ github.event.inputs.tag }} \
|
||||
oras push ghcr.io/${{ github.repository }}/${{ env.PROJECT_NAME }}-dashboards:${{ github.event.inputs.tag }} \
|
||||
dashboard.json:application/grafana-dashboard+json \
|
||||
alerts.yaml:application/prometheus-alerts+yaml
|
||||
working-directory: monitoring
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY_NAME }}/${{ env.PROJECT_NAME }}/${{ env.PROJECT_NAME }}:${{ github.event.inputs.tag }}
|
||||
tags: ghcr.io/${{ github.repository }}:${{ github.event.inputs.tag }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
name: Release ${{ github.event.inputs.tag }}
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
name: tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'development/**'
|
||||
|
@ -65,23 +67,24 @@ env:
|
|||
ENABLE_LOCAL_CACHE: "true"
|
||||
REPORT_TOKEN: "report-token-1"
|
||||
REMOTE_MANAGEMENT_DISABLE: "1"
|
||||
|
||||
# https://github.com/git-lfs/git-lfs/issues/5749
|
||||
GIT_CLONE_PROTECTION_ACTIVE: 'false'
|
||||
jobs:
|
||||
linting-coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: yarn
|
||||
- name: install dependencies
|
||||
run: yarn install --frozen-lockfile --network-concurrency 1
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip
|
||||
|
@ -114,7 +117,7 @@ jobs:
|
|||
find . -name "*junit*.xml" -exec cp {} artifacts/junit/ ";"
|
||||
if: always()
|
||||
- name: Upload files to artifacts
|
||||
uses: scality/action-artifacts@v2
|
||||
uses: scality/action-artifacts@v4
|
||||
with:
|
||||
method: upload
|
||||
url: https://artifacts.scality.net
|
||||
|
@ -125,60 +128,88 @@ jobs:
|
|||
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to GitHub Registry
|
||||
uses: docker/login-action@v1.10.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Login to Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: registry.scality.com
|
||||
username: ${{ secrets.REGISTRY_LOGIN }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
password: ${{ github.token }}
|
||||
- name: Build and push cloudserver image
|
||||
uses: docker/build-push-action@v2.7.0
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
provenance: false
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}/cloudserver:${{ github.sha }}
|
||||
registry.scality.com/cloudserver-dev/cloudserver:${{ github.sha }}
|
||||
ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
labels: |
|
||||
git.repository=${{ github.repository }}
|
||||
git.commit-sha=${{ github.sha }}
|
||||
cache-from: type=gha,scope=cloudserver
|
||||
cache-to: type=gha,mode=max,scope=cloudserver
|
||||
- name: Build and push pykmip image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: true
|
||||
context: .github/pykmip
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}/pykmip:${{ github.sha }}
|
||||
labels: |
|
||||
git.repository=${{ github.repository }}
|
||||
git.commit-sha=${{ github.sha }}
|
||||
cache-from: type=gha,scope=pykmip
|
||||
cache-to: type=gha,mode=max,scope=pykmip
|
||||
- name: Build and push MongoDB
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: true
|
||||
context: .github/docker/mongodb
|
||||
tags: ghcr.io/${{ github.repository }}/ci-mongodb:${{ github.sha }}
|
||||
cache-from: type=gha,scope=mongodb
|
||||
cache-to: type=gha,mode=max,scope=mongodb
|
||||
|
||||
multiple-backend:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}/cloudserver:${{ github.sha }}
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
MONGODB_IMAGE: ghcr.io/${{ github.repository }}/ci-mongodb:${{ github.sha }}
|
||||
S3BACKEND: mem
|
||||
S3_LOCATION_FILE: /usr/src/app/tests/locationConfig/locationConfigTests.json
|
||||
S3DATA: multiple
|
||||
JOB_NAME: ${{ github.job }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Login to Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ github.token }}
|
||||
- name: Setup CI environment
|
||||
uses: ./.github/actions/setup-ci
|
||||
- name: Setup CI services
|
||||
run: docker-compose up -d
|
||||
run: docker compose --profile sproxyd up -d
|
||||
working-directory: .github/docker
|
||||
- name: Run multiple backend test
|
||||
run: |-
|
||||
set -o pipefail;
|
||||
bash wait_for_local_port.bash 8000 40
|
||||
bash wait_for_local_port.bash 81 40
|
||||
yarn run multiple_backend_test | tee /tmp/artifacts/${{ github.job }}/tests.log
|
||||
env:
|
||||
S3_LOCATION_FILE: tests/locationConfig/locationConfigTests.json
|
||||
- name: Upload logs to artifacts
|
||||
uses: scality/action-artifacts@v3
|
||||
uses: scality/action-artifacts@v4
|
||||
with:
|
||||
method: upload
|
||||
url: https://artifacts.scality.net
|
||||
|
@ -197,26 +228,26 @@ jobs:
|
|||
S3KMS: file
|
||||
S3_LOCATION_FILE: /usr/src/app/tests/locationConfig/locationConfigTests.json
|
||||
DEFAULT_BUCKET_KEY_FORMAT: v0
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}/cloudserver:${{ github.sha }}
|
||||
MONGODB_IMAGE: ghcr.io/${{ github.repository }}/ci-mongodb:${{ github.sha }}
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
JOB_NAME: ${{ github.job }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup CI environment
|
||||
uses: ./.github/actions/setup-ci
|
||||
- name: Setup CI services
|
||||
run: docker-compose --profile mongo up -d
|
||||
run: docker compose --profile mongo up -d
|
||||
working-directory: .github/docker
|
||||
- name: Run functional tests
|
||||
run: |-
|
||||
set -o pipefail;
|
||||
source ~/.virtualenv/py2/bin/activate
|
||||
bash wait_for_local_port.bash 8000 40
|
||||
yarn run ft_test | tee /tmp/artifacts/${{ github.job }}/tests.log
|
||||
env:
|
||||
S3_LOCATION_FILE: tests/locationConfig/locationConfigTests.json
|
||||
- name: Upload logs to artifacts
|
||||
uses: scality/action-artifacts@v3
|
||||
uses: scality/action-artifacts@v4
|
||||
with:
|
||||
method: upload
|
||||
url: https://artifacts.scality.net
|
||||
|
@ -236,27 +267,27 @@ jobs:
|
|||
S3_LOCATION_FILE: /usr/src/app/tests/locationConfig/locationConfigTests.json
|
||||
DEFAULT_BUCKET_KEY_FORMAT: v1
|
||||
METADATA_MAX_CACHED_BUCKETS: 1
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}/cloudserver:${{ github.sha }}
|
||||
MONGODB_IMAGE: ghcr.io/${{ github.repository }}/ci-mongodb:${{ github.sha }}
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
JOB_NAME: ${{ github.job }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup CI environment
|
||||
uses: ./.github/actions/setup-ci
|
||||
- name: Setup CI services
|
||||
run: docker-compose --profile mongo up -d
|
||||
run: docker compose --profile mongo up -d
|
||||
working-directory: .github/docker
|
||||
- name: Run functional tests
|
||||
run: |-
|
||||
set -o pipefail;
|
||||
source ~/.virtualenv/py2/bin/activate
|
||||
set -o pipefail;
|
||||
bash wait_for_local_port.bash 8000 40
|
||||
yarn run ft_test | tee /tmp/artifacts/${{ github.job }}/tests.log
|
||||
yarn run ft_mixed_bucket_format_version | tee /tmp/artifacts/${{ github.job }}/mixed-tests.log
|
||||
env:
|
||||
S3_LOCATION_FILE: tests/locationConfig/locationConfigTests.json
|
||||
- name: Upload logs to artifacts
|
||||
uses: scality/action-artifacts@v3
|
||||
uses: scality/action-artifacts@v4
|
||||
with:
|
||||
method: upload
|
||||
url: https://artifacts.scality.net
|
||||
|
@ -266,30 +297,40 @@ jobs:
|
|||
if: always()
|
||||
|
||||
file-ft-tests:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- job-name: file-ft-tests
|
||||
name: ${{ matrix.job-name }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
S3BACKEND: file
|
||||
S3VAULT: mem
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}/cloudserver:${{ github.sha }}
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
MONGODB_IMAGE: ghcr.io/${{ github.repository }}/ci-mongodb:${{ github.sha }}
|
||||
MPU_TESTING: "yes"
|
||||
JOB_NAME: ${{ github.job }}
|
||||
JOB_NAME: ${{ matrix.job-name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup CI environment
|
||||
uses: ./.github/actions/setup-ci
|
||||
- name: Setup matrix job artifacts directory
|
||||
shell: bash
|
||||
run: |
|
||||
set -exu
|
||||
mkdir -p /tmp/artifacts/${{ matrix.job-name }}/
|
||||
- name: Setup CI services
|
||||
run: docker-compose up -d
|
||||
run: docker compose up -d
|
||||
working-directory: .github/docker
|
||||
- name: Run file ft tests
|
||||
run: |-
|
||||
set -o pipefail;
|
||||
bash wait_for_local_port.bash 8000 40
|
||||
source ~/.virtualenv/py2/bin/activate
|
||||
yarn run ft_test | tee /tmp/artifacts/${{ github.job }}/tests.log
|
||||
yarn run ft_test | tee /tmp/artifacts/${{ matrix.job-name }}/tests.log
|
||||
- name: Upload logs to artifacts
|
||||
uses: scality/action-artifacts@v3
|
||||
uses: scality/action-artifacts@v4
|
||||
with:
|
||||
method: upload
|
||||
url: https://artifacts.scality.net
|
||||
|
@ -303,17 +344,18 @@ jobs:
|
|||
needs: build
|
||||
env:
|
||||
ENABLE_UTAPI_V2: t
|
||||
S3BACKEND: mem
|
||||
S3BACKEND: mem
|
||||
BUCKET_DENY_FILTER: utapi-event-filter-deny-bucket
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}/cloudserver:${{ github.sha }}
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
MONGODB_IMAGE: ghcr.io/${{ github.repository }}/ci-mongodb:${{ github.sha }}
|
||||
JOB_NAME: ${{ github.job }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup CI environment
|
||||
uses: ./.github/actions/setup-ci
|
||||
- name: Setup CI services
|
||||
run: docker-compose up -d
|
||||
run: docker compose up -d
|
||||
working-directory: .github/docker
|
||||
- name: Run file utapi v2 tests
|
||||
run: |-
|
||||
|
@ -321,7 +363,51 @@ jobs:
|
|||
bash wait_for_local_port.bash 8000 40
|
||||
yarn run test_utapi_v2 | tee /tmp/artifacts/${{ github.job }}/tests.log
|
||||
- name: Upload logs to artifacts
|
||||
uses: scality/action-artifacts@v3
|
||||
uses: scality/action-artifacts@v4
|
||||
with:
|
||||
method: upload
|
||||
url: https://artifacts.scality.net
|
||||
user: ${{ secrets.ARTIFACTS_USER }}
|
||||
password: ${{ secrets.ARTIFACTS_PASSWORD }}
|
||||
source: /tmp/artifacts
|
||||
if: always()
|
||||
|
||||
quota-tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
matrix:
|
||||
inflights:
|
||||
- name: "With Inflights"
|
||||
value: "true"
|
||||
- name: "Without Inflights"
|
||||
value: "false"
|
||||
env:
|
||||
S3METADATA: mongodb
|
||||
S3BACKEND: mem
|
||||
S3QUOTA: scuba
|
||||
QUOTA_ENABLE_INFLIGHTS: ${{ matrix.inflights.value }}
|
||||
SCUBA_HOST: localhost
|
||||
SCUBA_PORT: 8100
|
||||
SCUBA_HEALTHCHECK_FREQUENCY: 100
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
MONGODB_IMAGE: ghcr.io/${{ github.repository }}/ci-mongodb:${{ github.sha }}
|
||||
JOB_NAME: ${{ github.job }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup CI environment
|
||||
uses: ./.github/actions/setup-ci
|
||||
- name: Setup CI services
|
||||
run: docker compose --profile mongo up -d
|
||||
working-directory: .github/docker
|
||||
- name: Run quota tests
|
||||
run: |-
|
||||
set -ex -o pipefail;
|
||||
bash wait_for_local_port.bash 8000 40
|
||||
yarn run test_quota | tee /tmp/artifacts/${{ github.job }}/tests.log
|
||||
- name: Upload logs to artifacts
|
||||
uses: scality/action-artifacts@v4
|
||||
with:
|
||||
method: upload
|
||||
url: https://artifacts.scality.net
|
||||
|
@ -337,18 +423,20 @@ jobs:
|
|||
S3BACKEND: file
|
||||
S3VAULT: mem
|
||||
MPU_TESTING: "yes"
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}/cloudserver:${{ github.sha }}
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
PYKMIP_IMAGE: ghcr.io/${{ github.repository }}/pykmip:${{ github.sha }}
|
||||
MONGODB_IMAGE: ghcr.io/${{ github.repository }}/ci-mongodb:${{ github.sha }}
|
||||
JOB_NAME: ${{ github.job }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup CI environment
|
||||
uses: ./.github/actions/setup-ci
|
||||
- name: Copy KMIP certs
|
||||
run: cp -r ./certs /tmp/ssl-kmip
|
||||
working-directory: .github/pykmip
|
||||
- name: Setup CI services
|
||||
run: docker-compose --profile pykmip up -d
|
||||
run: docker compose --profile pykmip up -d
|
||||
working-directory: .github/docker
|
||||
- name: Run file KMIP tests
|
||||
run: |-
|
||||
|
@ -357,7 +445,7 @@ jobs:
|
|||
bash wait_for_local_port.bash 5696 40
|
||||
yarn run ft_kmip | tee /tmp/artifacts/${{ github.job }}/tests.log
|
||||
- name: Upload logs to artifacts
|
||||
uses: scality/action-artifacts@v3
|
||||
uses: scality/action-artifacts@v4
|
||||
with:
|
||||
method: upload
|
||||
url: https://artifacts.scality.net
|
||||
|
@ -365,7 +453,7 @@ jobs:
|
|||
password: ${{ secrets.ARTIFACTS_PASSWORD }}
|
||||
source: /tmp/artifacts
|
||||
if: always()
|
||||
|
||||
|
||||
ceph-backend-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
@ -376,30 +464,31 @@ jobs:
|
|||
CI_CEPH: 'true'
|
||||
MPU_TESTING: "yes"
|
||||
S3_LOCATION_FILE: /usr/src/app/tests/locationConfig/locationConfigCeph.json
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}/cloudserver:${{ github.sha }}
|
||||
MONGODB_IMAGE: ghcr.io/${{ github.repository }}/ci-mongodb:${{ github.sha }}
|
||||
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
JOB_NAME: ${{ github.job }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Login to GitHub Registry
|
||||
uses: docker/login-action@v1.10.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
password: ${{ github.token }}
|
||||
- name: Setup CI environment
|
||||
uses: ./.github/actions/setup-ci
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '2.5.0'
|
||||
ruby-version: '2.5.9'
|
||||
- name: Install Ruby dependencies
|
||||
run: |
|
||||
gem install nokogiri:1.12.5 fog-aws:1.3.0 json mime-types:3.1 rspec:3.5
|
||||
gem install nokogiri:1.12.5 excon:0.109.0 fog-aws:1.3.0 json mime-types:3.1 rspec:3.5
|
||||
- name: Install Java dependencies
|
||||
run: |
|
||||
sudo apt-get install -y default-jdk maven
|
||||
sudo apt-get update && sudo apt-get install -y --fix-missing default-jdk maven
|
||||
- name: Setup CI services
|
||||
run: docker-compose --profile ceph up -d
|
||||
run: docker compose --profile ceph up -d
|
||||
working-directory: .github/docker
|
||||
env:
|
||||
S3METADATA: mongodb
|
||||
|
@ -421,12 +510,11 @@ jobs:
|
|||
- name: Run Ruby tests
|
||||
run: |-
|
||||
set -ex -o pipefail;
|
||||
rspec tests.rb | tee /tmp/artifacts/${{ github.job }}/ruby-tests.log
|
||||
rspec -fd --backtrace tests.rb | tee /tmp/artifacts/${{ github.job }}/ruby-tests.log
|
||||
working-directory: tests/functional/fog
|
||||
- name: Run Javascript AWS SDK tests
|
||||
run: |-
|
||||
set -ex -o pipefail;
|
||||
source ~/.virtualenv/py2/bin/activate
|
||||
yarn run ft_awssdk | tee /tmp/artifacts/${{ github.job }}/js-awssdk-tests.log;
|
||||
yarn run ft_s3cmd | tee /tmp/artifacts/${{ github.job }}/js-s3cmd-tests.log;
|
||||
env:
|
||||
|
@ -435,7 +523,7 @@ jobs:
|
|||
S3VAULT: mem
|
||||
S3METADATA: mongodb
|
||||
- name: Upload logs to artifacts
|
||||
uses: scality/action-artifacts@v3
|
||||
uses: scality/action-artifacts@v4
|
||||
with:
|
||||
method: upload
|
||||
url: https://artifacts.scality.net
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
ARG NODE_VERSION=16.17.1-bullseye-slim
|
||||
ARG NODE_VERSION=16.20-bullseye-slim
|
||||
|
||||
FROM node:${NODE_VERSION} as builder
|
||||
|
||||
|
@ -23,6 +23,7 @@ RUN apt-get update \
|
|||
|
||||
ENV PYTHON=python3
|
||||
COPY package.json yarn.lock /usr/src/app/
|
||||
RUN npm install typescript -g
|
||||
RUN yarn install --production --ignore-optional --frozen-lockfile --ignore-engines --network-concurrency 1
|
||||
|
||||
################################################################################
|
||||
|
@ -42,6 +43,7 @@ EXPOSE 8002
|
|||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
jq \
|
||||
tini \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
@ -53,6 +55,6 @@ COPY --from=builder /usr/src/app/node_modules ./node_modules/
|
|||
|
||||
VOLUME ["/usr/src/app/localData","/usr/src/app/localMetadata"]
|
||||
|
||||
ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"]
|
||||
ENTRYPOINT ["tini", "--", "/usr/src/app/docker-entrypoint.sh"]
|
||||
|
||||
CMD [ "yarn", "start" ]
|
||||
|
|
175
README.md
175
README.md
|
@ -1,10 +1,7 @@
|
|||
# Zenko CloudServer
|
||||
# Zenko CloudServer with Vitastor Backend
|
||||
|
||||
![Zenko CloudServer logo](res/scality-cloudserver-logo.png)
|
||||
|
||||
[![Docker Pulls][badgedocker]](https://hub.docker.com/r/zenko/cloudserver)
|
||||
[![Docker Pulls][badgetwitter]](https://twitter.com/zenko)
|
||||
|
||||
## Overview
|
||||
|
||||
CloudServer (formerly S3 Server) is an open-source Amazon S3-compatible
|
||||
|
@ -14,137 +11,71 @@ Scality’s Open Source Multi-Cloud Data Controller.
|
|||
CloudServer provides a single AWS S3 API interface to access multiple
|
||||
backend data storage both on-premise or public in the cloud.
|
||||
|
||||
CloudServer is useful for Developers, either to run as part of a
|
||||
continous integration test environment to emulate the AWS S3 service locally
|
||||
or as an abstraction layer to develop object storage enabled
|
||||
application on the go.
|
||||
This repository contains a fork of CloudServer with [Vitastor](https://git.yourcmc.ru/vitalif/vitastor)
|
||||
backend support.
|
||||
|
||||
## Learn more at [www.zenko.io/cloudserver](https://www.zenko.io/cloudserver/)
|
||||
## Quick Start with Vitastor
|
||||
|
||||
## [May I offer you some lovely documentation?](http://s3-server.readthedocs.io/en/latest/)
|
||||
Vitastor Backend is in experimental status, however you can already try to
|
||||
run it and write or read something, or even mount it with [GeeseFS](https://github.com/yandex-cloud/geesefs),
|
||||
it works too 😊.
|
||||
|
||||
## Docker
|
||||
Installation instructions:
|
||||
|
||||
[Run your Zenko CloudServer with Docker](https://hub.docker.com/r/zenko/cloudserver/)
|
||||
### Install Vitastor
|
||||
|
||||
## Contributing
|
||||
Refer to [Vitastor Quick Start Manual](https://git.yourcmc.ru/vitalif/vitastor/src/branch/master/docs/intro/quickstart.en.md).
|
||||
|
||||
In order to contribute, please follow the
|
||||
[Contributing Guidelines](
|
||||
https://github.com/scality/Guidelines/blob/master/CONTRIBUTING.md).
|
||||
### Install Zenko with Vitastor Backend
|
||||
|
||||
## Installation
|
||||
- Clone this repository: `git clone https://git.yourcmc.ru/vitalif/zenko-cloudserver-vitastor`
|
||||
- Install dependencies: `npm install --omit dev` or just `npm install`
|
||||
- Clone Vitastor repository: `git clone https://git.yourcmc.ru/vitalif/vitastor`
|
||||
- Build Vitastor node.js binding by running `npm install` in `node-binding` subdirectory of Vitastor repository.
|
||||
You need `node-gyp` and `vitastor-client-dev` (Vitastor client library) for it to succeed.
|
||||
- Symlink Vitastor module to Zenko: `ln -s /path/to/vitastor/node-binding /path/to/zenko/node_modules/vitastor`
|
||||
|
||||
### Dependencies
|
||||
### Install and Configure MongoDB
|
||||
|
||||
Building and running the Zenko CloudServer requires node.js 10.x and yarn v1.17.x
|
||||
. Up-to-date versions can be found at
|
||||
[Nodesource](https://github.com/nodesource/distributions).
|
||||
Refer to [MongoDB Manual](https://www.mongodb.com/docs/manual/installation/).
|
||||
|
||||
### Clone source code
|
||||
### Setup Zenko
|
||||
|
||||
```shell
|
||||
git clone https://github.com/scality/S3.git
|
||||
- Create a separate pool for S3 object data in your Vitastor cluster: `vitastor-cli create-pool s3-data`
|
||||
- Retrieve ID of the new pool from `vitastor-cli ls-pools --detail s3-data`
|
||||
- In another pool, create an image for storing Vitastor volume metadata: `vitastor-cli create -s 10G s3-volume-meta`
|
||||
- Copy `config.json.vitastor` to `config.json`, adjust it to match your domain
|
||||
- Copy `authdata.json.example` to `authdata.json` - this is where you set S3 access & secret keys,
|
||||
and also adjust them if you want to. Scality seems to use a separate auth service "Scality Vault" for
|
||||
access keys, but it's not published, so let's use a file for now.
|
||||
- Copy `locationConfig.json.vitastor` to `locationConfig.json` - this is where you set Vitastor cluster access data.
|
||||
You should put correct values for `pool_id` (pool ID from the second step) and `metadata_image` (from the third step)
|
||||
in this file.
|
||||
|
||||
Note: `locationConfig.json` in this version corresponds to storage classes (like STANDARD, COLD, etc)
|
||||
instead of "locations" (zones like us-east-1) as it was in original Zenko CloudServer.
|
||||
|
||||
### Start Zenko
|
||||
|
||||
Start the S3 server with: `node index.js`
|
||||
|
||||
If you use default settings, Zenko CloudServer starts on port 8000.
|
||||
The default access key is `accessKey1` with a secret key of `verySecretKey1`.
|
||||
|
||||
Now you can access your S3 with `s3cmd` or `geesefs`:
|
||||
|
||||
```
|
||||
s3cmd --access_key=accessKey1 --secret_key=verySecretKey1 --host=http://localhost:8000 mb s3://testbucket
|
||||
```
|
||||
|
||||
### Install js dependencies
|
||||
|
||||
Go to the ./S3 folder,
|
||||
|
||||
```shell
|
||||
yarn install --frozen-lockfile
|
||||
```
|
||||
AWS_ACCESS_KEY_ID=accessKey1 \
|
||||
AWS_SECRET_ACCESS_KEY=verySecretKey1 \
|
||||
geesefs --endpoint http://localhost:8000 testbucket mountdir
|
||||
```
|
||||
|
||||
If you get an error regarding installation of the diskUsage module,
|
||||
please install g++.
|
||||
# Author & License
|
||||
|
||||
If you get an error regarding level-down bindings, try clearing your yarn cache:
|
||||
|
||||
```shell
|
||||
yarn cache clean
|
||||
```
|
||||
|
||||
## Run it with a file backend
|
||||
|
||||
```shell
|
||||
yarn start
|
||||
```
|
||||
|
||||
This starts a Zenko CloudServer on port 8000. Two additional ports 9990 and
|
||||
9991 are also open locally for internal transfer of metadata and data,
|
||||
respectively.
|
||||
|
||||
The default access key is accessKey1 with
|
||||
a secret key of verySecretKey1.
|
||||
|
||||
By default the metadata files will be saved in the
|
||||
localMetadata directory and the data files will be saved
|
||||
in the localData directory within the ./S3 directory on your
|
||||
machine. These directories have been pre-created within the
|
||||
repository. If you would like to save the data or metadata in
|
||||
different locations of your choice, you must specify them with absolute paths.
|
||||
So, when starting the server:
|
||||
|
||||
```shell
|
||||
mkdir -m 700 $(pwd)/myFavoriteDataPath
|
||||
mkdir -m 700 $(pwd)/myFavoriteMetadataPath
|
||||
export S3DATAPATH="$(pwd)/myFavoriteDataPath"
|
||||
export S3METADATAPATH="$(pwd)/myFavoriteMetadataPath"
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Run it with multiple data backends
|
||||
|
||||
```shell
|
||||
export S3DATA='multiple'
|
||||
yarn start
|
||||
```
|
||||
|
||||
This starts a Zenko CloudServer on port 8000.
|
||||
The default access key is accessKey1 with
|
||||
a secret key of verySecretKey1.
|
||||
|
||||
With multiple backends, you have the ability to
|
||||
choose where each object will be saved by setting
|
||||
the following header with a locationConstraint on
|
||||
a PUT request:
|
||||
|
||||
```shell
|
||||
'x-amz-meta-scal-location-constraint':'myLocationConstraint'
|
||||
```
|
||||
|
||||
If no header is sent with a PUT object request, the
|
||||
location constraint of the bucket will determine
|
||||
where the data is saved. If the bucket has no location
|
||||
constraint, the endpoint of the PUT request will be
|
||||
used to determine location.
|
||||
|
||||
See the Configuration section in our documentation
|
||||
[here](http://s3-server.readthedocs.io/en/latest/GETTING_STARTED/#configuration)
|
||||
to learn how to set location constraints.
|
||||
|
||||
## Run it with an in-memory backend
|
||||
|
||||
```shell
|
||||
yarn run mem_backend
|
||||
```
|
||||
|
||||
This starts a Zenko CloudServer on port 8000.
|
||||
The default access key is accessKey1 with
|
||||
a secret key of verySecretKey1.
|
||||
|
||||
## Run it with Vault user management
|
||||
|
||||
Note: Vault is proprietary and must be accessed separately.
|
||||
|
||||
```shell
|
||||
export S3VAULT=vault
|
||||
yarn start
|
||||
```
|
||||
|
||||
This starts a Zenko CloudServer using Vault for user management.
|
||||
|
||||
[badgetwitter]: https://img.shields.io/twitter/follow/zenko.svg?style=social&label=Follow
|
||||
[badgedocker]: https://img.shields.io/docker/pulls/scality/s3server.svg
|
||||
[badgepub]: https://circleci.com/gh/scality/S3.svg?style=svg
|
||||
[badgepriv]: http://ci.ironmann.io/gh/scality/S3.svg?style=svg&circle-token=1f105b7518b53853b5b7cf72302a3f75d8c598ae
|
||||
- [Zenko CloudServer](https://s3-server.readthedocs.io/en/latest/) author is Scality, licensed under [Apache License, version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
- [Vitastor](https://git.yourcmc.ru/vitalif/vitastor/) and Zenko Vitastor backend author is Vitaliy Filippov, licensed under [VNPL-1.1](https://git.yourcmc.ru/vitalif/vitastor/src/branch/master/VNPL-1.1.txt)
|
||||
(a "network copyleft" license based on AGPL/SSPL, but worded in a better way)
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
'use strict'; // eslint-disable-line strict
|
||||
|
||||
const {
|
||||
startWSManagementClient,
|
||||
startPushConnectionHealthCheckServer,
|
||||
} = require('../lib/management/push');
|
||||
|
||||
const logger = require('../lib/utilities/logger');
|
||||
|
||||
const {
|
||||
PUSH_ENDPOINT: pushEndpoint,
|
||||
INSTANCE_ID: instanceId,
|
||||
MANAGEMENT_TOKEN: managementToken,
|
||||
} = process.env;
|
||||
|
||||
if (!pushEndpoint) {
|
||||
logger.error('missing push endpoint env var');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!instanceId) {
|
||||
logger.error('missing instance id env var');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!managementToken) {
|
||||
logger.error('missing management token env var');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
startPushConnectionHealthCheckServer(err => {
|
||||
if (err) {
|
||||
logger.error('could not start healthcheck server', { error: err });
|
||||
process.exit(1);
|
||||
}
|
||||
const url = `${pushEndpoint}/${instanceId}/ws?metrics=1`;
|
||||
startWSManagementClient(url, managementToken, err => {
|
||||
if (err) {
|
||||
logger.error('connection failed, exiting', { error: err });
|
||||
process.exit(1);
|
||||
}
|
||||
logger.info('no more connection, exiting');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
|
@ -1,46 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
'use strict'; // eslint-disable-line strict
|
||||
|
||||
const {
|
||||
startWSManagementClient,
|
||||
startPushConnectionHealthCheckServer,
|
||||
} = require('../lib/management/push');
|
||||
|
||||
const logger = require('../lib/utilities/logger');
|
||||
|
||||
const {
|
||||
PUSH_ENDPOINT: pushEndpoint,
|
||||
INSTANCE_ID: instanceId,
|
||||
MANAGEMENT_TOKEN: managementToken,
|
||||
} = process.env;
|
||||
|
||||
if (!pushEndpoint) {
|
||||
logger.error('missing push endpoint env var');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!instanceId) {
|
||||
logger.error('missing instance id env var');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!managementToken) {
|
||||
logger.error('missing management token env var');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
startPushConnectionHealthCheckServer(err => {
|
||||
if (err) {
|
||||
logger.error('could not start healthcheck server', { error: err });
|
||||
process.exit(1);
|
||||
}
|
||||
const url = `${pushEndpoint}/${instanceId}/ws?proxy=1`;
|
||||
startWSManagementClient(url, managementToken, err => {
|
||||
if (err) {
|
||||
logger.error('connection failed, exiting', { error: err });
|
||||
process.exit(1);
|
||||
}
|
||||
logger.info('no more connection, exiting');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
|
@ -4,6 +4,7 @@
|
|||
"metricsPort": 8002,
|
||||
"metricsListenOn": [],
|
||||
"replicationGroupId": "RG001",
|
||||
"workers": 4,
|
||||
"restEndpoints": {
|
||||
"localhost": "us-east-1",
|
||||
"127.0.0.1": "us-east-1",
|
||||
|
@ -101,6 +102,14 @@
|
|||
"readPreference": "primary",
|
||||
"database": "metadata"
|
||||
},
|
||||
"authdata": "authdata.json",
|
||||
"backends": {
|
||||
"auth": "file",
|
||||
"data": "file",
|
||||
"metadata": "mongodb",
|
||||
"kms": "file",
|
||||
"quota": "none"
|
||||
},
|
||||
"externalBackends": {
|
||||
"aws_s3": {
|
||||
"httpAgent": {
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"port": 8000,
|
||||
"listenOn": [],
|
||||
"metricsPort": 8002,
|
||||
"metricsListenOn": [],
|
||||
"replicationGroupId": "RG001",
|
||||
"restEndpoints": {
|
||||
"localhost": "STANDARD",
|
||||
"127.0.0.1": "STANDARD",
|
||||
"yourhostname.ru": "STANDARD"
|
||||
},
|
||||
"websiteEndpoints": [
|
||||
"static.yourhostname.ru"
|
||||
],
|
||||
"replicationEndpoints": [ {
|
||||
"site": "zenko",
|
||||
"servers": ["127.0.0.1:8000"],
|
||||
"default": true
|
||||
} ],
|
||||
"log": {
|
||||
"logLevel": "info",
|
||||
"dumpLevel": "error"
|
||||
},
|
||||
"healthChecks": {
|
||||
"allowFrom": ["127.0.0.1/8", "::1"]
|
||||
},
|
||||
"backends": {
|
||||
"metadata": "mongodb"
|
||||
},
|
||||
"mongodb": {
|
||||
"replicaSetHosts": "127.0.0.1:27017",
|
||||
"writeConcern": "majority",
|
||||
"replicaSet": "rs0",
|
||||
"readPreference": "primary",
|
||||
"database": "s3",
|
||||
"authCredentials": {
|
||||
"username": "s3",
|
||||
"password": ""
|
||||
}
|
||||
},
|
||||
"externalBackends": {
|
||||
"aws_s3": {
|
||||
"httpAgent": {
|
||||
"keepAlive": false,
|
||||
"keepAliveMsecs": 1000,
|
||||
"maxFreeSockets": 256,
|
||||
"maxSockets": null
|
||||
}
|
||||
},
|
||||
"gcp": {
|
||||
"httpAgent": {
|
||||
"keepAlive": true,
|
||||
"keepAliveMsecs": 1000,
|
||||
"maxFreeSockets": 256,
|
||||
"maxSockets": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"requests": {
|
||||
"viaProxy": false,
|
||||
"trustedProxyCIDRs": [],
|
||||
"extractClientIPFromHeader": ""
|
||||
},
|
||||
"bucketNotificationDestinations": [
|
||||
{
|
||||
"resource": "target1",
|
||||
"type": "dummy",
|
||||
"host": "localhost:6000"
|
||||
}
|
||||
]
|
||||
}
|
51
constants.js
51
constants.js
|
@ -116,7 +116,8 @@ const constants = {
|
|||
],
|
||||
|
||||
// user metadata header to set object locationConstraint
|
||||
objectLocationConstraintHeader: 'x-amz-meta-scal-location-constraint',
|
||||
objectLocationConstraintHeader: 'x-amz-storage-class',
|
||||
lastModifiedHeader: 'x-amz-meta-x-scal-last-modified',
|
||||
legacyLocations: ['sproxyd', 'legacy'],
|
||||
// declare here all existing service accounts and their properties
|
||||
// (if any, otherwise an empty object)
|
||||
|
@ -129,7 +130,7 @@ const constants = {
|
|||
},
|
||||
},
|
||||
/* eslint-disable camelcase */
|
||||
externalBackends: { aws_s3: true, azure: true, gcp: true, pfs: true, dmf: true },
|
||||
externalBackends: { aws_s3: true, azure: true, gcp: true, pfs: true, dmf: true, azure_archive: true },
|
||||
// some of the available data backends (if called directly rather
|
||||
// than through the multiple backend gateway) need a key provided
|
||||
// as a string as first parameter of the get/delete methods.
|
||||
|
@ -175,6 +176,8 @@ const constants = {
|
|||
'objectDeleteTagging',
|
||||
'objectGetTagging',
|
||||
'objectPutTagging',
|
||||
'objectPutLegalHold',
|
||||
'objectPutRetention',
|
||||
],
|
||||
// response header to be sent when there are invalid
|
||||
// user metadata in the object's metadata
|
||||
|
@ -195,11 +198,51 @@ const constants = {
|
|||
'user',
|
||||
'bucket',
|
||||
],
|
||||
arrayOfAllowed: [
|
||||
'objectPutTagging',
|
||||
'objectPutLegalHold',
|
||||
'objectPutRetention',
|
||||
],
|
||||
allowedUtapiEventFilterStates: ['allow', 'deny'],
|
||||
allowedRestoreObjectRequestTierValues: ['Standard'],
|
||||
validStorageClasses: [
|
||||
'STANDARD',
|
||||
lifecycleListing: {
|
||||
CURRENT_TYPE: 'current',
|
||||
NON_CURRENT_TYPE: 'noncurrent',
|
||||
ORPHAN_DM_TYPE: 'orphan',
|
||||
},
|
||||
multiObjectDeleteConcurrency: 50,
|
||||
maxScannedLifecycleListingEntries: 10000,
|
||||
overheadField: [
|
||||
'content-length',
|
||||
'owner-id',
|
||||
'versionId',
|
||||
'isNull',
|
||||
'isDeleteMarker',
|
||||
],
|
||||
unsupportedSignatureChecksums: new Set([
|
||||
'STREAMING-UNSIGNED-PAYLOAD-TRAILER',
|
||||
'STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER',
|
||||
'STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD',
|
||||
'STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER',
|
||||
]),
|
||||
supportedSignatureChecksums: new Set([
|
||||
'UNSIGNED-PAYLOAD',
|
||||
'STREAMING-AWS4-HMAC-SHA256-PAYLOAD',
|
||||
]),
|
||||
ipv4Regex: /^(\d{1,3}\.){3}\d{1,3}(\/(3[0-2]|[12]?\d))?$/,
|
||||
ipv6Regex: /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/i,
|
||||
// The AWS assumed Role resource type
|
||||
assumedRoleArnResourceType: 'assumed-role',
|
||||
// Session name of the backbeat lifecycle assumed role session.
|
||||
backbeatLifecycleSessionName: 'backbeat-lifecycle',
|
||||
actionsToConsiderAsObjectPut: [
|
||||
'initiateMultipartUpload',
|
||||
'objectPutPart',
|
||||
'completeMultipartUpload',
|
||||
],
|
||||
// if requester is not bucket owner, bucket policy actions should be denied with
|
||||
// MethodNotAllowed error
|
||||
onlyOwnerAllowed: ['bucketDeletePolicy', 'bucketGetPolicy', 'bucketPutPolicy'],
|
||||
};
|
||||
|
||||
module.exports = constants;
|
||||
|
|
|
@ -199,6 +199,10 @@ if [[ -n "$BUCKET_DENY_FILTER" ]]; then
|
|||
JQ_FILTERS_CONFIG="$JQ_FILTERS_CONFIG | .utapi.filter.deny.bucket=[\"$BUCKET_DENY_FILTER\"]"
|
||||
fi
|
||||
|
||||
if [[ "$TESTING_MODE" ]]; then
|
||||
JQ_FILTERS_CONFIG="$JQ_FILTERS_CONFIG | .testingMode=true"
|
||||
fi
|
||||
|
||||
if [[ $JQ_FILTERS_CONFIG != "." ]]; then
|
||||
jq "$JQ_FILTERS_CONFIG" config.json > config.json.tmp
|
||||
mv config.json.tmp config.json
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
|
||||
## Docker Image Generation
|
||||
|
||||
Docker images are hosted on [registry.scality.com](registry.scality.com).
|
||||
CloudServer has two namespaces there:
|
||||
Docker images are hosted on [ghcri.io](https://github.com/orgs/scality/packages).
|
||||
CloudServer has a few images there:
|
||||
|
||||
* Production Namespace: registry.scality.com/cloudserver
|
||||
* Dev Namespace: registry.scality.com/cloudserver-dev
|
||||
* Cloudserver container image: ghcr.io/scality/cloudserver
|
||||
* Dashboard oras image: ghcr.io/scality/cloudserver/cloudser-dashboard
|
||||
* Policies oras image: ghcr.io/scality/cloudserver/cloudser-dashboard
|
||||
|
||||
With every CI build, the CI will push images, tagging the
|
||||
content with the developer branch's short SHA-1 commit hash.
|
||||
|
@ -18,8 +19,8 @@ Tagged versions of cloudserver will be stored in the production namespace.
|
|||
## How to Pull Docker Images
|
||||
|
||||
```sh
|
||||
docker pull registry.scality.com/cloudserver-dev/cloudserver:<commit hash>
|
||||
docker pull registry.scality.com/cloudserver/cloudserver:<tag>
|
||||
docker pull ghcr.io/scality/cloudserver:<commit hash>
|
||||
docker pull ghcr.io/scality/cloudserver:<tag>
|
||||
```
|
||||
|
||||
## Release Process
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM registry.scality.com/federation/nodesvc-base:7.10.6.0
|
||||
FROM ghcr.io/scality/federation/nodesvc-base:7.10.6.0
|
||||
|
||||
ENV S3_CONFIG_FILE=${CONF_DIR}/config.json
|
||||
ENV S3_LOCATION_FILE=${CONF_DIR}/locationConfig.json
|
||||
|
@ -14,8 +14,10 @@ RUN rm -f ~/.gitconfig && \
|
|||
git config --global --add safe.directory . && \
|
||||
git lfs install && \
|
||||
GIT_LFS_SKIP_SMUDGE=1 && \
|
||||
yarn global add typescript && \
|
||||
yarn install --frozen-lockfile --production --network-concurrency 1 && \
|
||||
yarn cache clean --all
|
||||
yarn cache clean --all && \
|
||||
yarn global remove typescript
|
||||
|
||||
# run symlinking separately to avoid yarn installation errors
|
||||
# we might have to check if the symlinking is really needed!
|
||||
|
|
7
index.js
7
index.js
|
@ -1,3 +1,10 @@
|
|||
'use strict'; // eslint-disable-line strict
|
||||
|
||||
require('werelogs').stderrUtils.catchAndTimestampStderr(
|
||||
undefined,
|
||||
// Do not exit as workers have their own listener that will exit
|
||||
// But primary don't have another listener
|
||||
require('cluster').isPrimary ? 1 : null,
|
||||
);
|
||||
|
||||
require('./lib/server.js')();
|
||||
|
|
445
lib/Config.js
445
lib/Config.js
|
@ -8,16 +8,18 @@ const crypto = require('crypto');
|
|||
const { v4: uuidv4 } = require('uuid');
|
||||
const cronParser = require('cron-parser');
|
||||
const joi = require('@hapi/joi');
|
||||
|
||||
const { isValidBucketName } = require('arsenal').s3routes.routesUtils;
|
||||
const validateAuthConfig = require('arsenal').auth.inMemory.validateAuthConfig;
|
||||
const { s3routes, auth: arsenalAuth, s3middleware } = require('arsenal');
|
||||
const { isValidBucketName } = s3routes.routesUtils;
|
||||
const validateAuthConfig = arsenalAuth.inMemory.validateAuthConfig;
|
||||
const { buildAuthDataAccount } = require('./auth/in_memory/builder');
|
||||
const validExternalBackends = require('../constants').externalBackends;
|
||||
const { azureAccountNameRegex, base64Regex,
|
||||
allowedUtapiEventFilterFields, allowedUtapiEventFilterStates,
|
||||
} = require('../constants');
|
||||
const { utapiVersion } = require('utapi');
|
||||
const { scaleMsPerDay } = s3middleware.objectUtils;
|
||||
|
||||
const constants = require('../constants');
|
||||
|
||||
// config paths
|
||||
const configSearchPaths = [
|
||||
|
@ -105,6 +107,47 @@ function parseSproxydConfig(configSproxyd) {
|
|||
return joi.attempt(configSproxyd, joiSchema, 'bad config');
|
||||
}
|
||||
|
||||
function parseRedisConfig(redisConfig) {
|
||||
const joiSchema = joi.object({
|
||||
password: joi.string().allow(''),
|
||||
host: joi.string(),
|
||||
port: joi.number(),
|
||||
retry: joi.object({
|
||||
connectBackoff: joi.object({
|
||||
min: joi.number().required(),
|
||||
max: joi.number().required(),
|
||||
jitter: joi.number().required(),
|
||||
factor: joi.number().required(),
|
||||
deadline: joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
// sentinel config
|
||||
sentinels: joi.alternatives().try(
|
||||
joi.string()
|
||||
.pattern(/^[a-zA-Z0-9.-]+:[0-9]+(,[a-zA-Z0-9.-]+:[0-9]+)*$/)
|
||||
.custom(hosts => hosts.split(',').map(item => {
|
||||
const [host, port] = item.split(':');
|
||||
return { host, port: Number.parseInt(port, 10) };
|
||||
})),
|
||||
joi.array().items(
|
||||
joi.object({
|
||||
host: joi.string().required(),
|
||||
port: joi.number().required(),
|
||||
})
|
||||
).min(1),
|
||||
),
|
||||
name: joi.string(),
|
||||
sentinelPassword: joi.string().allow(''),
|
||||
})
|
||||
.and('host', 'port')
|
||||
.and('sentinels', 'name')
|
||||
.xor('host', 'sentinels')
|
||||
.without('sentinels', ['host', 'port'])
|
||||
.without('host', ['sentinels', 'sentinelPassword']);
|
||||
|
||||
return joi.attempt(redisConfig, joiSchema, 'bad config');
|
||||
}
|
||||
|
||||
function restEndpointsAssert(restEndpoints, locationConstraints) {
|
||||
assert(typeof restEndpoints === 'object',
|
||||
'bad config: restEndpoints must be an object of endpoints');
|
||||
|
@ -237,6 +280,60 @@ function hdClientLocationConstraintAssert(configHd) {
|
|||
return hdclientFields;
|
||||
}
|
||||
|
||||
function azureArchiveLocationConstraintAssert(locationObj) {
|
||||
const checkedFields = [
|
||||
'azureContainerName',
|
||||
'azureStorageEndpoint',
|
||||
];
|
||||
if (Object.keys(locationObj.details).length === 0 ||
|
||||
!checkedFields.every(field => field in locationObj.details)) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
azureContainerName,
|
||||
azureStorageEndpoint,
|
||||
} = locationObj.details;
|
||||
const stringFields = [
|
||||
azureContainerName,
|
||||
azureStorageEndpoint,
|
||||
];
|
||||
stringFields.forEach(field => {
|
||||
assert(typeof field === 'string',
|
||||
`bad config: ${field} must be a string`);
|
||||
});
|
||||
|
||||
let hasAuthMethod = false;
|
||||
if (locationObj.details.sasToken !== undefined) {
|
||||
assert(typeof locationObj.details.sasToken === 'string',
|
||||
`bad config: ${locationObj.details.sasToken} must be a string`);
|
||||
hasAuthMethod = true;
|
||||
}
|
||||
|
||||
if (locationObj.details.azureStorageAccountName !== undefined &&
|
||||
locationObj.details.azureStorageAccessKey !== undefined) {
|
||||
assert(typeof locationObj.details.azureStorageAccountName === 'string',
|
||||
`bad config: ${locationObj.details.azureStorageAccountName} must be a string`);
|
||||
assert(typeof locationObj.details.azureStorageAccessKey === 'string',
|
||||
`bad config: ${locationObj.details.azureStorageAccessKey} must be a string`);
|
||||
assert(!hasAuthMethod, 'Multiple authentication methods are not allowed');
|
||||
hasAuthMethod = true;
|
||||
}
|
||||
|
||||
if (locationObj.details.tenantId !== undefined &&
|
||||
locationObj.details.clientId !== undefined &&
|
||||
locationObj.details.clientKey !== undefined) {
|
||||
assert(typeof locationObj.details.tenantId === 'string',
|
||||
`bad config: ${locationObj.details.tenantId} must be a string`);
|
||||
assert(typeof locationObj.details.clientId === 'string',
|
||||
`bad config: ${locationObj.details.clientId} must be a string`);
|
||||
assert(typeof locationObj.details.clientKey === 'string',
|
||||
`bad config: ${locationObj.details.clientKey} must be a string`);
|
||||
assert(!hasAuthMethod, 'Multiple authentication methods are not allowed');
|
||||
hasAuthMethod = true;
|
||||
}
|
||||
assert(hasAuthMethod, 'Missing authentication method');
|
||||
}
|
||||
|
||||
function dmfLocationConstraintAssert(locationObj) {
|
||||
const checkedFields = [
|
||||
'endpoint',
|
||||
|
@ -280,7 +377,7 @@ function dmfLocationConstraintAssert(locationObj) {
|
|||
function locationConstraintAssert(locationConstraints) {
|
||||
const supportedBackends =
|
||||
['mem', 'file', 'scality',
|
||||
'mongodb', 'dmf'].concat(Object.keys(validExternalBackends));
|
||||
'mongodb', 'dmf', 'azure_archive', 'vitastor'].concat(Object.keys(validExternalBackends));
|
||||
assert(typeof locationConstraints === 'object',
|
||||
'bad config: locationConstraints must be an object');
|
||||
Object.keys(locationConstraints).forEach(l => {
|
||||
|
@ -391,6 +488,9 @@ function locationConstraintAssert(locationConstraints) {
|
|||
if (locationConstraints[l].type === 'dmf') {
|
||||
dmfLocationConstraintAssert(locationConstraints[l]);
|
||||
}
|
||||
if (locationConstraints[l].type === 'azure_archive') {
|
||||
azureArchiveLocationConstraintAssert(locationConstraints[l]);
|
||||
}
|
||||
if (locationConstraints[l].type === 'pfs') {
|
||||
assert(typeof details.pfsDaemonEndpoint === 'object',
|
||||
'bad config: pfsDaemonEndpoint is mandatory and must be an object');
|
||||
|
@ -402,26 +502,33 @@ function locationConstraintAssert(locationConstraints) {
|
|||
locationConstraints[l].details.connector.hdclient);
|
||||
}
|
||||
});
|
||||
assert(Object.keys(locationConstraints)
|
||||
.includes('us-east-1'), 'bad locationConfig: must ' +
|
||||
'include us-east-1 as a locationConstraint');
|
||||
}
|
||||
|
||||
function parseUtapiReindex({ enabled, schedule, sentinel, bucketd }) {
|
||||
function parseUtapiReindex(config) {
|
||||
const {
|
||||
enabled,
|
||||
schedule,
|
||||
redis,
|
||||
bucketd,
|
||||
onlyCountLatestWhenObjectLocked,
|
||||
} = config;
|
||||
assert(typeof enabled === 'boolean',
|
||||
'bad config: utapi.reindex.enabled must be a boolean');
|
||||
assert(typeof sentinel === 'object',
|
||||
'bad config: utapi.reindex.sentinel must be an object');
|
||||
assert(typeof sentinel.port === 'number',
|
||||
'bad config: utapi.reindex.sentinel.port must be a number');
|
||||
assert(typeof sentinel.name === 'string',
|
||||
'bad config: utapi.reindex.sentinel.name must be a string');
|
||||
'bad config: utapi.reindex.enabled must be a boolean');
|
||||
|
||||
const parsedRedis = parseRedisConfig(redis);
|
||||
assert(Array.isArray(parsedRedis.sentinels),
|
||||
'bad config: utapi reindex redis config requires a list of sentinels');
|
||||
|
||||
assert(typeof bucketd === 'object',
|
||||
'bad config: utapi.reindex.bucketd must be an object');
|
||||
assert(typeof bucketd.port === 'number',
|
||||
'bad config: utapi.reindex.bucketd.port must be a number');
|
||||
assert(typeof schedule === 'string',
|
||||
'bad config: utapi.reindex.schedule must be a string');
|
||||
if (onlyCountLatestWhenObjectLocked !== undefined) {
|
||||
assert(typeof onlyCountLatestWhenObjectLocked === 'boolean',
|
||||
'bad config: utapi.reindex.onlyCountLatestWhenObjectLocked must be a boolean');
|
||||
}
|
||||
try {
|
||||
cronParser.parseExpression(schedule);
|
||||
} catch (e) {
|
||||
|
@ -429,6 +536,13 @@ function parseUtapiReindex({ enabled, schedule, sentinel, bucketd }) {
|
|||
'bad config: utapi.reindex.schedule must be a valid ' +
|
||||
`cron schedule. ${e.message}.`);
|
||||
}
|
||||
return {
|
||||
enabled,
|
||||
schedule,
|
||||
redis: parsedRedis,
|
||||
bucketd,
|
||||
onlyCountLatestWhenObjectLocked,
|
||||
};
|
||||
}
|
||||
|
||||
function requestsConfigAssert(requestsConfig) {
|
||||
|
@ -516,7 +630,6 @@ class Config extends EventEmitter {
|
|||
// Read config automatically
|
||||
this._getLocationConfig();
|
||||
this._getConfig();
|
||||
this._configureBackends();
|
||||
}
|
||||
|
||||
_getLocationConfig() {
|
||||
|
@ -728,11 +841,11 @@ class Config extends EventEmitter {
|
|||
this.websiteEndpoints = config.websiteEndpoints;
|
||||
}
|
||||
|
||||
this.clusters = false;
|
||||
if (config.clusters !== undefined) {
|
||||
assert(Number.isInteger(config.clusters) && config.clusters > 0,
|
||||
'bad config: clusters must be a positive integer');
|
||||
this.clusters = config.clusters;
|
||||
this.workers = false;
|
||||
if (config.workers !== undefined) {
|
||||
assert(Number.isInteger(config.workers) && config.workers > 0,
|
||||
'bad config: workers must be a positive integer');
|
||||
this.workers = config.workers;
|
||||
}
|
||||
|
||||
if (config.usEastBehavior !== undefined) {
|
||||
|
@ -970,8 +1083,7 @@ class Config extends EventEmitter {
|
|||
assert(typeof config.localCache.port === 'number',
|
||||
'config: bad port for localCache. port must be a number');
|
||||
if (config.localCache.password !== undefined) {
|
||||
assert(
|
||||
this._verifyRedisPassword(config.localCache.password),
|
||||
assert(typeof config.localCache.password === 'string',
|
||||
'config: vad password for localCache. password must' +
|
||||
' be a string');
|
||||
}
|
||||
|
@ -997,56 +1109,46 @@ class Config extends EventEmitter {
|
|||
}
|
||||
|
||||
if (config.redis) {
|
||||
if (config.redis.sentinels) {
|
||||
this.redis = { sentinels: [], name: null };
|
||||
|
||||
assert(typeof config.redis.name === 'string',
|
||||
'bad config: redis sentinel name must be a string');
|
||||
this.redis.name = config.redis.name;
|
||||
assert(Array.isArray(config.redis.sentinels) ||
|
||||
typeof config.redis.sentinels === 'string',
|
||||
'bad config: redis sentinels must be an array or string');
|
||||
|
||||
if (typeof config.redis.sentinels === 'string') {
|
||||
config.redis.sentinels.split(',').forEach(item => {
|
||||
const [host, port] = item.split(':');
|
||||
this.redis.sentinels.push({ host,
|
||||
port: Number.parseInt(port, 10) });
|
||||
});
|
||||
} else if (Array.isArray(config.redis.sentinels)) {
|
||||
config.redis.sentinels.forEach(item => {
|
||||
const { host, port } = item;
|
||||
assert(typeof host === 'string',
|
||||
'bad config: redis sentinel host must be a string');
|
||||
assert(typeof port === 'number',
|
||||
'bad config: redis sentinel port must be a number');
|
||||
this.redis.sentinels.push({ host, port });
|
||||
});
|
||||
}
|
||||
|
||||
if (config.redis.sentinelPassword !== undefined) {
|
||||
assert(
|
||||
this._verifyRedisPassword(config.redis.sentinelPassword));
|
||||
this.redis.sentinelPassword = config.redis.sentinelPassword;
|
||||
}
|
||||
} else {
|
||||
// check for standalone configuration
|
||||
this.redis = {};
|
||||
assert(typeof config.redis.host === 'string',
|
||||
'bad config: redis.host must be a string');
|
||||
assert(typeof config.redis.port === 'number',
|
||||
'bad config: redis.port must be a number');
|
||||
this.redis.host = config.redis.host;
|
||||
this.redis.port = config.redis.port;
|
||||
this.redis = parseRedisConfig(config.redis);
|
||||
}
|
||||
if (config.scuba) {
|
||||
this.scuba = {};
|
||||
if (config.scuba.host) {
|
||||
assert(typeof config.scuba.host === 'string',
|
||||
'bad config: scuba host must be a string');
|
||||
this.scuba.host = config.scuba.host;
|
||||
}
|
||||
if (config.redis.password !== undefined) {
|
||||
assert(
|
||||
this._verifyRedisPassword(config.redis.password),
|
||||
'bad config: invalid password for redis. password must ' +
|
||||
'be a string');
|
||||
this.redis.password = config.redis.password;
|
||||
if (config.scuba.port) {
|
||||
assert(Number.isInteger(config.scuba.port)
|
||||
&& config.scuba.port > 0,
|
||||
'bad config: scuba port must be a positive integer');
|
||||
this.scuba.port = config.scuba.port;
|
||||
}
|
||||
}
|
||||
if (process.env.SCUBA_HOST && process.env.SCUBA_PORT) {
|
||||
assert(typeof process.env.SCUBA_HOST === 'string',
|
||||
'bad config: scuba host must be a string');
|
||||
assert(Number.isInteger(Number(process.env.SCUBA_PORT))
|
||||
&& Number(process.env.SCUBA_PORT) > 0,
|
||||
'bad config: scuba port must be a positive integer');
|
||||
this.scuba = {
|
||||
host: process.env.SCUBA_HOST,
|
||||
port: Number(process.env.SCUBA_PORT),
|
||||
};
|
||||
}
|
||||
if (this.scuba) {
|
||||
this.quotaEnabled = true;
|
||||
}
|
||||
const maxStaleness = Number(process.env.QUOTA_MAX_STALENESS_MS) ||
|
||||
config.quota?.maxStatenessMS ||
|
||||
24 * 60 * 60 * 1000;
|
||||
assert(Number.isInteger(maxStaleness), 'bad config: maxStalenessMS must be an integer');
|
||||
const enableInflights = process.env.QUOTA_ENABLE_INFLIGHTS === 'true' ||
|
||||
config.quota?.enableInflights || false;
|
||||
this.quota = {
|
||||
maxStaleness,
|
||||
enableInflights,
|
||||
};
|
||||
if (config.utapi) {
|
||||
this.utapi = { component: 's3' };
|
||||
if (config.utapi.host) {
|
||||
|
@ -1075,50 +1177,8 @@ class Config extends EventEmitter {
|
|||
assert(config.redis, 'missing required property of utapi ' +
|
||||
'configuration: redis');
|
||||
if (config.utapi.redis) {
|
||||
if (config.utapi.redis.sentinels) {
|
||||
this.utapi.redis = { sentinels: [], name: null };
|
||||
|
||||
assert(typeof config.utapi.redis.name === 'string',
|
||||
'bad config: redis sentinel name must be a string');
|
||||
this.utapi.redis.name = config.utapi.redis.name;
|
||||
|
||||
assert(Array.isArray(config.utapi.redis.sentinels),
|
||||
'bad config: redis sentinels must be an array');
|
||||
config.utapi.redis.sentinels.forEach(item => {
|
||||
const { host, port } = item;
|
||||
assert(typeof host === 'string',
|
||||
'bad config: redis sentinel host must be a string');
|
||||
assert(typeof port === 'number',
|
||||
'bad config: redis sentinel port must be a number');
|
||||
this.utapi.redis.sentinels.push({ host, port });
|
||||
});
|
||||
} else {
|
||||
// check for standalone configuration
|
||||
this.utapi.redis = {};
|
||||
assert(typeof config.utapi.redis.host === 'string',
|
||||
'bad config: redis.host must be a string');
|
||||
assert(typeof config.utapi.redis.port === 'number',
|
||||
'bad config: redis.port must be a number');
|
||||
this.utapi.redis.host = config.utapi.redis.host;
|
||||
this.utapi.redis.port = config.utapi.redis.port;
|
||||
}
|
||||
if (config.utapi.redis.retry !== undefined) {
|
||||
if (config.utapi.redis.retry.connectBackoff !== undefined) {
|
||||
const { min, max, jitter, factor, deadline } = config.utapi.redis.retry.connectBackoff;
|
||||
assert.strictEqual(typeof min, 'number',
|
||||
'utapi.redis.retry.connectBackoff: min must be a number');
|
||||
assert.strictEqual(typeof max, 'number',
|
||||
'utapi.redis.retry.connectBackoff: max must be a number');
|
||||
assert.strictEqual(typeof jitter, 'number',
|
||||
'utapi.redis.retry.connectBackoff: jitter must be a number');
|
||||
assert.strictEqual(typeof factor, 'number',
|
||||
'utapi.redis.retry.connectBackoff: factor must be a number');
|
||||
assert.strictEqual(typeof deadline, 'number',
|
||||
'utapi.redis.retry.connectBackoff: deadline must be a number');
|
||||
}
|
||||
|
||||
this.utapi.redis.retry = config.utapi.redis.retry;
|
||||
} else {
|
||||
this.utapi.redis = parseRedisConfig(config.utapi.redis);
|
||||
if (this.utapi.redis.retry === undefined) {
|
||||
this.utapi.redis.retry = {
|
||||
connectBackoff: {
|
||||
min: 10,
|
||||
|
@ -1129,22 +1189,6 @@ class Config extends EventEmitter {
|
|||
},
|
||||
};
|
||||
}
|
||||
if (config.utapi.redis.password !== undefined) {
|
||||
assert(
|
||||
this._verifyRedisPassword(config.utapi.redis.password),
|
||||
'config: invalid password for utapi redis. password' +
|
||||
' must be a string');
|
||||
this.utapi.redis.password = config.utapi.redis.password;
|
||||
}
|
||||
if (config.utapi.redis.sentinelPassword !== undefined) {
|
||||
assert(
|
||||
this._verifyRedisPassword(
|
||||
config.utapi.redis.sentinelPassword),
|
||||
'config: invalid password for utapi redis. password' +
|
||||
' must be a string');
|
||||
this.utapi.redis.sentinelPassword =
|
||||
config.utapi.redis.sentinelPassword;
|
||||
}
|
||||
}
|
||||
if (config.utapi.metrics) {
|
||||
this.utapi.metrics = config.utapi.metrics;
|
||||
|
@ -1214,8 +1258,7 @@ class Config extends EventEmitter {
|
|||
}
|
||||
|
||||
if (config.utapi && config.utapi.reindex) {
|
||||
parseUtapiReindex(config.utapi.reindex);
|
||||
this.utapi.reindex = config.utapi.reindex;
|
||||
this.utapi.reindex = parseUtapiReindex(config.utapi.reindex);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1260,6 +1303,8 @@ class Config extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
this.authdata = config.authdata || 'authdata.json';
|
||||
|
||||
this.kms = {};
|
||||
if (config.kms) {
|
||||
assert(typeof config.kms.userName === 'string');
|
||||
|
@ -1479,25 +1524,6 @@ class Config extends EventEmitter {
|
|||
this.outboundProxy.certs = certObj.certs;
|
||||
}
|
||||
|
||||
this.managementAgent = {};
|
||||
this.managementAgent.port = 8010;
|
||||
this.managementAgent.host = 'localhost';
|
||||
if (config.managementAgent !== undefined) {
|
||||
if (config.managementAgent.port !== undefined) {
|
||||
assert(Number.isInteger(config.managementAgent.port)
|
||||
&& config.managementAgent.port > 0,
|
||||
'bad config: managementAgent port must be a positive ' +
|
||||
'integer');
|
||||
this.managementAgent.port = config.managementAgent.port;
|
||||
}
|
||||
if (config.managementAgent.host !== undefined) {
|
||||
assert.strictEqual(typeof config.managementAgent.host, 'string',
|
||||
'bad config: management agent host must ' +
|
||||
'be a string');
|
||||
this.managementAgent.host = config.managementAgent.host;
|
||||
}
|
||||
}
|
||||
|
||||
// Ephemeral token to protect the reporting endpoint:
|
||||
// try inherited from parent first, then hardcoded in conf file,
|
||||
// then create a fresh one as last resort.
|
||||
|
@ -1550,6 +1576,10 @@ class Config extends EventEmitter {
|
|||
requestsConfigAssert(config.requests);
|
||||
this.requests = config.requests;
|
||||
}
|
||||
// CLDSRV-378: on 8.x branches, null version compatibility
|
||||
// mode is enforced because null keys are not supported by the
|
||||
// MongoDB backend.
|
||||
this.nullVersionCompatMode = true;
|
||||
if (config.bucketNotificationDestinations) {
|
||||
this.bucketNotificationDestinations = bucketNotifAssert(config.bucketNotificationDestinations);
|
||||
}
|
||||
|
@ -1558,43 +1588,108 @@ class Config extends EventEmitter {
|
|||
|
||||
// Version of the configuration we're running under
|
||||
this.overlayVersion = config.overlayVersion || 0;
|
||||
|
||||
this._setTimeOptions();
|
||||
this.multiObjectDeleteConcurrency = constants.multiObjectDeleteConcurrency;
|
||||
const extractedNumber = Number.parseInt(config.multiObjectDeleteConcurrency, 10);
|
||||
if (!isNaN(extractedNumber) && extractedNumber > 0 && extractedNumber < 1000) {
|
||||
this.multiObjectDeleteConcurrency = extractedNumber;
|
||||
}
|
||||
|
||||
this.multiObjectDeleteEnableOptimizations = true;
|
||||
if (config.multiObjectDeleteEnableOptimizations === false) {
|
||||
this.multiObjectDeleteEnableOptimizations = false;
|
||||
}
|
||||
|
||||
this.testingMode = config.testingMode || false;
|
||||
|
||||
this.maxScannedLifecycleListingEntries = constants.maxScannedLifecycleListingEntries;
|
||||
if (config.maxScannedLifecycleListingEntries !== undefined) {
|
||||
// maxScannedLifecycleListingEntries > 2 is required as a minimum because we must
|
||||
// scan at least three entries to determine version eligibility.
|
||||
// Two entries representing the master key and the following one representing the non-current version.
|
||||
assert(Number.isInteger(config.maxScannedLifecycleListingEntries) &&
|
||||
config.maxScannedLifecycleListingEntries > 2,
|
||||
'bad config: maxScannedLifecycleListingEntries must be greater than 2');
|
||||
this.maxScannedLifecycleListingEntries = config.maxScannedLifecycleListingEntries;
|
||||
}
|
||||
|
||||
this._configureBackends(config);
|
||||
}
|
||||
|
||||
_setTimeOptions() {
|
||||
// NOTE: EXPIRE_ONE_DAY_EARLIER and TRANSITION_ONE_DAY_EARLIER are deprecated in favor of
|
||||
// TIME_PROGRESSION_FACTOR which decreases the weight attributed to a day in order to among other things
|
||||
// expedite the lifecycle of objects.
|
||||
|
||||
// moves lifecycle expiration deadlines 1 day earlier, mostly for testing
|
||||
const expireOneDayEarlier = process.env.EXPIRE_ONE_DAY_EARLIER === 'true';
|
||||
// moves lifecycle transition deadlines 1 day earlier, mostly for testing
|
||||
const transitionOneDayEarlier = process.env.TRANSITION_ONE_DAY_EARLIER === 'true';
|
||||
// decreases the weight attributed to a day in order to expedite the lifecycle of objects.
|
||||
const timeProgressionFactor = Number.parseInt(process.env.TIME_PROGRESSION_FACTOR, 10) || 1;
|
||||
|
||||
const isIncompatible = (expireOneDayEarlier || transitionOneDayEarlier) && (timeProgressionFactor > 1);
|
||||
assert(!isIncompatible, 'The environment variables "EXPIRE_ONE_DAY_EARLIER" or ' +
|
||||
'"TRANSITION_ONE_DAY_EARLIER" are not compatible with the "TIME_PROGRESSION_FACTOR" variable.');
|
||||
|
||||
// The scaledMsPerDay value is initially set to the number of milliseconds per day
|
||||
// (24 * 60 * 60 * 1000) as the default value.
|
||||
// However, during testing, if the timeProgressionFactor is defined and greater than 1,
|
||||
// the scaledMsPerDay value is decreased. This adjustment allows for simulating actions occurring
|
||||
// earlier in time.
|
||||
const scaledMsPerDay = scaleMsPerDay(timeProgressionFactor);
|
||||
|
||||
this.timeOptions = {
|
||||
expireOneDayEarlier,
|
||||
transitionOneDayEarlier,
|
||||
timeProgressionFactor,
|
||||
scaledMsPerDay,
|
||||
};
|
||||
}
|
||||
|
||||
getTimeOptions() {
|
||||
return this.timeOptions;
|
||||
}
|
||||
|
||||
_getAuthData() {
|
||||
return require(findConfigFile(process.env.S3AUTH_CONFIG || 'authdata.json'));
|
||||
return JSON.parse(fs.readFileSync(findConfigFile(process.env.S3AUTH_CONFIG || this.authdata), { encoding: 'utf-8' }));
|
||||
}
|
||||
|
||||
_configureBackends() {
|
||||
_configureBackends(config) {
|
||||
const backends = config.backends || {};
|
||||
/**
|
||||
* Configure the backends for Authentication, Data and Metadata.
|
||||
*/
|
||||
let auth = 'mem';
|
||||
let data = 'multiple';
|
||||
let metadata = 'file';
|
||||
let kms = 'file';
|
||||
let auth = backends.auth || 'mem';
|
||||
let data = backends.data || 'multiple';
|
||||
let metadata = backends.metadata || 'file';
|
||||
let kms = backends.kms || 'file';
|
||||
let quota = backends.quota || 'none';
|
||||
if (process.env.S3BACKEND) {
|
||||
const validBackends = ['mem', 'file', 'scality', 'cdmi'];
|
||||
assert(validBackends.indexOf(process.env.S3BACKEND) > -1,
|
||||
'bad environment variable: S3BACKEND environment variable ' +
|
||||
'should be one of mem/file/scality/cdmi'
|
||||
);
|
||||
auth = process.env.S3BACKEND;
|
||||
auth = process.env.S3BACKEND == 'scality' ? 'scality' : 'mem';
|
||||
data = process.env.S3BACKEND;
|
||||
metadata = process.env.S3BACKEND;
|
||||
kms = process.env.S3BACKEND;
|
||||
}
|
||||
if (process.env.S3VAULT) {
|
||||
auth = process.env.S3VAULT;
|
||||
auth = (auth === 'file' || auth === 'mem' || auth === 'cdmi' ? 'mem' : auth);
|
||||
}
|
||||
|
||||
if (auth === 'file' || auth === 'mem' || auth === 'cdmi') {
|
||||
// Auth only checks for 'mem' since mem === file
|
||||
auth = 'mem';
|
||||
let authData;
|
||||
if (process.env.SCALITY_ACCESS_KEY_ID &&
|
||||
process.env.SCALITY_SECRET_ACCESS_KEY) {
|
||||
process.env.SCALITY_SECRET_ACCESS_KEY) {
|
||||
authData = buildAuthDataAccount(
|
||||
process.env.SCALITY_ACCESS_KEY_ID,
|
||||
process.env.SCALITY_SECRET_ACCESS_KEY);
|
||||
process.env.SCALITY_ACCESS_KEY_ID,
|
||||
process.env.SCALITY_SECRET_ACCESS_KEY);
|
||||
} else {
|
||||
authData = this._getAuthData();
|
||||
}
|
||||
|
@ -1602,7 +1697,7 @@ class Config extends EventEmitter {
|
|||
throw new Error('bad config: invalid auth config file.');
|
||||
}
|
||||
this.authData = authData;
|
||||
} else if (auth === 'multiple') {
|
||||
} else if (auth === 'multiple') {
|
||||
const authData = this._getAuthData();
|
||||
if (validateAuthConfig(authData)) {
|
||||
throw new Error('bad config: invalid auth config file.');
|
||||
|
@ -1617,9 +1712,9 @@ class Config extends EventEmitter {
|
|||
'should be one of mem/file/scality/multiple'
|
||||
);
|
||||
data = process.env.S3DATA;
|
||||
}
|
||||
if (data === 'scality' || data === 'multiple') {
|
||||
data = 'multiple';
|
||||
if (data === 'scality' || data === 'multiple') {
|
||||
data = 'multiple';
|
||||
}
|
||||
}
|
||||
assert(this.locationConstraints !== undefined &&
|
||||
this.restEndpoints !== undefined,
|
||||
|
@ -1632,18 +1727,18 @@ class Config extends EventEmitter {
|
|||
if (process.env.S3KMS) {
|
||||
kms = process.env.S3KMS;
|
||||
}
|
||||
if (process.env.S3QUOTA) {
|
||||
quota = process.env.S3QUOTA;
|
||||
}
|
||||
this.backends = {
|
||||
auth,
|
||||
data,
|
||||
metadata,
|
||||
kms,
|
||||
quota,
|
||||
};
|
||||
}
|
||||
|
||||
_verifyRedisPassword(password) {
|
||||
return typeof password === 'string';
|
||||
}
|
||||
|
||||
setAuthDataAccounts(accounts) {
|
||||
this.authData.accounts = accounts;
|
||||
this.emit('authdata-update');
|
||||
|
@ -1766,10 +1861,19 @@ class Config extends EventEmitter {
|
|||
.update(instanceId)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
isQuotaEnabled() {
|
||||
return !!this.quotaEnabled;
|
||||
}
|
||||
|
||||
isQuotaInflightEnabled() {
|
||||
return this.quota.enableInflights;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseSproxydConfig,
|
||||
parseRedisConfig,
|
||||
locationConstraintAssert,
|
||||
ConfigObject: Config,
|
||||
config: new Config(),
|
||||
|
@ -1777,4 +1881,5 @@ module.exports = {
|
|||
bucketNotifAssert,
|
||||
azureGetStorageAccountName,
|
||||
azureGetLocationCredentials,
|
||||
azureArchiveLocationConstraintAssert,
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ const bucketDeleteEncryption = require('./bucketDeleteEncryption');
|
|||
const bucketDeleteWebsite = require('./bucketDeleteWebsite');
|
||||
const bucketDeleteLifecycle = require('./bucketDeleteLifecycle');
|
||||
const bucketDeletePolicy = require('./bucketDeletePolicy');
|
||||
const bucketDeleteQuota = require('./bucketDeleteQuota');
|
||||
const { bucketGet } = require('./bucketGet');
|
||||
const bucketGetACL = require('./bucketGetACL');
|
||||
const bucketGetCors = require('./bucketGetCors');
|
||||
|
@ -17,6 +18,7 @@ const bucketGetLifecycle = require('./bucketGetLifecycle');
|
|||
const bucketGetNotification = require('./bucketGetNotification');
|
||||
const bucketGetObjectLock = require('./bucketGetObjectLock');
|
||||
const bucketGetPolicy = require('./bucketGetPolicy');
|
||||
const bucketGetQuota = require('./bucketGetQuota');
|
||||
const bucketGetEncryption = require('./bucketGetEncryption');
|
||||
const bucketHead = require('./bucketHead');
|
||||
const { bucketPut } = require('./bucketPut');
|
||||
|
@ -33,6 +35,7 @@ const bucketPutNotification = require('./bucketPutNotification');
|
|||
const bucketPutEncryption = require('./bucketPutEncryption');
|
||||
const bucketPutPolicy = require('./bucketPutPolicy');
|
||||
const bucketPutObjectLock = require('./bucketPutObjectLock');
|
||||
const bucketUpdateQuota = require('./bucketUpdateQuota');
|
||||
const bucketGetReplication = require('./bucketGetReplication');
|
||||
const bucketDeleteReplication = require('./bucketDeleteReplication');
|
||||
const corsPreflight = require('./corsPreflight');
|
||||
|
@ -44,7 +47,7 @@ const metadataSearch = require('./metadataSearch');
|
|||
const { multiObjectDelete } = require('./multiObjectDelete');
|
||||
const multipartDelete = require('./multipartDelete');
|
||||
const objectCopy = require('./objectCopy');
|
||||
const objectDelete = require('./objectDelete');
|
||||
const { objectDelete } = require('./objectDelete');
|
||||
const objectDeleteTagging = require('./objectDeleteTagging');
|
||||
const objectGet = require('./objectGet');
|
||||
const objectGetACL = require('./objectGetACL');
|
||||
|
@ -64,8 +67,7 @@ const prepareRequestContexts
|
|||
= require('./apiUtils/authorization/prepareRequestContexts');
|
||||
const serviceGet = require('./serviceGet');
|
||||
const vault = require('../auth/vault');
|
||||
const websiteGet = require('./websiteGet');
|
||||
const websiteHead = require('./websiteHead');
|
||||
const website = require('./website');
|
||||
const writeContinue = require('../utilities/writeContinue');
|
||||
const validateQueryAndHeaders = require('../utilities/validateQueryAndHeaders');
|
||||
const parseCopySource = require('./apiUtils/object/parseCopySource');
|
||||
|
@ -83,6 +85,10 @@ const api = {
|
|||
// Attach the apiMethod method to the request, so it can used by monitoring in the server
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
request.apiMethod = apiMethod;
|
||||
// Array of end of API callbacks, used to perform some logic
|
||||
// at the end of an API.
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
request.finalizerHooks = [];
|
||||
|
||||
const actionLog = monitoringMap[apiMethod];
|
||||
if (!actionLog &&
|
||||
|
@ -117,6 +123,7 @@ const api = {
|
|||
// no need to check auth on website or cors preflight requests
|
||||
if (apiMethod === 'websiteGet' || apiMethod === 'websiteHead' ||
|
||||
apiMethod === 'corsPreflight') {
|
||||
request.actionImplicitDenies = false;
|
||||
return this[apiMethod](request, log, callback);
|
||||
}
|
||||
|
||||
|
@ -139,15 +146,25 @@ const api = {
|
|||
|
||||
const requestContexts = prepareRequestContexts(apiMethod, request,
|
||||
sourceBucket, sourceObject, sourceVersionId);
|
||||
// Extract all the _apiMethods and store them in an array
|
||||
const apiMethods = requestContexts ? requestContexts.map(context => context._apiMethod) : [];
|
||||
// Attach the names to the current request
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
request.apiMethods = apiMethods;
|
||||
|
||||
function checkAuthResults(authResults) {
|
||||
let returnTagCount = true;
|
||||
const isImplicitDeny = {};
|
||||
let isOnlyImplicitDeny = true;
|
||||
if (apiMethod === 'objectGet') {
|
||||
// first item checks s3:GetObject(Version) action
|
||||
if (!authResults[0].isAllowed) {
|
||||
if (!authResults[0].isAllowed && !authResults[0].isImplicit) {
|
||||
log.trace('get object authorization denial from Vault');
|
||||
return errors.AccessDenied;
|
||||
}
|
||||
// TODO add support for returnTagCount in the bucket policy
|
||||
// checks
|
||||
isImplicitDeny[authResults[0].action] = authResults[0].isImplicit;
|
||||
// second item checks s3:GetObject(Version)Tagging action
|
||||
if (!authResults[1].isAllowed) {
|
||||
log.trace('get tagging authorization denial ' +
|
||||
|
@ -156,25 +173,41 @@ const api = {
|
|||
}
|
||||
} else {
|
||||
for (let i = 0; i < authResults.length; i++) {
|
||||
if (!authResults[i].isAllowed) {
|
||||
isImplicitDeny[authResults[i].action] = true;
|
||||
if (!authResults[i].isAllowed && !authResults[i].isImplicit) {
|
||||
// Any explicit deny rejects the current API call
|
||||
log.trace('authorization denial from Vault');
|
||||
return errors.AccessDenied;
|
||||
}
|
||||
if (authResults[i].isAllowed) {
|
||||
// If the action is allowed, the result is not implicit
|
||||
// Deny.
|
||||
isImplicitDeny[authResults[i].action] = false;
|
||||
isOnlyImplicitDeny = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnTagCount;
|
||||
// These two APIs cannot use ACLs or Bucket Policies, hence, any
|
||||
// implicit deny from vault must be treated as an explicit deny.
|
||||
if ((apiMethod === 'bucketPut' || apiMethod === 'serviceGet') && isOnlyImplicitDeny) {
|
||||
return errors.AccessDenied;
|
||||
}
|
||||
return { returnTagCount, isImplicitDeny };
|
||||
}
|
||||
|
||||
return async.waterfall([
|
||||
next => auth.server.doAuth(
|
||||
request, log, (err, userInfo, authorizationResults, streamingV4Params) => {
|
||||
request, log, (err, userInfo, authorizationResults, streamingV4Params, infos) => {
|
||||
if (err) {
|
||||
// VaultClient returns standard errors, but the route requires
|
||||
// Arsenal errors
|
||||
const arsenalError = err.metadata ? err : errors[err.code] || errors.InternalError;
|
||||
log.trace('authentication error', { error: err });
|
||||
return next(err);
|
||||
return next(arsenalError);
|
||||
}
|
||||
return next(null, userInfo, authorizationResults, streamingV4Params);
|
||||
return next(null, userInfo, authorizationResults, streamingV4Params, infos);
|
||||
}, 's3', requestContexts),
|
||||
(userInfo, authorizationResults, streamingV4Params, next) => {
|
||||
(userInfo, authorizationResults, streamingV4Params, infos, next) => {
|
||||
const authNames = { accountName: userInfo.getAccountDisplayName() };
|
||||
if (userInfo.isRequesterAnIAMUser()) {
|
||||
authNames.userName = userInfo.getIAMdisplayName();
|
||||
|
@ -184,7 +217,7 @@ const api = {
|
|||
}
|
||||
log.addDefaultFields(authNames);
|
||||
if (apiMethod === 'objectPut' || apiMethod === 'objectPutPart') {
|
||||
return next(null, userInfo, authorizationResults, streamingV4Params);
|
||||
return next(null, userInfo, authorizationResults, streamingV4Params, infos);
|
||||
}
|
||||
// issue 100 Continue to the client
|
||||
writeContinue(request, response);
|
||||
|
@ -215,12 +248,12 @@ const api = {
|
|||
}
|
||||
// Convert array of post buffers into one string
|
||||
request.post = Buffer.concat(post, postLength).toString();
|
||||
return next(null, userInfo, authorizationResults, streamingV4Params);
|
||||
return next(null, userInfo, authorizationResults, streamingV4Params, infos);
|
||||
});
|
||||
return undefined;
|
||||
},
|
||||
// Tag condition keys require information from CloudServer for evaluation
|
||||
(userInfo, authorizationResults, streamingV4Params, next) => tagConditionKeyAuth(
|
||||
(userInfo, authorizationResults, streamingV4Params, infos, next) => tagConditionKeyAuth(
|
||||
authorizationResults,
|
||||
request,
|
||||
requestContexts,
|
||||
|
@ -231,33 +264,47 @@ const api = {
|
|||
log.trace('tag authentication error', { error: err });
|
||||
return next(err);
|
||||
}
|
||||
return next(null, userInfo, authResultsWithTags, streamingV4Params);
|
||||
return next(null, userInfo, authResultsWithTags, streamingV4Params, infos);
|
||||
},
|
||||
),
|
||||
], (err, userInfo, authorizationResults, streamingV4Params) => {
|
||||
], (err, userInfo, authorizationResults, streamingV4Params, infos) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
request.accountQuotas = infos?.accountQuota;
|
||||
if (authorizationResults) {
|
||||
const checkedResults = checkAuthResults(authorizationResults);
|
||||
if (checkedResults instanceof Error) {
|
||||
return callback(checkedResults);
|
||||
}
|
||||
returnTagCount = checkedResults;
|
||||
returnTagCount = checkedResults.returnTagCount;
|
||||
request.actionImplicitDenies = checkedResults.isImplicitDeny;
|
||||
} else {
|
||||
// create an object of keys apiMethods with all values to false:
|
||||
// for backward compatibility, all apiMethods are allowed by default
|
||||
// thus it is explicitly allowed, so implicit deny is false
|
||||
request.actionImplicitDenies = apiMethods.reduce((acc, curr) => {
|
||||
acc[curr] = false;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
const methodCallback = (err, ...results) => async.forEachLimit(request.finalizerHooks, 5,
|
||||
(hook, done) => hook(err, done),
|
||||
() => callback(err, ...results));
|
||||
|
||||
if (apiMethod === 'objectPut' || apiMethod === 'objectPutPart') {
|
||||
request._response = response;
|
||||
return this[apiMethod](userInfo, request, streamingV4Params,
|
||||
log, callback, authorizationResults);
|
||||
log, methodCallback, authorizationResults);
|
||||
}
|
||||
if (apiMethod === 'objectCopy' || apiMethod === 'objectPutCopyPart') {
|
||||
return this[apiMethod](userInfo, request, sourceBucket,
|
||||
sourceObject, sourceVersionId, log, callback);
|
||||
sourceObject, sourceVersionId, log, methodCallback);
|
||||
}
|
||||
if (apiMethod === 'objectGet') {
|
||||
return this[apiMethod](userInfo, request, returnTagCount, log, callback);
|
||||
}
|
||||
return this[apiMethod](userInfo, request, log, callback);
|
||||
return this[apiMethod](userInfo, request, log, methodCallback);
|
||||
});
|
||||
},
|
||||
bucketDelete,
|
||||
|
@ -284,11 +331,14 @@ const api = {
|
|||
bucketPutReplication,
|
||||
bucketGetReplication,
|
||||
bucketDeleteReplication,
|
||||
bucketDeleteQuota,
|
||||
bucketPutLifecycle,
|
||||
bucketUpdateQuota,
|
||||
bucketGetLifecycle,
|
||||
bucketDeleteLifecycle,
|
||||
bucketPutPolicy,
|
||||
bucketGetPolicy,
|
||||
bucketGetQuota,
|
||||
bucketDeletePolicy,
|
||||
bucketPutObjectLock,
|
||||
bucketPutNotification,
|
||||
|
@ -320,8 +370,8 @@ const api = {
|
|||
objectPutRetention,
|
||||
objectRestore,
|
||||
serviceGet,
|
||||
websiteGet,
|
||||
websiteHead,
|
||||
websiteGet: website,
|
||||
websiteHead: website,
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
|
|
|
@ -1,11 +1,23 @@
|
|||
const { evaluators, actionMaps, RequestContext } = require('arsenal').policies;
|
||||
const { evaluators, actionMaps, RequestContext, requestUtils } = require('arsenal').policies;
|
||||
const { errors } = require('arsenal');
|
||||
const { parseCIDR, isValid } = require('ipaddr.js');
|
||||
const constants = require('../../../../constants');
|
||||
const { config } = require('../../../Config');
|
||||
|
||||
const { allAuthedUsersId, bucketOwnerActions, logId, publicId } = constants;
|
||||
const {
|
||||
allAuthedUsersId,
|
||||
bucketOwnerActions,
|
||||
logId,
|
||||
publicId,
|
||||
arrayOfAllowed,
|
||||
assumedRoleArnResourceType,
|
||||
backbeatLifecycleSessionName,
|
||||
actionsToConsiderAsObjectPut,
|
||||
} = constants;
|
||||
|
||||
// whitelist buckets to allow public read on objects
|
||||
const publicReadBuckets = process.env.ALLOW_PUBLIC_READ_BUCKETS ?
|
||||
process.env.ALLOW_PUBLIC_READ_BUCKETS.split(',') : [];
|
||||
const publicReadBuckets = process.env.ALLOW_PUBLIC_READ_BUCKETS
|
||||
? process.env.ALLOW_PUBLIC_READ_BUCKETS.split(',') : [];
|
||||
|
||||
function getServiceAccountProperties(canonicalID) {
|
||||
const canonicalIDArray = canonicalID.split('/');
|
||||
|
@ -26,13 +38,41 @@ function isRequesterNonAccountUser(authInfo) {
|
|||
return authInfo.isRequesterAnIAMUser() || isRequesterASessionUser(authInfo);
|
||||
}
|
||||
|
||||
function checkBucketAcls(bucket, requestType, canonicalID) {
|
||||
/**
|
||||
* Checks the access control for a given bucket based on the request type and user's canonical ID.
|
||||
*
|
||||
* @param {Bucket} bucket - The bucket to check access control for.
|
||||
* @param {string} requestType - The list of s3 actions to check within the API call.
|
||||
* @param {string} canonicalID - The canonical ID of the user making the request.
|
||||
* @param {string} mainApiCall - The main API call (first item of the requestType).
|
||||
*
|
||||
* @returns {boolean} - Returns true if the user has the necessary access rights, otherwise false.
|
||||
*/
|
||||
|
||||
function checkBucketAcls(bucket, requestType, canonicalID, mainApiCall) {
|
||||
// Same logic applies on the Versioned APIs, so let's simplify it.
|
||||
let requestTypeParsed = requestType.endsWith('Version') ?
|
||||
requestType.slice(0, 'Version'.length * -1) : requestType;
|
||||
requestTypeParsed = actionsToConsiderAsObjectPut.includes(requestTypeParsed) ?
|
||||
'objectPut' : requestTypeParsed;
|
||||
const parsedMainApiCall = actionsToConsiderAsObjectPut.includes(mainApiCall) ?
|
||||
'objectPut' : mainApiCall;
|
||||
if (bucket.getOwner() === canonicalID) {
|
||||
return true;
|
||||
}
|
||||
if (parsedMainApiCall === 'objectGet') {
|
||||
if (requestTypeParsed === 'objectGetTagging') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (parsedMainApiCall === 'objectPut') {
|
||||
if (arrayOfAllowed.includes(requestTypeParsed)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const bucketAcl = bucket.getAcl();
|
||||
if (requestType === 'bucketGet' || requestType === 'bucketHead') {
|
||||
if (requestTypeParsed === 'bucketGet' || requestTypeParsed === 'bucketHead') {
|
||||
if (bucketAcl.Canned === 'public-read'
|
||||
|| bucketAcl.Canned === 'public-read-write'
|
||||
|| (bucketAcl.Canned === 'authenticated-read'
|
||||
|
@ -50,7 +90,7 @@ function checkBucketAcls(bucket, requestType, canonicalID) {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
if (requestType === 'bucketGetACL') {
|
||||
if (requestTypeParsed === 'bucketGetACL') {
|
||||
if ((bucketAcl.Canned === 'log-delivery-write'
|
||||
&& canonicalID === logId)
|
||||
|| bucketAcl.FULL_CONTROL.indexOf(canonicalID) > -1
|
||||
|
@ -66,7 +106,7 @@ function checkBucketAcls(bucket, requestType, canonicalID) {
|
|||
}
|
||||
}
|
||||
|
||||
if (requestType === 'bucketPutACL') {
|
||||
if (requestTypeParsed === 'bucketPutACL') {
|
||||
if (bucketAcl.FULL_CONTROL.indexOf(canonicalID) > -1
|
||||
|| bucketAcl.WRITE_ACP.indexOf(canonicalID) > -1) {
|
||||
return true;
|
||||
|
@ -80,11 +120,7 @@ function checkBucketAcls(bucket, requestType, canonicalID) {
|
|||
}
|
||||
}
|
||||
|
||||
if (requestType === 'bucketDelete' && bucket.getOwner() === canonicalID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (requestType === 'objectDelete' || requestType === 'objectPut') {
|
||||
if (requestTypeParsed === 'objectDelete' || requestTypeParsed === 'objectPut') {
|
||||
if (bucketAcl.Canned === 'public-read-write'
|
||||
|| bucketAcl.FULL_CONTROL.indexOf(canonicalID) > -1
|
||||
|| bucketAcl.WRITE.indexOf(canonicalID) > -1) {
|
||||
|
@ -104,25 +140,39 @@ function checkBucketAcls(bucket, requestType, canonicalID) {
|
|||
// objectPutACL, objectGetACL, objectHead or objectGet, the bucket
|
||||
// authorization check should just return true so can move on to check
|
||||
// rights at the object level.
|
||||
return (requestType === 'objectPutACL' || requestType === 'objectGetACL' ||
|
||||
requestType === 'objectGet' || requestType === 'objectHead');
|
||||
return (requestTypeParsed === 'objectPutACL' || requestTypeParsed === 'objectGetACL'
|
||||
|| requestTypeParsed === 'objectGet' || requestTypeParsed === 'objectHead');
|
||||
}
|
||||
|
||||
function checkObjectAcls(bucket, objectMD, requestType, canonicalID) {
|
||||
function checkObjectAcls(bucket, objectMD, requestType, canonicalID, requesterIsNotUser,
|
||||
isUserUnauthenticated, mainApiCall) {
|
||||
const bucketOwner = bucket.getOwner();
|
||||
const requestTypeParsed = actionsToConsiderAsObjectPut.includes(requestType) ?
|
||||
'objectPut' : requestType;
|
||||
const parsedMainApiCall = actionsToConsiderAsObjectPut.includes(mainApiCall) ?
|
||||
'objectPut' : mainApiCall;
|
||||
// acls don't distinguish between users and accounts, so both should be allowed
|
||||
if (bucketOwnerActions.includes(requestType)
|
||||
if (bucketOwnerActions.includes(requestTypeParsed)
|
||||
&& (bucketOwner === canonicalID)) {
|
||||
return true;
|
||||
}
|
||||
if (objectMD['owner-id'] === canonicalID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Backward compatibility
|
||||
if (parsedMainApiCall === 'objectGet') {
|
||||
if ((isUserUnauthenticated || (requesterIsNotUser && bucketOwner === objectMD['owner-id']))
|
||||
&& requestTypeParsed === 'objectGetTagging') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!objectMD.acl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requestType === 'objectGet' || requestType === 'objectHead') {
|
||||
if (requestTypeParsed === 'objectGet' || requestTypeParsed === 'objectHead') {
|
||||
if (objectMD.acl.Canned === 'public-read'
|
||||
|| objectMD.acl.Canned === 'public-read-write'
|
||||
|| (objectMD.acl.Canned === 'authenticated-read'
|
||||
|
@ -148,11 +198,11 @@ function checkObjectAcls(bucket, objectMD, requestType, canonicalID) {
|
|||
|
||||
// User is already authorized on the bucket for FULL_CONTROL or WRITE or
|
||||
// bucket has canned ACL public-read-write
|
||||
if (requestType === 'objectPut' || requestType === 'objectDelete') {
|
||||
if (requestTypeParsed === 'objectPut' || requestTypeParsed === 'objectDelete') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (requestType === 'objectPutACL') {
|
||||
if (requestTypeParsed === 'objectPutACL') {
|
||||
if ((objectMD.acl.Canned === 'bucket-owner-full-control'
|
||||
&& bucketOwner === canonicalID)
|
||||
|| objectMD.acl.FULL_CONTROL.indexOf(canonicalID) > -1
|
||||
|
@ -168,7 +218,7 @@ function checkObjectAcls(bucket, objectMD, requestType, canonicalID) {
|
|||
}
|
||||
}
|
||||
|
||||
if (requestType === 'objectGetACL') {
|
||||
if (requestTypeParsed === 'objectGetACL') {
|
||||
if ((objectMD.acl.Canned === 'bucket-owner-full-control'
|
||||
&& bucketOwner === canonicalID)
|
||||
|| objectMD.acl.FULL_CONTROL.indexOf(canonicalID) > -1
|
||||
|
@ -187,9 +237,9 @@ function checkObjectAcls(bucket, objectMD, requestType, canonicalID) {
|
|||
// allow public reads on buckets that are whitelisted for anonymous reads
|
||||
// TODO: remove this after bucket policies are implemented
|
||||
const bucketAcl = bucket.getAcl();
|
||||
const allowPublicReads = publicReadBuckets.includes(bucket.getName()) &&
|
||||
bucketAcl.Canned === 'public-read' &&
|
||||
(requestType === 'objectGet' || requestType === 'objectHead');
|
||||
const allowPublicReads = publicReadBuckets.includes(bucket.getName())
|
||||
&& bucketAcl.Canned === 'public-read'
|
||||
&& (requestTypeParsed === 'objectGet' || requestTypeParsed === 'objectHead');
|
||||
if (allowPublicReads) {
|
||||
return true;
|
||||
}
|
||||
|
@ -216,6 +266,20 @@ function _checkBucketPolicyResources(request, resource, log) {
|
|||
return evaluators.isResourceApplicable(requestContext, resource, log);
|
||||
}
|
||||
|
||||
function _checkBucketPolicyConditions(request, conditions, log) {
|
||||
const ip = request ? requestUtils.getClientIp(request, config) : undefined;
|
||||
if (!conditions) {
|
||||
return true;
|
||||
}
|
||||
// build request context from the request!
|
||||
const requestContext = new RequestContext(request.headers, request.query,
|
||||
request.bucketName, request.objectKey, ip,
|
||||
request.connection.encrypted, request.resourceType, 's3', null, null,
|
||||
null, null, null, null, null, null, null, null, null,
|
||||
request.objectLockRetentionDays);
|
||||
return evaluators.meetConditions(requestContext, conditions, log);
|
||||
}
|
||||
|
||||
function _getAccountId(arn) {
|
||||
// account or user arn is of format 'arn:aws:iam::<12-digit-acct-id>:etc...
|
||||
return arn.substr(13, 12);
|
||||
|
@ -260,11 +324,11 @@ function _checkPrincipals(canonicalID, arn, principal) {
|
|||
return false;
|
||||
}
|
||||
|
||||
function checkBucketPolicy(policy, requestType, canonicalID, arn, bucketOwner, log, request) {
|
||||
function checkBucketPolicy(policy, requestType, canonicalID, arn, bucketOwner, log, request, actionImplicitDenies) {
|
||||
let permission = 'defaultDeny';
|
||||
// if requester is user within bucket owner account, actions should be
|
||||
// allowed unless explicitly denied (assumes allowed by IAM policy)
|
||||
if (bucketOwner === canonicalID) {
|
||||
if (bucketOwner === canonicalID && actionImplicitDenies[requestType] === false) {
|
||||
permission = 'allow';
|
||||
}
|
||||
let copiedStatement = JSON.parse(JSON.stringify(policy.Statement));
|
||||
|
@ -273,18 +337,13 @@ function checkBucketPolicy(policy, requestType, canonicalID, arn, bucketOwner, l
|
|||
const principalMatch = _checkPrincipals(canonicalID, arn, s.Principal);
|
||||
const actionMatch = _checkBucketPolicyActions(requestType, s.Action, log);
|
||||
const resourceMatch = _checkBucketPolicyResources(request, s.Resource, log);
|
||||
const conditionsMatch = _checkBucketPolicyConditions(request, s.Condition, log);
|
||||
|
||||
console.log('------------------', s);
|
||||
console.log(principalMatch, s.Principal, arn, canonicalID);
|
||||
console.log(actionMatch, s.Action, requestType);
|
||||
console.log(resourceMatch, s.Resource, request);
|
||||
|
||||
|
||||
if (principalMatch && actionMatch && resourceMatch && s.Effect === 'Deny') {
|
||||
if (principalMatch && actionMatch && resourceMatch && conditionsMatch && s.Effect === 'Deny') {
|
||||
// explicit deny trumps any allows, so return immediately
|
||||
return 'explicitDeny';
|
||||
}
|
||||
if (principalMatch && actionMatch && resourceMatch && s.Effect === 'Allow') {
|
||||
if (principalMatch && actionMatch && resourceMatch && conditionsMatch && s.Effect === 'Allow') {
|
||||
permission = 'allow';
|
||||
}
|
||||
copiedStatement = copiedStatement.splice(1);
|
||||
|
@ -292,80 +351,141 @@ function checkBucketPolicy(policy, requestType, canonicalID, arn, bucketOwner, l
|
|||
return permission;
|
||||
}
|
||||
|
||||
function isBucketAuthorized(bucket, requestType, canonicalID, authInfo, log, request) {
|
||||
// Check to see if user is authorized to perform a
|
||||
// particular action on bucket based on ACLs.
|
||||
// TODO: Add IAM checks
|
||||
let requesterIsNotUser = true;
|
||||
let arn = null;
|
||||
if (authInfo) {
|
||||
requesterIsNotUser = !isRequesterNonAccountUser(authInfo);
|
||||
arn = authInfo.getArn();
|
||||
}
|
||||
// if the bucket owner is an account, users should not have default access
|
||||
if (((bucket.getOwner() === canonicalID) && requesterIsNotUser)
|
||||
|| isServiceAccount(canonicalID)) {
|
||||
return true;
|
||||
}
|
||||
const aclPermission = checkBucketAcls(bucket, requestType, canonicalID);
|
||||
function processBucketPolicy(requestType, bucket, canonicalID, arn, bucketOwner, log,
|
||||
request, aclPermission, results, actionImplicitDenies) {
|
||||
const bucketPolicy = bucket.getBucketPolicy();
|
||||
let processedResult = results[requestType];
|
||||
if (!bucketPolicy) {
|
||||
return aclPermission;
|
||||
processedResult = actionImplicitDenies[requestType] === false && aclPermission;
|
||||
} else {
|
||||
const bucketPolicyPermission = checkBucketPolicy(bucketPolicy, requestType, canonicalID, arn,
|
||||
bucketOwner, log, request, actionImplicitDenies);
|
||||
|
||||
if (bucketPolicyPermission === 'explicitDeny') {
|
||||
processedResult = false;
|
||||
} else if (bucketPolicyPermission === 'allow') {
|
||||
processedResult = true;
|
||||
} else {
|
||||
processedResult = actionImplicitDenies[requestType] === false && aclPermission;
|
||||
}
|
||||
}
|
||||
const bucketPolicyPermission = checkBucketPolicy(bucketPolicy, requestType,
|
||||
canonicalID, arn, bucket.getOwner(), log, request);
|
||||
if (bucketPolicyPermission === 'explicitDeny') {
|
||||
return false;
|
||||
}
|
||||
return (aclPermission || (bucketPolicyPermission === 'allow'));
|
||||
return processedResult;
|
||||
}
|
||||
|
||||
function isObjAuthorized(bucket, objectMD, requestType, canonicalID, authInfo, log, request) {
|
||||
const bucketOwner = bucket.getOwner();
|
||||
if (!objectMD) {
|
||||
// User is already authorized on the bucket for FULL_CONTROL or WRITE or
|
||||
// bucket has canned ACL public-read-write
|
||||
if (requestType === 'objectPut' || requestType === 'objectDelete') {
|
||||
return true;
|
||||
function isBucketAuthorized(bucket, requestTypesInput, canonicalID, authInfo, log, request,
|
||||
actionImplicitDeniesInput = {}, isWebsite = false) {
|
||||
const requestTypes = Array.isArray(requestTypesInput) ? requestTypesInput : [requestTypesInput];
|
||||
const actionImplicitDenies = !actionImplicitDeniesInput ? {} : actionImplicitDeniesInput;
|
||||
const mainApiCall = requestTypes[0];
|
||||
const results = {};
|
||||
return requestTypes.every(_requestType => {
|
||||
// By default, all missing actions are defined as allowed from IAM, to be
|
||||
// backward compatible
|
||||
actionImplicitDenies[_requestType] = actionImplicitDenies[_requestType] || false;
|
||||
// Check to see if user is authorized to perform a
|
||||
// particular action on bucket based on ACLs.
|
||||
// TODO: Add IAM checks
|
||||
let requesterIsNotUser = true;
|
||||
let arn = null;
|
||||
if (authInfo) {
|
||||
requesterIsNotUser = !isRequesterNonAccountUser(authInfo);
|
||||
arn = authInfo.getArn();
|
||||
}
|
||||
// check bucket has read access
|
||||
// 'bucketGet' covers listObjects and listMultipartUploads, bucket read actions
|
||||
return isBucketAuthorized(bucket, 'bucketGet', canonicalID, authInfo, log, request);
|
||||
}
|
||||
let requesterIsNotUser = true;
|
||||
let arn = null;
|
||||
if (authInfo) {
|
||||
requesterIsNotUser = !isRequesterNonAccountUser(authInfo);
|
||||
arn = authInfo.getArn();
|
||||
}
|
||||
if (objectMD['owner-id'] === canonicalID && requesterIsNotUser) {
|
||||
return true;
|
||||
}
|
||||
// if the bucket owner is an account, users should not have default access
|
||||
if ((bucket.getOwner() === canonicalID) && requesterIsNotUser || isServiceAccount(canonicalID)) {
|
||||
results[_requestType] = actionImplicitDenies[_requestType] === false;
|
||||
return results[_requestType];
|
||||
}
|
||||
const aclPermission = checkBucketAcls(bucket, _requestType, canonicalID, mainApiCall);
|
||||
// In case of error bucket access is checked with bucketGet
|
||||
// For website, bucket policy only uses objectGet and ignores bucketGet
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteAccessPermissionsReqd.html
|
||||
// bucketGet should be used to check acl but switched to objectGet for bucket policy
|
||||
if (isWebsite && _requestType === 'bucketGet') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
_requestType = 'objectGet';
|
||||
actionImplicitDenies.objectGet = actionImplicitDenies.objectGet || false;
|
||||
}
|
||||
return processBucketPolicy(_requestType, bucket, canonicalID, arn, bucket.getOwner(), log,
|
||||
request, aclPermission, results, actionImplicitDenies);
|
||||
});
|
||||
}
|
||||
|
||||
if (isServiceAccount(canonicalID)) {
|
||||
return true;
|
||||
}
|
||||
// account is authorized if:
|
||||
// - requesttype is included in bucketOwnerActions and
|
||||
// - account is the bucket owner
|
||||
// - requester is account, not user
|
||||
if (bucketOwnerActions.includes(requestType)
|
||||
function evaluateBucketPolicyWithIAM(bucket, requestTypesInput, canonicalID, authInfo, actionImplicitDeniesInput = {},
|
||||
log, request) {
|
||||
const requestTypes = Array.isArray(requestTypesInput) ? requestTypesInput : [requestTypesInput];
|
||||
const actionImplicitDenies = !actionImplicitDeniesInput ? {} : actionImplicitDeniesInput;
|
||||
const results = {};
|
||||
return requestTypes.every(_requestType => {
|
||||
// By default, all missing actions are defined as allowed from IAM, to be
|
||||
// backward compatible
|
||||
actionImplicitDenies[_requestType] = actionImplicitDenies[_requestType] || false;
|
||||
let arn = null;
|
||||
if (authInfo) {
|
||||
arn = authInfo.getArn();
|
||||
}
|
||||
return processBucketPolicy(_requestType, bucket, canonicalID, arn, bucket.getOwner(), log,
|
||||
request, true, results, actionImplicitDenies);
|
||||
});
|
||||
}
|
||||
|
||||
function isObjAuthorized(bucket, objectMD, requestTypesInput, canonicalID, authInfo, log, request,
|
||||
actionImplicitDeniesInput = {}, isWebsite = false) {
|
||||
const requestTypes = Array.isArray(requestTypesInput) ? requestTypesInput : [requestTypesInput];
|
||||
const actionImplicitDenies = !actionImplicitDeniesInput ? {} : actionImplicitDeniesInput;
|
||||
const results = {};
|
||||
const mainApiCall = requestTypes[0];
|
||||
return requestTypes.every(_requestType => {
|
||||
// By default, all missing actions are defined as allowed from IAM, to be
|
||||
// backward compatible
|
||||
actionImplicitDenies[_requestType] = actionImplicitDenies[_requestType] || false;
|
||||
const parsedMethodName = _requestType.endsWith('Version')
|
||||
? _requestType.slice(0, -7) : _requestType;
|
||||
const bucketOwner = bucket.getOwner();
|
||||
if (!objectMD) {
|
||||
// check bucket has read access
|
||||
// 'bucketGet' covers listObjects and listMultipartUploads, bucket read actions
|
||||
let permission = 'bucketGet';
|
||||
if (actionsToConsiderAsObjectPut.includes(_requestType)) {
|
||||
permission = 'objectPut';
|
||||
}
|
||||
results[_requestType] = isBucketAuthorized(bucket, permission, canonicalID, authInfo, log, request,
|
||||
actionImplicitDenies, isWebsite);
|
||||
// User is already authorized on the bucket for FULL_CONTROL or WRITE or
|
||||
// bucket has canned ACL public-read-write
|
||||
if ((parsedMethodName === 'objectPut' || parsedMethodName === 'objectDelete')
|
||||
&& results[_requestType] === false) {
|
||||
results[_requestType] = actionImplicitDenies[_requestType] === false;
|
||||
}
|
||||
return results[_requestType];
|
||||
}
|
||||
let requesterIsNotUser = true;
|
||||
let arn = null;
|
||||
let isUserUnauthenticated = false;
|
||||
if (authInfo) {
|
||||
requesterIsNotUser = !isRequesterNonAccountUser(authInfo);
|
||||
arn = authInfo.getArn();
|
||||
isUserUnauthenticated = arn === undefined;
|
||||
}
|
||||
if (objectMD['owner-id'] === canonicalID && requesterIsNotUser || isServiceAccount(canonicalID)) {
|
||||
results[_requestType] = actionImplicitDenies[_requestType] === false;
|
||||
return results[_requestType];
|
||||
}
|
||||
// account is authorized if:
|
||||
// - requesttype is included in bucketOwnerActions and
|
||||
// - account is the bucket owner
|
||||
// - requester is account, not user
|
||||
if (bucketOwnerActions.includes(parsedMethodName)
|
||||
&& (bucketOwner === canonicalID)
|
||||
&& requesterIsNotUser) {
|
||||
return true;
|
||||
}
|
||||
const aclPermission = checkObjectAcls(bucket, objectMD, requestType,
|
||||
canonicalID);
|
||||
const bucketPolicy = bucket.getBucketPolicy();
|
||||
if (!bucketPolicy) {
|
||||
return aclPermission;
|
||||
}
|
||||
const bucketPolicyPermission = checkBucketPolicy(bucketPolicy, requestType,
|
||||
canonicalID, arn, bucket.getOwner(), log, request);
|
||||
if (bucketPolicyPermission === 'explicitDeny') {
|
||||
return false;
|
||||
}
|
||||
return (aclPermission || (bucketPolicyPermission === 'allow'));
|
||||
results[_requestType] = actionImplicitDenies[_requestType] === false;
|
||||
return results[_requestType];
|
||||
}
|
||||
const aclPermission = checkObjectAcls(bucket, objectMD, parsedMethodName,
|
||||
canonicalID, requesterIsNotUser, isUserUnauthenticated, mainApiCall);
|
||||
return processBucketPolicy(_requestType, bucket, canonicalID, arn, bucketOwner,
|
||||
log, request, aclPermission, results, actionImplicitDenies);
|
||||
});
|
||||
}
|
||||
|
||||
function _checkResource(resource, bucketArn) {
|
||||
|
@ -374,8 +494,7 @@ function _checkResource(resource, bucketArn) {
|
|||
}
|
||||
if (resource.includes('/')) {
|
||||
const rSubs = resource.split('/');
|
||||
const result = rSubs[0] === bucketArn;
|
||||
return result
|
||||
return rSubs[0] === bucketArn;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -395,6 +514,117 @@ function validatePolicyResource(bucketName, policy) {
|
|||
});
|
||||
}
|
||||
|
||||
function checkIp(value) {
|
||||
const errString = 'Invalid IP address in Conditions';
|
||||
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
// these preliminary checks are validating the provided
|
||||
// ip address against ipaddr.js, the library we use when
|
||||
// evaluating IP condition keys. It ensures compatibility,
|
||||
// but additional checks are required to enforce the right
|
||||
// notation (e.g., xxx.xxx.xxx.xxx/xx for IPv4). Otherwise,
|
||||
// we would accept different ip formats, which is not
|
||||
// standard in an AWS use case.
|
||||
try {
|
||||
try {
|
||||
parseCIDR(values[i]);
|
||||
} catch (err) {
|
||||
isValid(values[i]);
|
||||
}
|
||||
} catch (err) {
|
||||
return errString;
|
||||
}
|
||||
|
||||
// Apply the existing IP validation logic to each element
|
||||
const validateIpRegex = ip => {
|
||||
if (constants.ipv4Regex.test(ip)) {
|
||||
return ip.split('.').every(part => parseInt(part, 10) <= 255);
|
||||
}
|
||||
if (constants.ipv6Regex.test(ip)) {
|
||||
return ip.split(':').every(part => part.length <= 4);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (validateIpRegex(values[i]) !== true) {
|
||||
return errString;
|
||||
}
|
||||
}
|
||||
|
||||
// If the function hasn't returned by now, all elements are valid
|
||||
return null;
|
||||
}
|
||||
|
||||
// This function checks all bucket policy conditions if the values provided
|
||||
// are valid for the condition type. If not it returns a relevant Malformed policy error string
|
||||
function validatePolicyConditions(policy) {
|
||||
const validConditions = [
|
||||
{ conditionKey: 'aws:SourceIp', conditionValueTypeChecker: checkIp },
|
||||
{ conditionKey: 's3:object-lock-remaining-retention-days' },
|
||||
];
|
||||
// keys where value type does not seem to be checked by AWS:
|
||||
// - s3:object-lock-remaining-retention-days
|
||||
|
||||
if (!policy.Statement || !Array.isArray(policy.Statement) || policy.Statement.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// there can be multiple statements in the policy, each with a Condition enclosure
|
||||
for (let i = 0; i < policy.Statement.length; i++) {
|
||||
const s = policy.Statement[i];
|
||||
if (s.Condition) {
|
||||
const conditionOperators = Object.keys(s.Condition);
|
||||
// there can be multiple condition operations in the Condition enclosure
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const conditionOperator of conditionOperators) {
|
||||
const conditionKey = Object.keys(s.Condition[conditionOperator])[0];
|
||||
const conditionValue = s.Condition[conditionOperator][conditionKey];
|
||||
const validCondition = validConditions.find(validCondition =>
|
||||
validCondition.conditionKey === conditionKey
|
||||
);
|
||||
// AWS returns does not return an error if the condition starts with 'aws:'
|
||||
// so we reproduce this behaviour
|
||||
if (!validCondition && !conditionKey.startsWith('aws:')) {
|
||||
return errors.MalformedPolicy.customizeDescription('Policy has an invalid condition key');
|
||||
}
|
||||
if (validCondition && validCondition.conditionValueTypeChecker) {
|
||||
const conditionValueTypeError = validCondition.conditionValueTypeChecker(conditionValue);
|
||||
if (conditionValueTypeError) {
|
||||
return errors.MalformedPolicy.customizeDescription(conditionValueTypeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/** isLifecycleSession - check if it is the Lifecycle assumed role session arn.
|
||||
* @param {string} arn - Amazon resource name - example:
|
||||
* arn:aws:sts::257038443293:assumed-role/rolename/backbeat-lifecycle
|
||||
* @return {boolean} true if Lifecycle assumed role session arn, false if not.
|
||||
*/
|
||||
function isLifecycleSession(arn) {
|
||||
if (!arn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const arnSplits = arn.split(':');
|
||||
const service = arnSplits[2];
|
||||
|
||||
const resourceNames = arnSplits[arnSplits.length - 1].split('/');
|
||||
|
||||
const resourceType = resourceNames[0];
|
||||
const sessionName = resourceNames[resourceNames.length - 1];
|
||||
|
||||
return (service === 'sts'
|
||||
&& resourceType === assumedRoleArnResourceType
|
||||
&& sessionName === backbeatLifecycleSessionName);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isBucketAuthorized,
|
||||
isObjAuthorized,
|
||||
|
@ -405,4 +635,7 @@ module.exports = {
|
|||
checkBucketAcls,
|
||||
checkObjectAcls,
|
||||
validatePolicyResource,
|
||||
validatePolicyConditions,
|
||||
isLifecycleSession,
|
||||
evaluateBucketPolicyWithIAM,
|
||||
};
|
||||
|
|
|
@ -52,7 +52,7 @@ function prepareRequestContexts(apiMethod, request, sourceBucket,
|
|||
apiMethod, 's3');
|
||||
}
|
||||
|
||||
if (apiMethod === 'multiObjectDelete' || apiMethod === 'bucketPut') {
|
||||
if (apiMethod === 'bucketPut') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,17 @@ function prepareRequestContexts(apiMethod, request, sourceBucket,
|
|||
|
||||
const requestContexts = [];
|
||||
|
||||
if (apiMethodAfterVersionCheck === 'objectCopy'
|
||||
if (apiMethod === 'multiObjectDelete') {
|
||||
// MultiObjectDelete does not require any authorization when evaluating
|
||||
// the API. Instead, we authorize each object passed.
|
||||
// But in order to get any relevant information from the authorization service
|
||||
// for example, the account quota, we must send a request context object
|
||||
// with no `specificResource`. We expect the result to be an implicit deny.
|
||||
// In the API, we then ignore these authorization results, and we can use
|
||||
// any information returned, e.g., the quota.
|
||||
const requestContextMultiObjectDelete = generateRequestContext('objectDelete');
|
||||
requestContexts.push(requestContextMultiObjectDelete);
|
||||
} else if (apiMethodAfterVersionCheck === 'objectCopy'
|
||||
|| apiMethodAfterVersionCheck === 'objectPutCopyPart') {
|
||||
const objectGetAction = sourceVersionId ? 'objectGetVersion' :
|
||||
'objectGet';
|
||||
|
|
|
@ -24,7 +24,7 @@ function _deleteMPUbucket(destinationBucketName, log, cb) {
|
|||
});
|
||||
}
|
||||
|
||||
function _deleteOngoingMPUs(authInfo, bucketName, bucketMD, mpus, log, cb) {
|
||||
function _deleteOngoingMPUs(authInfo, bucketName, bucketMD, mpus, request, log, cb) {
|
||||
async.mapLimit(mpus, 1, (mpu, next) => {
|
||||
const splitterChar = mpu.key.includes(oldSplitter) ?
|
||||
oldSplitter : splitter;
|
||||
|
@ -40,7 +40,7 @@ function _deleteOngoingMPUs(authInfo, bucketName, bucketMD, mpus, log, cb) {
|
|||
byteLength: partSizeSum,
|
||||
});
|
||||
next(err);
|
||||
});
|
||||
}, request);
|
||||
}, cb);
|
||||
}
|
||||
/**
|
||||
|
@ -49,11 +49,13 @@ function _deleteOngoingMPUs(authInfo, bucketName, bucketMD, mpus, log, cb) {
|
|||
* @param {object} bucketMD - bucket attributes/metadata
|
||||
* @param {string} bucketName - bucket in which objectMetadata is stored
|
||||
* @param {string} canonicalID - account canonicalID of requester
|
||||
* @param {object} request - request object given by router
|
||||
* including normalized headers
|
||||
* @param {object} log - Werelogs logger
|
||||
* @param {function} cb - callback from async.waterfall in bucketDelete
|
||||
* @return {undefined}
|
||||
*/
|
||||
function deleteBucket(authInfo, bucketMD, bucketName, canonicalID, log, cb) {
|
||||
function deleteBucket(authInfo, bucketMD, bucketName, canonicalID, request, log, cb) {
|
||||
log.trace('deleting bucket from metadata');
|
||||
assert.strictEqual(typeof bucketName, 'string');
|
||||
assert.strictEqual(typeof canonicalID, 'string');
|
||||
|
@ -100,7 +102,7 @@ function deleteBucket(authInfo, bucketMD, bucketName, canonicalID, log, cb) {
|
|||
}
|
||||
if (objectsListRes.Contents.length) {
|
||||
return _deleteOngoingMPUs(authInfo, bucketName,
|
||||
bucketMD, objectsListRes.Contents, log, err => {
|
||||
bucketMD, objectsListRes.Contents, request, log, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
|
|
@ -30,6 +30,9 @@ function bucketShield(bucket, requestType) {
|
|||
// Otherwise return an error to the client
|
||||
if ((bucket.hasDeletedFlag() || bucket.hasTransientFlag()) &&
|
||||
(requestType !== 'objectPut' &&
|
||||
requestType !== 'initiateMultipartUpload' &&
|
||||
requestType !== 'objectPutPart' &&
|
||||
requestType !== 'completeMultipartUpload' &&
|
||||
requestType !== 'bucketPutACL' &&
|
||||
requestType !== 'bucketDelete')) {
|
||||
return true;
|
||||
|
|
|
@ -3,7 +3,7 @@ const async = require('async');
|
|||
const constants = require('../../../../constants');
|
||||
const { data } = require('../../../data/wrapper');
|
||||
const locationConstraintCheck = require('../object/locationConstraintCheck');
|
||||
const { metadataValidateBucketAndObj } =
|
||||
const { standardMetadataValidateBucketAndObj } =
|
||||
require('../../../metadata/metadataUtils');
|
||||
const services = require('../../../services');
|
||||
|
||||
|
@ -14,7 +14,7 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
|
|||
bucketName,
|
||||
objectKey,
|
||||
uploadId,
|
||||
preciseRequestType: 'multipartDelete',
|
||||
preciseRequestType: request.apiMethods || 'multipartDelete',
|
||||
request,
|
||||
};
|
||||
// For validating the request at the destinationBucket level
|
||||
|
@ -22,10 +22,11 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
|
|||
// but the requestType is the more general 'objectDelete'
|
||||
const metadataValParams = Object.assign({}, metadataValMPUparams);
|
||||
metadataValParams.requestType = 'objectPut';
|
||||
const authzIdentityResult = request ? request.actionImplicitDenies : false;
|
||||
|
||||
async.waterfall([
|
||||
function checkDestBucketVal(next) {
|
||||
metadataValidateBucketAndObj(metadataValParams, log,
|
||||
standardMetadataValidateBucketAndObj(metadataValParams, authzIdentityResult, log,
|
||||
(err, destinationBucket) => {
|
||||
if (err) {
|
||||
return next(err, destinationBucket);
|
||||
|
@ -56,9 +57,14 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
|
|||
function abortExternalMpu(mpuBucket, mpuOverviewObj, destBucket,
|
||||
next) {
|
||||
const location = mpuOverviewObj.controllingLocationConstraint;
|
||||
const originalIdentityAuthzResults = request.actionImplicitDenies;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
delete request.actionImplicitDenies;
|
||||
return data.abortMPU(objectKey, uploadId, location, bucketName,
|
||||
request, destBucket, locationConstraintCheck, log,
|
||||
(err, skipDataDelete) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
request.actionImplicitDenies = originalIdentityAuthzResults;
|
||||
if (err) {
|
||||
return next(err, destBucket);
|
||||
}
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
* Code based on Yutaka Oishi (Fujifilm) contributions
|
||||
* Date: 11 Sep 2020
|
||||
*/
|
||||
const ObjectMDArchive = require('arsenal').models.ObjectMDArchive;
|
||||
const { ObjectMDArchive } = require('arsenal').models;
|
||||
const errors = require('arsenal').errors;
|
||||
const { config } = require('../../../Config');
|
||||
const { locationConstraints } = config;
|
||||
|
||||
const { scaledMsPerDay } = config.getTimeOptions();
|
||||
|
||||
/**
|
||||
* Get response header "x-amz-restore"
|
||||
* Be called by objectHead.js
|
||||
|
@ -32,7 +34,6 @@ function getAmzRestoreResHeader(objMD) {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if restore can be done.
|
||||
*
|
||||
|
@ -41,6 +42,23 @@ function getAmzRestoreResHeader(objMD) {
|
|||
* @return {ArsenalError|undefined} - undefined if the conditions for RestoreObject are fulfilled
|
||||
*/
|
||||
function _validateStartRestore(objectMD, log) {
|
||||
if (objectMD.archive?.restoreCompletedAt) {
|
||||
if (new Date(objectMD.archive?.restoreWillExpireAt) < new Date(Date.now())) {
|
||||
// return InvalidObjectState error if the restored object is expired
|
||||
// but restore info md of this object has not yet been cleared
|
||||
log.debug('The restored object already expired.',
|
||||
{
|
||||
archive: objectMD.archive,
|
||||
method: '_validateStartRestore',
|
||||
});
|
||||
return errors.InvalidObjectState;
|
||||
}
|
||||
|
||||
// If object is already restored, no further check is needed
|
||||
// Furthermore, we cannot check if the location is cold, as the `dataStoreName` would have
|
||||
// been reset.
|
||||
return undefined;
|
||||
}
|
||||
const isLocationCold = locationConstraints[objectMD.dataStoreName]?.isCold;
|
||||
if (!isLocationCold) {
|
||||
// return InvalidObjectState error if the object is not in cold storage,
|
||||
|
@ -52,18 +70,7 @@ function _validateStartRestore(objectMD, log) {
|
|||
});
|
||||
return errors.InvalidObjectState;
|
||||
}
|
||||
if (objectMD.archive?.restoreCompletedAt
|
||||
&& new Date(objectMD.archive?.restoreWillExpireAt) < new Date(Date.now())) {
|
||||
// return InvalidObjectState error if the restored object is expired
|
||||
// but restore info md of this object has not yet been cleared
|
||||
log.debug('The restored object already expired.',
|
||||
{
|
||||
archive: objectMD.archive,
|
||||
method: '_validateStartRestore',
|
||||
});
|
||||
return errors.InvalidObjectState;
|
||||
}
|
||||
if (objectMD.archive?.restoreRequestedAt && !objectMD.archive?.restoreCompletedAt) {
|
||||
if (objectMD.archive?.restoreRequestedAt) {
|
||||
// return RestoreAlreadyInProgress error if the object is currently being restored
|
||||
// check if archive.restoreRequestAt exists and archive.restoreCompletedAt not yet exists
|
||||
log.debug('The object is currently being restored.',
|
||||
|
@ -120,22 +127,36 @@ function validatePutVersionId(objMD, versionId, log) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if the object is already restored
|
||||
* Check if the object is already restored, and update the expiration date accordingly:
|
||||
* > After restoring an archived object, you can update the restoration period by reissuing the
|
||||
* > request with a new period. Amazon S3 updates the restoration period relative to the current
|
||||
* > time.
|
||||
*
|
||||
* @param {ObjectMD} objectMD - object metadata
|
||||
* @param {object} log - werelogs logger
|
||||
* @return {boolean} - true if the object is already restored
|
||||
*/
|
||||
function isObjectAlreadyRestored(objectMD, log) {
|
||||
// check if restoreCompletedAt field exists
|
||||
// and archive.restoreWillExpireAt > current time
|
||||
const isObjectAlreadyRestored = objectMD.archive?.restoreCompletedAt
|
||||
&& new Date(objectMD.archive?.restoreWillExpireAt) >= new Date(Date.now());
|
||||
log.debug('The restore status of the object.',
|
||||
{
|
||||
isObjectAlreadyRestored,
|
||||
method: 'isObjectAlreadyRestored'
|
||||
});
|
||||
function _updateObjectExpirationDate(objectMD, log) {
|
||||
// Check if restoreCompletedAt field exists
|
||||
// Normally, we should check `archive.restoreWillExpireAt > current time`; however this is
|
||||
// checked earlier in the process, so checking again here would create weird states
|
||||
const isObjectAlreadyRestored = !!objectMD.archive.restoreCompletedAt;
|
||||
log.debug('The restore status of the object.', {
|
||||
isObjectAlreadyRestored,
|
||||
method: 'isObjectAlreadyRestored'
|
||||
});
|
||||
if (isObjectAlreadyRestored) {
|
||||
const expiryDate = new Date(objectMD.archive.restoreRequestedAt);
|
||||
expiryDate.setTime(expiryDate.getTime() + (objectMD.archive.restoreRequestedDays * scaledMsPerDay));
|
||||
|
||||
/* eslint-disable no-param-reassign */
|
||||
objectMD.archive.restoreWillExpireAt = expiryDate;
|
||||
objectMD['x-amz-restore'] = {
|
||||
'ongoing-request': false,
|
||||
'expiry-date': expiryDate,
|
||||
};
|
||||
/* eslint-enable no-param-reassign */
|
||||
}
|
||||
return isObjectAlreadyRestored;
|
||||
}
|
||||
|
||||
|
@ -195,12 +216,32 @@ function startRestore(objectMD, restoreParam, log, cb) {
|
|||
if (updateResultError) {
|
||||
return cb(updateResultError);
|
||||
}
|
||||
return cb(null, isObjectAlreadyRestored(objectMD, log));
|
||||
const isObjectAlreadyRestored = _updateObjectExpirationDate(objectMD, log);
|
||||
return cb(null, isObjectAlreadyRestored);
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if object data is available or if it's in cold storage
|
||||
* @param {ObjectMD} objMD Object metadata
|
||||
* @returns {ArsenalError|null} error if object data is not available
|
||||
*/
|
||||
function verifyColdObjectAvailable(objMD) {
|
||||
// return error when object is cold
|
||||
if (objMD.archive &&
|
||||
// Object is in cold backend
|
||||
(!objMD.archive.restoreRequestedAt ||
|
||||
// Object is being restored
|
||||
(objMD.archive.restoreRequestedAt && !objMD.archive.restoreCompletedAt))) {
|
||||
const err = errors.InvalidObjectState
|
||||
.customizeDescription('The operation is not valid for the object\'s storage class');
|
||||
return err;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startRestore,
|
||||
getAmzRestoreResHeader,
|
||||
validatePutVersionId,
|
||||
verifyColdObjectAvailable,
|
||||
};
|
||||
|
|
|
@ -5,7 +5,6 @@ const getMetaHeaders = s3middleware.userMetadata.getMetaHeaders;
|
|||
const constants = require('../../../../constants');
|
||||
const { data } = require('../../../data/wrapper');
|
||||
const services = require('../../../services');
|
||||
const logger = require('../../../utilities/logger');
|
||||
const { dataStore } = require('./storeObject');
|
||||
const locationConstraintCheck = require('./locationConstraintCheck');
|
||||
const { versioningPreprocessing, overwritingVersioning } = require('./versioning');
|
||||
|
@ -21,7 +20,7 @@ const externalVersioningErrorMessage = 'We do not currently support putting ' +
|
|||
'a versioned object to a location-constraint of type Azure or GCP.';
|
||||
|
||||
function _storeInMDandDeleteData(bucketName, dataGetInfo, cipherBundle,
|
||||
metadataStoreParams, dataToDelete, deleteLog, requestMethod, callback) {
|
||||
metadataStoreParams, dataToDelete, log, requestMethod, callback) {
|
||||
services.metadataStoreObject(bucketName, dataGetInfo,
|
||||
cipherBundle, metadataStoreParams, (err, result) => {
|
||||
if (err) {
|
||||
|
@ -31,7 +30,7 @@ function _storeInMDandDeleteData(bucketName, dataGetInfo, cipherBundle,
|
|||
const newDataStoreName = Array.isArray(dataGetInfo) ?
|
||||
dataGetInfo[0].dataStoreName : null;
|
||||
return data.batchDelete(dataToDelete, requestMethod,
|
||||
newDataStoreName, deleteLog, err => callback(err, result));
|
||||
newDataStoreName, log, err => callback(err, result));
|
||||
}
|
||||
return callback(null, result);
|
||||
});
|
||||
|
@ -51,7 +50,9 @@ function _storeInMDandDeleteData(bucketName, dataGetInfo, cipherBundle,
|
|||
* @param {(object|null)} streamingV4Params - if v4 auth, object containing
|
||||
* accessKey, signatureFromRequest, region, scopeDate, timestamp, and
|
||||
* credentialScope (to be used for streaming v4 auth if applicable)
|
||||
* @param {(object|null)} overheadField - fields to be included in metadata overhead
|
||||
* @param {RequestLogger} log - logger instance
|
||||
* @param {string} originOp - Origin operation
|
||||
* @param {function} callback - callback function
|
||||
* @return {undefined} and call callback with (err, result) -
|
||||
* result.contentMD5 - content md5 of new object or version
|
||||
|
@ -59,7 +60,7 @@ function _storeInMDandDeleteData(bucketName, dataGetInfo, cipherBundle,
|
|||
*/
|
||||
function createAndStoreObject(bucketName, bucketMD, objectKey, objMD, authInfo,
|
||||
canonicalID, cipherBundle, request, isDeleteMarker, streamingV4Params,
|
||||
log, callback) {
|
||||
overheadField, log, originOp, callback) {
|
||||
const putVersionId = request.headers['x-scal-s3-version-id'];
|
||||
const isPutVersion = putVersionId || putVersionId === '';
|
||||
|
||||
|
@ -115,6 +116,7 @@ function createAndStoreObject(bucketName, bucketMD, objectKey, objMD, authInfo,
|
|||
isDeleteMarker,
|
||||
replicationInfo: getReplicationInfo(
|
||||
objectKey, bucketMD, false, size, null, null, authInfo),
|
||||
overheadField,
|
||||
log,
|
||||
};
|
||||
|
||||
|
@ -141,7 +143,7 @@ function createAndStoreObject(bucketName, bucketMD, objectKey, objMD, authInfo,
|
|||
removeAWSChunked(request.headers['content-encoding']);
|
||||
metadataStoreParams.expires = request.headers.expires;
|
||||
metadataStoreParams.tagging = request.headers['x-amz-tagging'];
|
||||
metadataStoreParams.originOp = 's3:ObjectCreated:Put';
|
||||
metadataStoreParams.originOp = originOp;
|
||||
const defaultObjectLockConfiguration
|
||||
= bucketMD.getObjectLockConfiguration();
|
||||
if (defaultObjectLockConfiguration) {
|
||||
|
@ -156,7 +158,7 @@ function createAndStoreObject(bucketName, bucketMD, objectKey, objMD, authInfo,
|
|||
// eslint-disable-next-line no-param-reassign
|
||||
request.headers[constants.objectLocationConstraintHeader] =
|
||||
objMD[constants.objectLocationConstraintHeader];
|
||||
metadataStoreParams.originOp = 's3:ObjectRemoved:DeleteMarkerCreated';
|
||||
metadataStoreParams.originOp = originOp;
|
||||
}
|
||||
|
||||
const backendInfoObj =
|
||||
|
@ -187,14 +189,17 @@ function createAndStoreObject(bucketName, bucketMD, objectKey, objMD, authInfo,
|
|||
}
|
||||
}
|
||||
|
||||
if (objMD && objMD.uploadId) {
|
||||
metadataStoreParams.oldReplayId = objMD.uploadId;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
const dontSkipBackend = externalBackends;
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
const requestLogger =
|
||||
logger.newRequestLoggerFromSerializedUids(log.getSerializedUids());
|
||||
const mdOnlyHeader = request.headers['x-amz-meta-mdonly'];
|
||||
const mdOnlySize = request.headers['x-amz-meta-size'];
|
||||
|
||||
return async.waterfall([
|
||||
function storeData(next) {
|
||||
if (size === 0) {
|
||||
|
@ -283,11 +288,13 @@ function createAndStoreObject(bucketName, bucketMD, objectKey, objMD, authInfo,
|
|||
metadataStoreParams.versionId = options.versionId;
|
||||
metadataStoreParams.versioning = options.versioning;
|
||||
metadataStoreParams.isNull = options.isNull;
|
||||
metadataStoreParams.nullVersionId = options.nullVersionId;
|
||||
metadataStoreParams.nullUploadId = options.nullUploadId;
|
||||
metadataStoreParams.deleteNullKey = options.deleteNullKey;
|
||||
if (options.extraMD) {
|
||||
Object.assign(metadataStoreParams, options.extraMD);
|
||||
}
|
||||
return _storeInMDandDeleteData(bucketName, infoArr,
|
||||
cipherBundle, metadataStoreParams,
|
||||
options.dataToDelete, requestLogger, requestMethod, next);
|
||||
options.dataToDelete, log, requestMethod, next);
|
||||
},
|
||||
], callback);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* _bucketRequiresOplogUpdate - DELETE an object from a bucket
|
||||
* @param {BucketInfo} bucket - bucket object
|
||||
* @return {boolean} whether objects require oplog updates on deletion, or not
|
||||
*/
|
||||
function _bucketRequiresOplogUpdate(bucket) {
|
||||
// Default behavior is to require an oplog update
|
||||
if (!bucket || !bucket.getLifecycleConfiguration || !bucket.getNotificationConfiguration) {
|
||||
return true;
|
||||
}
|
||||
// If the bucket has lifecycle configuration or notification configuration
|
||||
// set, we also require an oplog update
|
||||
return bucket.getLifecycleConfiguration() || bucket.getNotificationConfiguration();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
_bucketRequiresOplogUpdate,
|
||||
};
|
|
@ -4,23 +4,25 @@ const {
|
|||
LifecycleDateTime,
|
||||
LifecycleUtils,
|
||||
} = require('arsenal').s3middleware.lifecycleHelpers;
|
||||
const { config } = require('../../../Config');
|
||||
|
||||
// moves lifecycle transition deadlines 1 day earlier, mostly for testing
|
||||
const transitionOneDayEarlier = process.env.TRANSITION_ONE_DAY_EARLIER === 'true';
|
||||
// moves lifecycle expiration deadlines 1 day earlier, mostly for testing
|
||||
const expireOneDayEarlier = process.env.EXPIRE_ONE_DAY_EARLIER === 'true';
|
||||
const {
|
||||
expireOneDayEarlier,
|
||||
transitionOneDayEarlier,
|
||||
timeProgressionFactor,
|
||||
scaledMsPerDay,
|
||||
} = config.getTimeOptions();
|
||||
|
||||
const lifecycleDateTime = new LifecycleDateTime({
|
||||
transitionOneDayEarlier,
|
||||
expireOneDayEarlier,
|
||||
timeProgressionFactor,
|
||||
});
|
||||
|
||||
const lifecycleUtils = new LifecycleUtils(supportedLifecycleRules, lifecycleDateTime);
|
||||
|
||||
const oneDay = 24 * 60 * 60 * 1000; // Milliseconds in a day.
|
||||
const lifecycleUtils = new LifecycleUtils(supportedLifecycleRules, lifecycleDateTime, timeProgressionFactor);
|
||||
|
||||
function calculateDate(objDate, expDays, datetime) {
|
||||
return new Date(datetime.getTimestamp(objDate) + expDays * oneDay);
|
||||
return new Date(datetime.getTimestamp(objDate) + (expDays * scaledMsPerDay));
|
||||
}
|
||||
|
||||
function formatExpirationHeader(date, id) {
|
||||
|
@ -37,8 +39,10 @@ const AMZ_ABORT_ID_HEADER = 'x-amz-abort-rule-id';
|
|||
|
||||
function _generateExpHeadersObjects(rules, params, datetime) {
|
||||
const tags = {
|
||||
TagSet: Object.keys(params.tags)
|
||||
.map(key => ({ Key: key, Value: params.tags[key] })),
|
||||
TagSet: params.tags
|
||||
? Object.keys(params.tags)
|
||||
.map(key => ({ Key: key, Value: params.tags[key] }))
|
||||
: [],
|
||||
};
|
||||
|
||||
const objectInfo = { Key: params.key };
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
const { versioning } = require('arsenal');
|
||||
const versionIdUtils = versioning.VersionID;
|
||||
|
||||
const { lifecycleListing } = require('../../../../constants');
|
||||
const { CURRENT_TYPE, NON_CURRENT_TYPE, ORPHAN_DM_TYPE } = lifecycleListing;
|
||||
|
||||
function _makeTags(tags) {
|
||||
const res = [];
|
||||
Object.entries(tags).forEach(([key, value]) =>
|
||||
res.push(
|
||||
{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
));
|
||||
return res;
|
||||
}
|
||||
|
||||
function processCurrents(bucketName, listParams, isBucketVersioned, list) {
|
||||
const data = {
|
||||
Name: bucketName,
|
||||
Prefix: listParams.prefix,
|
||||
MaxKeys: listParams.maxKeys,
|
||||
MaxScannedLifecycleListingEntries: listParams.maxScannedLifecycleListingEntries,
|
||||
IsTruncated: !!list.IsTruncated,
|
||||
Marker: listParams.marker,
|
||||
BeforeDate: listParams.beforeDate,
|
||||
NextMarker: list.NextMarker,
|
||||
Contents: [],
|
||||
};
|
||||
|
||||
list.Contents.forEach(item => {
|
||||
const v = item.value;
|
||||
|
||||
const content = {
|
||||
Key: item.key,
|
||||
LastModified: v.LastModified,
|
||||
ETag: `"${v.ETag}"`,
|
||||
Size: v.Size,
|
||||
Owner: {
|
||||
ID: v.Owner.ID,
|
||||
DisplayName: v.Owner.DisplayName,
|
||||
},
|
||||
StorageClass: v.StorageClass,
|
||||
TagSet: _makeTags(v.tags),
|
||||
IsLatest: true, // for compatibility with AWS ListObjectVersions.
|
||||
DataStoreName: v.dataStoreName,
|
||||
ListType: CURRENT_TYPE,
|
||||
};
|
||||
|
||||
// NOTE: The current versions listed to be lifecycle should include version id
|
||||
// if the bucket is versioned.
|
||||
if (isBucketVersioned) {
|
||||
const versionId = (v.IsNull || v.VersionId === undefined) ?
|
||||
'null' : versionIdUtils.encode(v.VersionId);
|
||||
content.VersionId = versionId;
|
||||
}
|
||||
|
||||
data.Contents.push(content);
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function _encodeVersionId(vid) {
|
||||
let versionId = vid;
|
||||
if (versionId && versionId !== 'null') {
|
||||
versionId = versionIdUtils.encode(versionId);
|
||||
}
|
||||
return versionId;
|
||||
}
|
||||
|
||||
function processNonCurrents(bucketName, listParams, list) {
|
||||
const nextVersionIdMarker = _encodeVersionId(list.NextVersionIdMarker);
|
||||
const versionIdMarker = _encodeVersionId(listParams.versionIdMarker);
|
||||
|
||||
const data = {
|
||||
Name: bucketName,
|
||||
Prefix: listParams.prefix,
|
||||
MaxKeys: listParams.maxKeys,
|
||||
MaxScannedLifecycleListingEntries: listParams.maxScannedLifecycleListingEntries,
|
||||
IsTruncated: !!list.IsTruncated,
|
||||
KeyMarker: listParams.keyMarker,
|
||||
VersionIdMarker: versionIdMarker,
|
||||
BeforeDate: listParams.beforeDate,
|
||||
NextKeyMarker: list.NextKeyMarker,
|
||||
NextVersionIdMarker: nextVersionIdMarker,
|
||||
Contents: [],
|
||||
};
|
||||
|
||||
list.Contents.forEach(item => {
|
||||
const v = item.value;
|
||||
const versionId = (v.IsNull || v.VersionId === undefined) ?
|
||||
'null' : versionIdUtils.encode(v.VersionId);
|
||||
|
||||
const content = {
|
||||
Key: item.key,
|
||||
LastModified: v.LastModified,
|
||||
ETag: `"${v.ETag}"`,
|
||||
Size: v.Size,
|
||||
Owner: {
|
||||
ID: v.Owner.ID,
|
||||
DisplayName: v.Owner.DisplayName,
|
||||
},
|
||||
StorageClass: v.StorageClass,
|
||||
TagSet: _makeTags(v.tags),
|
||||
staleDate: v.staleDate, // lowerCamelCase to be compatible with existing lifecycle.
|
||||
VersionId: versionId,
|
||||
DataStoreName: v.dataStoreName,
|
||||
ListType: NON_CURRENT_TYPE,
|
||||
};
|
||||
|
||||
data.Contents.push(content);
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function processOrphans(bucketName, listParams, list) {
|
||||
const data = {
|
||||
Name: bucketName,
|
||||
Prefix: listParams.prefix,
|
||||
MaxKeys: listParams.maxKeys,
|
||||
MaxScannedLifecycleListingEntries: listParams.maxScannedLifecycleListingEntries,
|
||||
IsTruncated: !!list.IsTruncated,
|
||||
Marker: listParams.marker,
|
||||
BeforeDate: listParams.beforeDate,
|
||||
NextMarker: list.NextMarker,
|
||||
Contents: [],
|
||||
};
|
||||
|
||||
list.Contents.forEach(item => {
|
||||
const v = item.value;
|
||||
const versionId = (v.IsNull || v.VersionId === undefined) ?
|
||||
'null' : versionIdUtils.encode(v.VersionId);
|
||||
data.Contents.push({
|
||||
Key: item.key,
|
||||
LastModified: v.LastModified,
|
||||
Owner: {
|
||||
ID: v.Owner.ID,
|
||||
DisplayName: v.Owner.DisplayName,
|
||||
},
|
||||
VersionId: versionId,
|
||||
IsLatest: true, // for compatibility with AWS ListObjectVersions.
|
||||
ListType: ORPHAN_DM_TYPE,
|
||||
});
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function getLocationConstraintErrorMessage(locationName) {
|
||||
return 'value of the location you are attempting to set ' +
|
||||
`- ${locationName} - is not listed in the locationConstraint config`;
|
||||
}
|
||||
|
||||
/**
|
||||
* validateMaxScannedEntries - Validates and returns the maximum scanned entries value.
|
||||
*
|
||||
* @param {object} params - Query parameters
|
||||
* @param {object} config - CloudServer configuration
|
||||
* @param {number} min - Minimum number of entries to be scanned
|
||||
* @returns {Object} - An object indicating the validation result:
|
||||
* - isValid (boolean): Whether the validation is successful.
|
||||
* - maxScannedLifecycleListingEntries (number): The validated maximum scanned entries value if isValid is true.
|
||||
*/
|
||||
function validateMaxScannedEntries(params, config, min) {
|
||||
let maxScannedLifecycleListingEntries = config.maxScannedLifecycleListingEntries;
|
||||
|
||||
if (params['max-scanned-lifecycle-listing-entries']) {
|
||||
const maxEntriesParams = Number.parseInt(params['max-scanned-lifecycle-listing-entries'], 10);
|
||||
|
||||
if (Number.isNaN(maxEntriesParams) || maxEntriesParams < min ||
|
||||
maxEntriesParams > maxScannedLifecycleListingEntries) {
|
||||
return { isValid: false };
|
||||
}
|
||||
|
||||
maxScannedLifecycleListingEntries = maxEntriesParams;
|
||||
}
|
||||
|
||||
return { isValid: true, maxScannedLifecycleListingEntries };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processCurrents,
|
||||
processNonCurrents,
|
||||
processOrphans,
|
||||
getLocationConstraintErrorMessage,
|
||||
validateMaxScannedEntries,
|
||||
};
|
|
@ -3,7 +3,9 @@ const moment = require('moment');
|
|||
|
||||
const { config } = require('../../../Config');
|
||||
const vault = require('../../../auth/vault');
|
||||
const { evaluateBucketPolicyWithIAM } = require('../authorization/permissionChecks');
|
||||
|
||||
const { scaledMsPerDay } = config.getTimeOptions();
|
||||
/**
|
||||
* Calculates retain until date for the locked object version
|
||||
* @param {object} retention - includes days or years retention period
|
||||
|
@ -19,8 +21,9 @@ function calculateRetainUntilDate(retention) {
|
|||
const date = moment();
|
||||
// Calculate the number of days to retain the lock on the object
|
||||
const retainUntilDays = days || years * 365;
|
||||
const retainUntilDaysInMs = retainUntilDays * scaledMsPerDay;
|
||||
const retainUntilDate
|
||||
= date.add(retainUntilDays, 'days');
|
||||
= date.add(retainUntilDaysInMs, 'ms');
|
||||
return retainUntilDate.toISOString();
|
||||
}
|
||||
/**
|
||||
|
@ -202,7 +205,13 @@ class ObjectLockInfo {
|
|||
* @returns {bool} - True if the retention policy allows the objects data to be modified (overwritten/deleted)
|
||||
*/
|
||||
canModifyObject(hasGovernanceBypass) {
|
||||
return !this.isLocked() || (this.isGovernanceMode() && !!hasGovernanceBypass);
|
||||
// can modify object if object is not locked
|
||||
// cannot modify object in any cases if legal hold is enabled
|
||||
// if no legal hold, can only modify object if bypassing governance when locked
|
||||
if (!this.isLocked()) {
|
||||
return true;
|
||||
}
|
||||
return !this.legalHold && this.isGovernanceMode() && !!hasGovernanceBypass;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -296,7 +305,9 @@ function checkUserGovernanceBypass(request, authInfo, bucketMD, objectKey, log,
|
|||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (authorizationResults[0].isAllowed !== true) {
|
||||
const explicitDenyExists = authorizationResults.some(
|
||||
authzResult => authzResult.isAllowed === false && !authzResult.isImplicit);
|
||||
if (explicitDenyExists) {
|
||||
log.trace('authorization check failed for user',
|
||||
{
|
||||
'method': 'checkUserPolicyGovernanceBypass',
|
||||
|
@ -304,7 +315,25 @@ function checkUserGovernanceBypass(request, authInfo, bucketMD, objectKey, log,
|
|||
});
|
||||
return cb(errors.AccessDenied);
|
||||
}
|
||||
return cb(null);
|
||||
// Convert authorization results into an easier to handle format
|
||||
const actionImplicitDenies = authorizationResults.reduce((acc, curr, idx) => {
|
||||
const apiMethod = authorizationResults[idx].action;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
acc[apiMethod] = curr.isImplicit;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Evaluate against the bucket policies
|
||||
const areAllActionsAllowed = evaluateBucketPolicyWithIAM(
|
||||
bucketMD,
|
||||
Object.keys(actionImplicitDenies),
|
||||
authInfo.getCanonicalID(),
|
||||
authInfo,
|
||||
actionImplicitDenies,
|
||||
log,
|
||||
request);
|
||||
|
||||
return cb(areAllActionsAllowed === true ? null : errors.AccessDenied);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ const { pushMetric } = require('../../../utapi/utilities');
|
|||
const { decodeVersionId } = require('./versioning');
|
||||
const collectCorsHeaders = require('../../../utilities/collectCorsHeaders');
|
||||
const { parseRestoreRequestXml } = s3middleware.objectRestore;
|
||||
|
||||
const { processBytesToWrite, validateQuotas } = require('../quotas/quotaUtils');
|
||||
|
||||
/**
|
||||
* Check if tier is supported
|
||||
|
@ -58,13 +58,22 @@ function objectRestore(metadata, mdUtils, userInfo, request, log, callback) {
|
|||
bucketName,
|
||||
objectKey,
|
||||
versionId: decodedVidResult,
|
||||
requestType: 'restoreObject',
|
||||
requestType: request.apiMethods || 'restoreObject',
|
||||
/**
|
||||
* Restoring an object might not cause any impact on
|
||||
* the storage, if the object is already restored: in
|
||||
* this case, the duration is extended. We disable the
|
||||
* quota evaluation and trigger it manually.
|
||||
*/
|
||||
checkQuota: false,
|
||||
request,
|
||||
};
|
||||
|
||||
return async.waterfall([
|
||||
// get metadata of bucket and object
|
||||
function validateBucketAndObject(next) {
|
||||
return mdUtils.metadataValidateBucketAndObj(mdValueParams, log, (err, bucketMD, objectMD) => {
|
||||
return mdUtils.standardMetadataValidateBucketAndObj(mdValueParams, request.actionImplicitDenies,
|
||||
log, (err, bucketMD, objectMD) => {
|
||||
if (err) {
|
||||
log.trace('request authorization failed', { method: METHOD, error: err });
|
||||
return next(err);
|
||||
|
@ -115,6 +124,16 @@ function objectRestore(metadata, mdUtils, userInfo, request, log, callback) {
|
|||
return next(err, bucketMD, objectMD);
|
||||
});
|
||||
},
|
||||
function evaluateQuotas(bucketMD, objectMD, next) {
|
||||
if (isObjectRestored) {
|
||||
return next(null, bucketMD, objectMD);
|
||||
}
|
||||
const actions = Array.isArray(mdValueParams.requestType) ?
|
||||
mdValueParams.requestType : [mdValueParams.requestType];
|
||||
const bytes = processBytesToWrite(request.apiMethod, bucketMD, mdValueParams.versionId, 0, objectMD);
|
||||
return validateQuotas(request, bucketMD, request.accountQuotas, actions, request.apiMethod, bytes,
|
||||
false, log, err => next(err, bucketMD, objectMD));
|
||||
},
|
||||
function updateObjectMD(bucketMD, objectMD, next) {
|
||||
const params = objectMD.versionId ? { versionId: objectMD.versionId } : {};
|
||||
metadata.putObjectMD(bucketMD.getName(), objectKey, objectMD, params,
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
const { errors } = require('arsenal');
|
||||
|
||||
const { unsupportedSignatureChecksums, supportedSignatureChecksums } = require('../../../../constants');
|
||||
|
||||
function validateChecksumHeaders(headers) {
|
||||
// If the x-amz-trailer header is present the request is using one of the
|
||||
// trailing checksum algorithms, which are not supported.
|
||||
if (headers['x-amz-trailer'] !== undefined) {
|
||||
return errors.BadRequest.customizeDescription('trailing checksum is not supported');
|
||||
}
|
||||
|
||||
const signatureChecksum = headers['x-amz-content-sha256'];
|
||||
if (signatureChecksum === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (supportedSignatureChecksums.has(signatureChecksum)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the value is not one of the possible checksum algorithms
|
||||
// the only other valid value is the actual sha256 checksum of the payload.
|
||||
// Do a simple sanity check of the length to guard against future algos.
|
||||
// If the value is an unknown algo, then it will fail checksum validation.
|
||||
if (!unsupportedSignatureChecksums.has(signatureChecksum) && signatureChecksum.length === 64) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return errors.BadRequest.customizeDescription('unsupported checksum algorithm');
|
||||
}
|
||||
|
||||
module.exports = validateChecksumHeaders;
|
|
@ -3,9 +3,8 @@ const async = require('async');
|
|||
|
||||
const metadata = require('../../../metadata/wrapper');
|
||||
const { config } = require('../../../Config');
|
||||
const { addIsNonversionedBucket } = require('../../../metadata/metadataUtils');
|
||||
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
const { scaledMsPerDay } = config.getTimeOptions();
|
||||
|
||||
const versionIdUtils = versioning.VersionID;
|
||||
// Use Arsenal function to generate a version ID used internally by metadata
|
||||
|
@ -59,7 +58,7 @@ function decodeVersionId(reqQuery) {
|
|||
*/
|
||||
function getVersionIdResHeader(verCfg, objectMD) {
|
||||
if (verCfg) {
|
||||
if (objectMD.isNull || (objectMD && !objectMD.versionId)) {
|
||||
if (objectMD.isNull || !objectMD.versionId) {
|
||||
return 'null';
|
||||
}
|
||||
return versionIdUtils.encode(objectMD.versionId);
|
||||
|
@ -80,17 +79,34 @@ function checkQueryVersionId(query) {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
function _storeNullVersionMD(bucketName, objKey, objMD, options, log, cb) {
|
||||
metadata.putObjectMD(bucketName, objKey, objMD, options, log, err => {
|
||||
function _storeNullVersionMD(bucketName, objKey, nullVersionId, objMD, log, cb) {
|
||||
// In compatibility mode, create null versioned keys instead of null keys
|
||||
let versionId;
|
||||
let nullVersionMD;
|
||||
if (config.nullVersionCompatMode) {
|
||||
versionId = nullVersionId;
|
||||
nullVersionMD = Object.assign({}, objMD, {
|
||||
versionId: nullVersionId,
|
||||
isNull: true,
|
||||
});
|
||||
} else {
|
||||
versionId = 'null';
|
||||
nullVersionMD = Object.assign({}, objMD, {
|
||||
versionId: nullVersionId,
|
||||
isNull: true,
|
||||
isNull2: true,
|
||||
});
|
||||
}
|
||||
metadata.putObjectMD(bucketName, objKey, nullVersionMD, { versionId }, log, err => {
|
||||
if (err) {
|
||||
log.debug('error from metadata storing null version as new version',
|
||||
{ error: err });
|
||||
}
|
||||
cb(err, options);
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
/** get location of null version data for deletion
|
||||
/** check existence and get location of null version data for deletion
|
||||
* @param {string} bucketName - name of bucket
|
||||
* @param {string} objKey - name of object key
|
||||
* @param {object} options - metadata options for getting object MD
|
||||
|
@ -101,50 +117,56 @@ function _storeNullVersionMD(bucketName, objKey, objMD, options, log, cb) {
|
|||
* @param {function} cb - callback
|
||||
* @return {undefined} - and call callback with (err, dataToDelete)
|
||||
*/
|
||||
function _getNullVersionsToDelete(bucketName, objKey, options, mst, log, cb) {
|
||||
function _prepareNullVersionDeletion(bucketName, objKey, options, mst, log, cb) {
|
||||
const nullOptions = {};
|
||||
if (!options.deleteData) {
|
||||
return process.nextTick(cb, null, nullOptions);
|
||||
}
|
||||
if (options.versionId === mst.versionId) {
|
||||
// no need to get delete location, we already have the master's metadata
|
||||
const dataToDelete = mst.objLocation;
|
||||
return process.nextTick(cb, null, dataToDelete);
|
||||
// no need to get another key as the master is the target
|
||||
nullOptions.dataToDelete = mst.objLocation;
|
||||
return process.nextTick(cb, null, nullOptions);
|
||||
}
|
||||
if (options.versionId === 'null') {
|
||||
// deletion of the null key will be done by the main metadata
|
||||
// PUT via this option
|
||||
nullOptions.deleteNullKey = true;
|
||||
}
|
||||
return metadata.getObjectMD(bucketName, objKey, options, log,
|
||||
(err, versionMD) => {
|
||||
if (err) {
|
||||
log.debug('err from metadata getting specified version', {
|
||||
error: err,
|
||||
method: '_getNullVersionsToDelete',
|
||||
});
|
||||
// the null key may not exist, hence it's a normal
|
||||
// situation to have a NoSuchKey error, in which case
|
||||
// there is nothing to delete
|
||||
if (err.is.NoSuchKey) {
|
||||
log.debug('null version does not exist', {
|
||||
method: '_prepareNullVersionDeletion',
|
||||
});
|
||||
} else {
|
||||
log.warn('could not get null version metadata', {
|
||||
error: err,
|
||||
method: '_prepareNullVersionDeletion',
|
||||
});
|
||||
}
|
||||
return cb(err);
|
||||
}
|
||||
if (!versionMD.location) {
|
||||
return cb();
|
||||
if (versionMD.location) {
|
||||
const dataToDelete = Array.isArray(versionMD.location) ?
|
||||
versionMD.location : [versionMD.location];
|
||||
nullOptions.dataToDelete = dataToDelete;
|
||||
}
|
||||
const dataToDelete = Array.isArray(versionMD.location) ?
|
||||
versionMD.location : [versionMD.location];
|
||||
return cb(null, dataToDelete);
|
||||
return cb(null, nullOptions);
|
||||
});
|
||||
}
|
||||
|
||||
function _deleteNullVersionMD(bucketName, objKey, options, mst, log, cb) {
|
||||
return _getNullVersionsToDelete(bucketName, objKey, options, mst, log,
|
||||
(err, nullDataToDelete) => {
|
||||
if (err) {
|
||||
log.warn('could not find null version metadata', {
|
||||
error: err,
|
||||
method: '_deleteNullVersionMD',
|
||||
});
|
||||
return cb(err);
|
||||
}
|
||||
return metadata.deleteObjectMD(bucketName, objKey, options, log,
|
||||
err => {
|
||||
if (err) {
|
||||
log.warn('metadata error deleting null version',
|
||||
{ error: err, method: '_deleteNullVersionMD' });
|
||||
return cb(err);
|
||||
}
|
||||
return cb(null, nullDataToDelete);
|
||||
});
|
||||
});
|
||||
function _deleteNullVersionMD(bucketName, objKey, options, log, cb) {
|
||||
return metadata.deleteObjectMD(bucketName, objKey, options, log, err => {
|
||||
if (err) {
|
||||
log.warn('metadata error deleting null versioned key',
|
||||
{ bucketName, objKey, error: err, method: '_deleteNullVersionMD' });
|
||||
}
|
||||
return cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -154,73 +176,103 @@ function _deleteNullVersionMD(bucketName, objKey, options, mst, log, cb) {
|
|||
* @param {object} mst - state of master version, as returned by
|
||||
* getMasterState()
|
||||
* @param {string} vstat - bucket versioning status: 'Enabled' or 'Suspended'
|
||||
* @param {boolean} nullVersionCompatMode - if true, behaves in null
|
||||
* version compatibility mode and return appropriate values: this mode
|
||||
* does not attempt to create null keys but create null versioned keys
|
||||
* instead
|
||||
*
|
||||
* @return {object} result object with the following attributes:
|
||||
* - {object} options: versioning-related options to pass to the
|
||||
services.metadataStoreObject() call
|
||||
* - {object} [storeOptions]: options for metadata to create a new
|
||||
null version key, if needed
|
||||
* - {object} [options.extraMD]: extra attributes to set in object metadata
|
||||
* - {string} [nullVersionId]: null version key to create, if needed
|
||||
* - {object} [delOptions]: options for metadata to delete the null
|
||||
version key, if needed
|
||||
*/
|
||||
function processVersioningState(mst, vstat) {
|
||||
const options = {};
|
||||
const storeOptions = {};
|
||||
const delOptions = {};
|
||||
// object does not exist or is not versioned (before versioning)
|
||||
if (mst.versionId === undefined || mst.isNull) {
|
||||
// versioning is suspended, overwrite existing master version
|
||||
if (vstat === 'Suspended') {
|
||||
options.versionId = '';
|
||||
options.isNull = true;
|
||||
options.dataToDelete = mst.objLocation;
|
||||
// if null version exists, clean it up prior to put
|
||||
if (mst.isNull) {
|
||||
delOptions.versionId = mst.versionId;
|
||||
if (mst.uploadId) {
|
||||
delOptions.replayId = mst.uploadId;
|
||||
}
|
||||
function processVersioningState(mst, vstat, nullVersionCompatMode) {
|
||||
const versioningSuspended = (vstat === 'Suspended');
|
||||
const masterIsNull = mst.exists && (mst.isNull || !mst.versionId);
|
||||
|
||||
if (versioningSuspended) {
|
||||
// versioning is suspended: overwrite the existing null version
|
||||
const options = { versionId: '', isNull: true };
|
||||
if (masterIsNull) {
|
||||
// if the null version exists, clean it up prior to put
|
||||
if (mst.objLocation) {
|
||||
options.dataToDelete = mst.objLocation;
|
||||
}
|
||||
// backward-compat: a null version key may exist even with
|
||||
// a null master (due to S3C-7526), if so, delete it (its
|
||||
// data will be deleted as part of the master cleanup, so
|
||||
// no "deleteData" param is needed)
|
||||
//
|
||||
// "isNull2" attribute is set in master metadata when
|
||||
// null keys are used, which is used as an optimization to
|
||||
// avoid having to check the versioned key since there can
|
||||
// be no more versioned key to clean up
|
||||
if (mst.isNull && mst.versionId && !mst.isNull2) {
|
||||
const delOptions = { versionId: mst.versionId };
|
||||
return { options, delOptions };
|
||||
}
|
||||
return { options };
|
||||
}
|
||||
// versioning is enabled, create a new version
|
||||
options.versioning = true;
|
||||
if (mst.exists) {
|
||||
// store master version in a new key
|
||||
const versionId = mst.isNull ? mst.versionId : nonVersionedObjId;
|
||||
storeOptions.versionId = versionId;
|
||||
storeOptions.isNull = true;
|
||||
options.nullVersionId = versionId;
|
||||
// non-versioned (non-null) MPU objects don't have a
|
||||
// replay ID, so don't reference their uploadId
|
||||
if (mst.isNull && mst.uploadId) {
|
||||
options.nullUploadId = mst.uploadId;
|
||||
if (mst.nullVersionId) {
|
||||
// backward-compat: delete the null versioned key and data
|
||||
const delOptions = { versionId: mst.nullVersionId, deleteData: true };
|
||||
if (mst.nullUploadId) {
|
||||
delOptions.replayId = mst.nullUploadId;
|
||||
}
|
||||
return { options, storeOptions };
|
||||
}
|
||||
return { options };
|
||||
}
|
||||
// master is versioned and is not a null version
|
||||
const nullVersionId = mst.nullVersionId;
|
||||
if (vstat === 'Suspended') {
|
||||
// versioning is suspended, overwrite the existing master version
|
||||
options.versionId = '';
|
||||
options.isNull = true;
|
||||
if (nullVersionId === undefined) {
|
||||
return { options };
|
||||
}
|
||||
delOptions.versionId = nullVersionId;
|
||||
if (mst.nullUploadId) {
|
||||
delOptions.replayId = mst.nullUploadId;
|
||||
return { options, delOptions };
|
||||
}
|
||||
// clean up the eventual null key's location data prior to put
|
||||
|
||||
// NOTE: due to metadata v1 internal format, we cannot guess
|
||||
// from the master key whether there is an associated null
|
||||
// key, because the master key may be removed whenever the
|
||||
// latest version becomes a delete marker. Hence we need to
|
||||
// pessimistically try to get the null key metadata and delete
|
||||
// it if it exists.
|
||||
const delOptions = { versionId: 'null', deleteData: true };
|
||||
return { options, delOptions };
|
||||
}
|
||||
// versioning is enabled, put the new version
|
||||
options.versioning = true;
|
||||
options.nullVersionId = nullVersionId;
|
||||
if (mst.nullUploadId) {
|
||||
options.nullUploadId = mst.nullUploadId;
|
||||
|
||||
// versioning is enabled: create a new version
|
||||
const options = { versioning: true };
|
||||
if (masterIsNull) {
|
||||
// if master is a null version or a non-versioned key,
|
||||
// copy it to a new null key
|
||||
const nullVersionId = (mst.isNull && mst.versionId) ? mst.versionId : nonVersionedObjId;
|
||||
if (nullVersionCompatMode) {
|
||||
options.extraMD = {
|
||||
nullVersionId,
|
||||
};
|
||||
if (mst.uploadId) {
|
||||
options.extraMD.nullUploadId = mst.uploadId;
|
||||
}
|
||||
return { options, nullVersionId };
|
||||
}
|
||||
if (mst.isNull && !mst.isNull2) {
|
||||
// if master null version was put with an older
|
||||
// Cloudserver (or in compat mode), there is a
|
||||
// possibility that it also has a null versioned key
|
||||
// associated, so we need to delete it as we write the
|
||||
// null key
|
||||
const delOptions = {
|
||||
versionId: nullVersionId,
|
||||
};
|
||||
return { options, nullVersionId, delOptions };
|
||||
}
|
||||
return { options, nullVersionId };
|
||||
}
|
||||
// backward-compat: keep a reference to the existing null
|
||||
// versioned key
|
||||
if (mst.nullVersionId) {
|
||||
options.extraMD = {
|
||||
nullVersionId: mst.nullVersionId,
|
||||
};
|
||||
if (mst.nullUploadId) {
|
||||
options.extraMD.nullUploadId = mst.nullUploadId;
|
||||
}
|
||||
}
|
||||
return { options };
|
||||
}
|
||||
|
@ -247,6 +299,7 @@ function getMasterState(objMD) {
|
|||
versionId: objMD.versionId,
|
||||
uploadId: objMD.uploadId,
|
||||
isNull: objMD.isNull,
|
||||
isNull2: objMD.isNull2,
|
||||
nullVersionId: objMD.nullVersionId,
|
||||
nullUploadId: objMD.nullUploadId,
|
||||
};
|
||||
|
@ -270,9 +323,6 @@ function getMasterState(objMD) {
|
|||
* ('' overwrites the master version)
|
||||
* options.versioning - (true/undefined) metadata instruction to create new ver
|
||||
* options.isNull - (true/undefined) whether new version is null or not
|
||||
* options.nullVersionId - if storing a null version in version history, the
|
||||
* version id of the null version
|
||||
* options.deleteNullVersionData - whether to delete the data of the null ver
|
||||
*/
|
||||
function versioningPreprocessing(bucketName, bucketMD, objectKey, objMD,
|
||||
log, callback) {
|
||||
|
@ -284,43 +334,102 @@ function versioningPreprocessing(bucketName, bucketMD, objectKey, objMD,
|
|||
return process.nextTick(callback, null, options);
|
||||
}
|
||||
// bucket is versioning configured
|
||||
const { options, storeOptions, delOptions } =
|
||||
processVersioningState(mst, vCfg.Status);
|
||||
const { options, nullVersionId, delOptions } =
|
||||
processVersioningState(mst, vCfg.Status, config.nullVersionCompatMode);
|
||||
return async.series([
|
||||
function storeVersion(next) {
|
||||
if (!storeOptions) {
|
||||
function storeNullVersionMD(next) {
|
||||
if (!nullVersionId) {
|
||||
return process.nextTick(next);
|
||||
}
|
||||
const versionMD = Object.assign({}, objMD, storeOptions);
|
||||
const params = { versionId: storeOptions.versionId };
|
||||
return _storeNullVersionMD(bucketName, objectKey, versionMD,
|
||||
params, log, next);
|
||||
return _storeNullVersionMD(bucketName, objectKey, nullVersionId, objMD, log, next);
|
||||
},
|
||||
function deleteNullVersion(next) {
|
||||
function prepareNullVersionDeletion(next) {
|
||||
if (!delOptions) {
|
||||
return process.nextTick(next);
|
||||
}
|
||||
const updatedDelOptions = addIsNonversionedBucket(delOptions, bucketMD);
|
||||
return _deleteNullVersionMD(bucketName, objectKey, updatedDelOptions, mst,
|
||||
log, (err, nullDataToDelete) => {
|
||||
return _prepareNullVersionDeletion(
|
||||
bucketName, objectKey, delOptions, mst, log,
|
||||
(err, nullOptions) => {
|
||||
if (err) {
|
||||
log.warn('unexpected error deleting null version md', {
|
||||
error: err,
|
||||
method: 'versioningPreprocessing',
|
||||
});
|
||||
// it's possible there was a concurrent request to
|
||||
// delete the null version, so proceed with putting a
|
||||
// new version
|
||||
if (err.is.NoSuchKey) {
|
||||
return next(null, options);
|
||||
}
|
||||
return next(errors.InternalError);
|
||||
return next(err);
|
||||
}
|
||||
Object.assign(options, { dataToDelete: nullDataToDelete });
|
||||
Object.assign(options, nullOptions);
|
||||
return next();
|
||||
});
|
||||
},
|
||||
], err => callback(err, options));
|
||||
function deleteNullVersionMD(next) {
|
||||
if (delOptions &&
|
||||
delOptions.versionId &&
|
||||
delOptions.versionId !== 'null') {
|
||||
// backward-compat: delete old null versioned key
|
||||
return _deleteNullVersionMD(
|
||||
bucketName, objectKey, { versionId: delOptions.versionId }, log, next);
|
||||
}
|
||||
return process.nextTick(next);
|
||||
},
|
||||
], err => {
|
||||
// it's possible there was a prior request that deleted the
|
||||
// null version, so proceed with putting a new version
|
||||
if (err && err.is.NoSuchKey) {
|
||||
return callback(null, options);
|
||||
}
|
||||
return callback(err, options);
|
||||
});
|
||||
}
|
||||
|
||||
/** Return options to pass to Metadata layer for version-specific
|
||||
* operations with the given requested version ID
|
||||
*
|
||||
* @param {object} objectMD - object metadata
|
||||
* @param {boolean} nullVersionCompatMode - if true, behaves in null
|
||||
* version compatibility mode
|
||||
* @return {object} options object with params:
|
||||
* {string} [options.versionId] - specific versionId to update
|
||||
* {boolean} [options.isNull=true|false|undefined] - if set, tells the
|
||||
* Metadata backend if we're updating or deleting a new-style null
|
||||
* version (stored in master or null key), or not a null version.
|
||||
*/
|
||||
function getVersionSpecificMetadataOptions(objectMD, nullVersionCompatMode) {
|
||||
// Use the internal versionId if it is a "real" null version (not
|
||||
// non-versioned)
|
||||
//
|
||||
// If the target object is non-versioned: do not specify a
|
||||
// "versionId" attribute nor "isNull"
|
||||
//
|
||||
// If the target version is a null version, i.e. has the "isNull"
|
||||
// attribute:
|
||||
//
|
||||
// - send the "isNull=true" param to Metadata if the version is
|
||||
// already a null key put by a non-compat mode Cloudserver, to
|
||||
// let Metadata know that the null key is to be updated or
|
||||
// deleted. This is the case if the "isNull2" metadata attribute
|
||||
// exists
|
||||
//
|
||||
// - otherwise, do not send the "isNull" parameter to hint
|
||||
// Metadata that it is a legacy null version
|
||||
//
|
||||
// If the target version is not a null version and is versioned:
|
||||
//
|
||||
// - send the "isNull=false" param to Metadata in non-compat
|
||||
// mode (mandatory for v1 format)
|
||||
//
|
||||
// - otherwise, do not send the "isNull" parameter to hint
|
||||
// Metadata that an existing null version may not be stored in a
|
||||
// null key
|
||||
//
|
||||
//
|
||||
if (objectMD.versionId === undefined) {
|
||||
return {};
|
||||
}
|
||||
const options = { versionId: objectMD.versionId };
|
||||
if (objectMD.isNull) {
|
||||
if (objectMD.isNull2) {
|
||||
options.isNull = true;
|
||||
}
|
||||
} else if (!nullVersionCompatMode) {
|
||||
options.isNull = false;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/** preprocessingVersioningDelete - return versioning information for S3 to
|
||||
|
@ -329,66 +438,67 @@ function versioningPreprocessing(bucketName, bucketMD, objectKey, objMD,
|
|||
* @param {object} bucketMD - bucket metadata
|
||||
* @param {object} objectMD - obj metadata
|
||||
* @param {string} [reqVersionId] - specific version ID sent as part of request
|
||||
* @param {RequestLogger} log - logger instance
|
||||
* @param {function} callback - callback
|
||||
* @return {undefined} and call callback with params (err, options):
|
||||
* options.deleteData - (true/undefined) whether to delete data (if undefined
|
||||
* @param {boolean} nullVersionCompatMode - if true, behaves in null version compatibility mode
|
||||
* @return {object} options object with params:
|
||||
* {boolean} [options.deleteData=true|undefined] - whether to delete data (if undefined
|
||||
* means creating a delete marker instead)
|
||||
* options.versionId - specific versionId to delete
|
||||
* options.isNull - (true/undefined) whether version to be deleted/marked is null or not
|
||||
* {string} [options.versionId] - specific versionId to delete
|
||||
* {boolean} [options.isNull=true|false|undefined] - if set, tells the
|
||||
* Metadata backend if we're deleting a new-style null version (stored
|
||||
* in master or null key), or not a null version.
|
||||
*/
|
||||
function preprocessingVersioningDelete(bucketName, bucketMD, objectMD,
|
||||
reqVersionId, log, callback) {
|
||||
const options = {};
|
||||
// bucket is not versioning enabled
|
||||
if (!bucketMD.getVersioningConfiguration()) {
|
||||
function preprocessingVersioningDelete(bucketName, bucketMD, objectMD, reqVersionId, nullVersionCompatMode) {
|
||||
let options = {};
|
||||
if (bucketMD.getVersioningConfiguration() && reqVersionId) {
|
||||
options = getVersionSpecificMetadataOptions(objectMD, nullVersionCompatMode);
|
||||
}
|
||||
if (!bucketMD.getVersioningConfiguration() || reqVersionId) {
|
||||
// delete data if bucket is non-versioned or the request
|
||||
// deletes a specific version
|
||||
options.deleteData = true;
|
||||
return callback(null, options);
|
||||
}
|
||||
// bucket is versioning enabled
|
||||
if (reqVersionId && reqVersionId !== 'null') {
|
||||
// deleting a specific version
|
||||
options.deleteData = true;
|
||||
options.versionId = reqVersionId;
|
||||
if (objectMD.uploadId) {
|
||||
options.replayId = objectMD.uploadId;
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep metadatas when the object is restored from cold storage
|
||||
* but remove the specific ones we don't want to keep
|
||||
* @param {object} objMD - obj metadata
|
||||
* @param {object} metadataStoreParams - custom built object containing resource details.
|
||||
* @return {undefined}
|
||||
*/
|
||||
function restoreMetadata(objMD, metadataStoreParams) {
|
||||
/* eslint-disable no-param-reassign */
|
||||
const userMDToSkip = ['x-amz-meta-scal-s3-restore-attempt'];
|
||||
// We need to keep user metadata and tags
|
||||
Object.keys(objMD).forEach(key => {
|
||||
if (key.startsWith('x-amz-meta-') && !userMDToSkip.includes(key)) {
|
||||
metadataStoreParams.metaHeaders[key] = objMD[key];
|
||||
}
|
||||
return callback(null, options);
|
||||
});
|
||||
|
||||
if (objMD['x-amz-website-redirect-location']) {
|
||||
if (!metadataStoreParams.headers) {
|
||||
metadataStoreParams.headers = {};
|
||||
}
|
||||
metadataStoreParams.headers['x-amz-website-redirect-location'] = objMD['x-amz-website-redirect-location'];
|
||||
}
|
||||
if (reqVersionId) {
|
||||
// deleting the 'null' version if it exists
|
||||
if (objectMD.versionId === undefined) {
|
||||
// object is not versioned, deleting it
|
||||
options.deleteData = true;
|
||||
// non-versioned (non-null) MPU objects don't have a
|
||||
// replay ID, so don't reference their uploadId
|
||||
return callback(null, options);
|
||||
}
|
||||
if (objectMD.isNull) {
|
||||
// master is the null version
|
||||
options.deleteData = true;
|
||||
options.versionId = objectMD.versionId;
|
||||
if (objectMD.uploadId) {
|
||||
options.replayId = objectMD.uploadId;
|
||||
}
|
||||
return callback(null, options);
|
||||
}
|
||||
if (objectMD.nullVersionId) {
|
||||
// null version exists, deleting it
|
||||
options.deleteData = true;
|
||||
options.versionId = objectMD.nullVersionId;
|
||||
if (objectMD.nullUploadId) {
|
||||
options.replayId = objectMD.nullUploadId;
|
||||
}
|
||||
return callback(null, options);
|
||||
}
|
||||
// null version does not exist, no deletion
|
||||
// TODO check AWS behaviour for no deletion (seems having no error)
|
||||
return callback(errors.NoSuchKey);
|
||||
|
||||
if (objMD.replicationInfo) {
|
||||
metadataStoreParams.replicationInfo = objMD.replicationInfo;
|
||||
}
|
||||
// not deleting any specific version, making a delete marker instead
|
||||
options.isNull = true;
|
||||
return callback(null, options);
|
||||
|
||||
if (objMD.legalHold) {
|
||||
metadataStoreParams.legalHold = objMD.legalHold;
|
||||
}
|
||||
|
||||
if (objMD.acl) {
|
||||
metadataStoreParams.acl = objMD.acl;
|
||||
}
|
||||
|
||||
metadataStoreParams.creationTime = objMD['creation-time'];
|
||||
metadataStoreParams.lastModifiedDate = objMD['last-modified'];
|
||||
metadataStoreParams.taggingCopy = objMD.tags;
|
||||
}
|
||||
|
||||
/** overwritingVersioning - return versioning information for S3 to handle
|
||||
|
@ -402,10 +512,8 @@ function preprocessingVersioningDelete(bucketName, bucketMD, objectMD,
|
|||
* version id of the null version
|
||||
*/
|
||||
function overwritingVersioning(objMD, metadataStoreParams) {
|
||||
/* eslint-disable no-param-reassign */
|
||||
metadataStoreParams.creationTime = objMD['creation-time'];
|
||||
metadataStoreParams.lastModifiedDate = objMD['last-modified'];
|
||||
metadataStoreParams.updateMicroVersionId = true;
|
||||
metadataStoreParams.amzStorageClass = objMD['x-amz-storage-class'];
|
||||
|
||||
// set correct originOp
|
||||
metadataStoreParams.originOp = 's3:ObjectRestore:Completed';
|
||||
|
@ -418,7 +526,7 @@ function overwritingVersioning(objMD, metadataStoreParams) {
|
|||
restoreRequestedAt: objMD.archive?.restoreRequestedAt,
|
||||
restoreRequestedDays: objMD.archive?.restoreRequestedDays,
|
||||
restoreCompletedAt: new Date(now),
|
||||
restoreWillExpireAt: new Date(now + (days * oneDay)),
|
||||
restoreWillExpireAt: new Date(now + (days * scaledMsPerDay)),
|
||||
};
|
||||
|
||||
/* eslint-enable no-param-reassign */
|
||||
|
@ -427,8 +535,14 @@ function overwritingVersioning(objMD, metadataStoreParams) {
|
|||
const options = {
|
||||
versionId,
|
||||
isNull: objMD.isNull,
|
||||
nullVersionId: objMD.nullVersionId,
|
||||
};
|
||||
if (objMD.nullVersionId) {
|
||||
options.extraMD = {
|
||||
nullVersionId: objMD.nullVersionId,
|
||||
};
|
||||
}
|
||||
|
||||
restoreMetadata(objMD, metadataStoreParams);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
@ -440,6 +554,7 @@ module.exports = {
|
|||
processVersioningState,
|
||||
getMasterState,
|
||||
versioningPreprocessing,
|
||||
getVersionSpecificMetadataOptions,
|
||||
preprocessingVersioningDelete,
|
||||
overwritingVersioning,
|
||||
decodeVID,
|
||||
|
|
|
@ -101,8 +101,33 @@ function validateWebsiteHeader(header) {
|
|||
header.startsWith('http://') || header.startsWith('https://'));
|
||||
}
|
||||
|
||||
/**
|
||||
* appendWebsiteIndexDocument - append index to objectKey if necessary
|
||||
* @param {object} request - normalized request object
|
||||
* @param {string} indexDocumentSuffix - index document from website config
|
||||
* @param {boolean} force - flag to force append index
|
||||
* @return {undefined}
|
||||
*/
|
||||
function appendWebsiteIndexDocument(request, indexDocumentSuffix, force = false) {
|
||||
const reqObjectKey = request.objectKey ? request.objectKey : '';
|
||||
/* eslint-disable no-param-reassign */
|
||||
|
||||
// find index document if "directory" sent in request
|
||||
if (reqObjectKey.endsWith('/')) {
|
||||
request.objectKey += indexDocumentSuffix;
|
||||
// find index document if no key provided
|
||||
} else if (reqObjectKey === '') {
|
||||
request.objectKey = indexDocumentSuffix;
|
||||
// force for redirect 302 on folder without trailing / that has an index
|
||||
} else if (force) {
|
||||
request.objectKey += `/${indexDocumentSuffix}`;
|
||||
}
|
||||
/* eslint-enable no-param-reassign */
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
findRoutingRule,
|
||||
extractRedirectInfo,
|
||||
validateWebsiteHeader,
|
||||
appendWebsiteIndexDocument,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,314 @@
|
|||
const async = require('async');
|
||||
const { errors } = require('arsenal');
|
||||
const monitoring = require('../../../utilities/monitoringHandler');
|
||||
const {
|
||||
actionNeedQuotaCheckCopy,
|
||||
actionNeedQuotaCheck,
|
||||
actionWithDataDeletion,
|
||||
} = require('arsenal').policies;
|
||||
const { config } = require('../../../Config');
|
||||
const QuotaService = require('../../../quotas/quotas');
|
||||
|
||||
/**
|
||||
* Process the bytes to write based on the request and object metadata
|
||||
* @param {string} apiMethod - api method
|
||||
* @param {BucketInfo} bucket - bucket info
|
||||
* @param {string} versionId - version id of the object
|
||||
* @param {number} contentLength - content length of the object
|
||||
* @param {object} objMD - object metadata
|
||||
* @param {object} destObjMD - destination object metadata
|
||||
* @return {number} processed content length
|
||||
*/
|
||||
function processBytesToWrite(apiMethod, bucket, versionId, contentLength, objMD, destObjMD = null) {
|
||||
let bytes = contentLength;
|
||||
if (apiMethod === 'objectRestore') {
|
||||
// object is being restored
|
||||
bytes = Number.parseInt(objMD['content-length'], 10);
|
||||
} else if (!bytes && objMD?.['content-length']) {
|
||||
if (apiMethod === 'objectCopy' || apiMethod === 'objectPutCopyPart') {
|
||||
if (!destObjMD || bucket.isVersioningEnabled()) {
|
||||
// object is being copied
|
||||
bytes = Number.parseInt(objMD['content-length'], 10);
|
||||
} else if (!bucket.isVersioningEnabled()) {
|
||||
// object is being copied and replaces the target
|
||||
bytes = Number.parseInt(objMD['content-length'], 10) -
|
||||
Number.parseInt(destObjMD['content-length'], 10);
|
||||
}
|
||||
} else if (!bucket.isVersioningEnabled() || bucket.isVersioningEnabled() && versionId) {
|
||||
// object is being deleted
|
||||
bytes = -Number.parseInt(objMD['content-length'], 10);
|
||||
}
|
||||
} else if (bytes && objMD?.['content-length'] && !bucket.isVersioningEnabled()) {
|
||||
// object is being replaced: store the diff, if the bucket is not versioned
|
||||
bytes = bytes - Number.parseInt(objMD['content-length'], 10);
|
||||
}
|
||||
return bytes || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a metric is stale based on the provided parameters.
|
||||
*
|
||||
* @param {Object} metric - The metric object to check.
|
||||
* @param {string} resourceType - The type of the resource.
|
||||
* @param {string} resourceName - The name of the resource.
|
||||
* @param {string} action - The action being performed.
|
||||
* @param {number} inflight - The number of inflight requests.
|
||||
* @param {Object} log - The logger object.
|
||||
* @returns {boolean} Returns true if the metric is stale, false otherwise.
|
||||
*/
|
||||
function isMetricStale(metric, resourceType, resourceName, action, inflight, log) {
|
||||
if (metric.date && Date.now() - new Date(metric.date).getTime() >
|
||||
QuotaService.maxStaleness) {
|
||||
log.warn('Stale metrics from the quota service, allowing the request', {
|
||||
resourceType,
|
||||
resourceName,
|
||||
action,
|
||||
inflight,
|
||||
});
|
||||
monitoring.requestWithQuotaMetricsUnavailable.inc();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates quotas for a bucket and an account and update inflight count.
|
||||
*
|
||||
* @param {number} bucketQuota - The quota limit for the bucket.
|
||||
* @param {number} accountQuota - The quota limit for the account.
|
||||
* @param {object} bucket - The bucket object.
|
||||
* @param {object} account - The account object.
|
||||
* @param {number} inflight - The number of inflight requests.
|
||||
* @param {number} inflightForCheck - The number of inflight requests for checking quotas.
|
||||
* @param {string} action - The action being performed.
|
||||
* @param {object} log - The logger object.
|
||||
* @param {function} callback - The callback function to be called when evaluation is complete.
|
||||
* @returns {object} - The result of the evaluation.
|
||||
*/
|
||||
function _evaluateQuotas(
|
||||
bucketQuota,
|
||||
accountQuota,
|
||||
bucket,
|
||||
account,
|
||||
inflight,
|
||||
inflightForCheck,
|
||||
action,
|
||||
log,
|
||||
callback,
|
||||
) {
|
||||
let bucketQuotaExceeded = false;
|
||||
let accountQuotaExceeded = false;
|
||||
const creationDate = new Date(bucket.getCreationDate()).getTime();
|
||||
return async.parallel({
|
||||
bucketQuota: parallelDone => {
|
||||
if (bucketQuota > 0) {
|
||||
return QuotaService.getUtilizationMetrics('bucket',
|
||||
`${bucket.getName()}_${creationDate}`, null, {
|
||||
action,
|
||||
inflight,
|
||||
}, (err, bucketMetrics) => {
|
||||
if (err || inflight < 0) {
|
||||
return parallelDone(err);
|
||||
}
|
||||
if (!isMetricStale(bucketMetrics, 'bucket', bucket.getName(), action, inflight, log) &&
|
||||
bucketMetrics.bytesTotal + inflightForCheck > bucketQuota) {
|
||||
log.debug('Bucket quota exceeded', {
|
||||
bucket: bucket.getName(),
|
||||
action,
|
||||
inflight,
|
||||
quota: bucketQuota,
|
||||
bytesTotal: bucketMetrics.bytesTotal,
|
||||
});
|
||||
bucketQuotaExceeded = true;
|
||||
}
|
||||
return parallelDone();
|
||||
});
|
||||
}
|
||||
return parallelDone();
|
||||
},
|
||||
accountQuota: parallelDone => {
|
||||
if (accountQuota > 0 && account?.account) {
|
||||
return QuotaService.getUtilizationMetrics('account',
|
||||
account.account, null, {
|
||||
action,
|
||||
inflight,
|
||||
}, (err, accountMetrics) => {
|
||||
if (err || inflight < 0) {
|
||||
return parallelDone(err);
|
||||
}
|
||||
if (!isMetricStale(accountMetrics, 'account', account.account, action, inflight, log) &&
|
||||
accountMetrics.bytesTotal + inflightForCheck > accountQuota) {
|
||||
log.debug('Account quota exceeded', {
|
||||
accountId: account.account,
|
||||
action,
|
||||
inflight,
|
||||
quota: accountQuota,
|
||||
bytesTotal: accountMetrics.bytesTotal,
|
||||
});
|
||||
accountQuotaExceeded = true;
|
||||
}
|
||||
return parallelDone();
|
||||
});
|
||||
}
|
||||
return parallelDone();
|
||||
},
|
||||
}, err => {
|
||||
if (err) {
|
||||
log.warn('Error evaluating quotas', {
|
||||
error: err.name,
|
||||
description: err.message,
|
||||
isInflightDeletion: inflight < 0,
|
||||
});
|
||||
}
|
||||
return callback(err, bucketQuotaExceeded, accountQuotaExceeded);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors the duration of quota evaluation for a specific API method.
|
||||
*
|
||||
* @param {string} apiMethod - The name of the API method being monitored.
|
||||
* @param {string} type - The type of quota being evaluated.
|
||||
* @param {string} code - The code associated with the quota being evaluated.
|
||||
* @param {number} duration - The duration of the quota evaluation in nanoseconds.
|
||||
* @returns {undefined} - Returns nothing.
|
||||
*/
|
||||
function monitorQuotaEvaluationDuration(apiMethod, type, code, duration) {
|
||||
monitoring.quotaEvaluationDuration.labels({
|
||||
action: apiMethod,
|
||||
type,
|
||||
code,
|
||||
}).observe(duration / 1e9);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Request} request - request object
|
||||
* @param {BucketInfo} bucket - bucket object
|
||||
* @param {Account} account - account object
|
||||
* @param {array} apiNames - action names: operations to authorize
|
||||
* @param {string} apiMethod - the main API call
|
||||
* @param {number} inflight - inflight bytes
|
||||
* @param {boolean} isStorageReserved - Flag to check if the current quota, minus
|
||||
* the incoming bytes, are under the limit.
|
||||
* @param {Logger} log - logger
|
||||
* @param {function} callback - callback function
|
||||
* @returns {boolean} - true if the quota is valid, false otherwise
|
||||
*/
|
||||
function validateQuotas(request, bucket, account, apiNames, apiMethod, inflight, isStorageReserved, log, callback) {
|
||||
if (!config.isQuotaEnabled() || (!inflight && isStorageReserved)) {
|
||||
return callback(null);
|
||||
}
|
||||
let type;
|
||||
let bucketQuotaExceeded = false;
|
||||
let accountQuotaExceeded = false;
|
||||
let quotaEvaluationDuration;
|
||||
const requestStartTime = process.hrtime.bigint();
|
||||
const bucketQuota = bucket.getQuota();
|
||||
const accountQuota = account?.quota || 0;
|
||||
const shouldSendInflights = config.isQuotaInflightEnabled();
|
||||
|
||||
if (bucketQuota && accountQuota) {
|
||||
type = 'bucket+account';
|
||||
} else if (bucketQuota) {
|
||||
type = 'bucket';
|
||||
} else {
|
||||
type = 'account';
|
||||
}
|
||||
|
||||
if (actionWithDataDeletion[apiMethod]) {
|
||||
type = 'delete';
|
||||
}
|
||||
|
||||
if ((bucketQuota <= 0 && accountQuota <= 0) || !QuotaService?.enabled) {
|
||||
if (bucketQuota > 0 || accountQuota > 0) {
|
||||
log.warn('quota is set for a bucket, but the quota service is disabled', {
|
||||
bucketName: bucket.getName(),
|
||||
});
|
||||
monitoring.requestWithQuotaMetricsUnavailable.inc();
|
||||
}
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
if (isStorageReserved) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
inflight = 0;
|
||||
}
|
||||
|
||||
return async.forEach(apiNames, (apiName, done) => {
|
||||
// Object copy operations first check the target object,
|
||||
// meaning the source object, containing the current bytes,
|
||||
// is checked second. This logic handles these APIs calls by
|
||||
// ensuring the bytes are positives (i.e., not an object
|
||||
// replacement).
|
||||
if (actionNeedQuotaCheckCopy(apiName, apiMethod)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
inflight = Math.abs(inflight);
|
||||
} else if (!actionNeedQuotaCheck[apiName] && !actionWithDataDeletion[apiName]) {
|
||||
return done();
|
||||
}
|
||||
// When inflights are disabled, the sum of the current utilization metrics
|
||||
// and the current bytes are compared with the quota. The current bytes
|
||||
// are not sent to the utilization service. When inflights are enabled,
|
||||
// the sum of the current utilization metrics only are compared with the
|
||||
// quota. They include the current inflight bytes sent in the request.
|
||||
let _inflights = shouldSendInflights ? inflight : undefined;
|
||||
const inflightForCheck = shouldSendInflights ? 0 : inflight;
|
||||
return _evaluateQuotas(bucketQuota, accountQuota, bucket, account, _inflights,
|
||||
inflightForCheck, apiName, log,
|
||||
(err, _bucketQuotaExceeded, _accountQuotaExceeded) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
bucketQuotaExceeded = _bucketQuotaExceeded;
|
||||
accountQuotaExceeded = _accountQuotaExceeded;
|
||||
|
||||
// Inflights are inverted: in case of cleanup, we just re-issue
|
||||
// the same API call.
|
||||
if (_inflights) {
|
||||
_inflights = -_inflights;
|
||||
}
|
||||
|
||||
request.finalizerHooks.push((errorFromAPI, _done) => {
|
||||
const code = (bucketQuotaExceeded || accountQuotaExceeded) ? 429 : 200;
|
||||
const quotaCleanUpStartTime = process.hrtime.bigint();
|
||||
// Quotas are cleaned only in case of error in the API
|
||||
async.waterfall([
|
||||
cb => {
|
||||
if (errorFromAPI) {
|
||||
return _evaluateQuotas(bucketQuota, accountQuota, bucket, account, _inflights,
|
||||
null, apiName, log, cb);
|
||||
}
|
||||
return cb();
|
||||
},
|
||||
], () => {
|
||||
monitorQuotaEvaluationDuration(apiMethod, type, code, quotaEvaluationDuration +
|
||||
Number(process.hrtime.bigint() - quotaCleanUpStartTime));
|
||||
return _done();
|
||||
});
|
||||
});
|
||||
|
||||
return done();
|
||||
});
|
||||
}, err => {
|
||||
quotaEvaluationDuration = Number(process.hrtime.bigint() - requestStartTime);
|
||||
if (err) {
|
||||
log.warn('Error getting metrics from the quota service, allowing the request', {
|
||||
error: err.name,
|
||||
description: err.message,
|
||||
});
|
||||
}
|
||||
if (!actionWithDataDeletion[apiMethod] &&
|
||||
(bucketQuotaExceeded || accountQuotaExceeded)) {
|
||||
return callback(errors.QuotaExceeded);
|
||||
}
|
||||
return callback();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processBytesToWrite,
|
||||
isMetricStale,
|
||||
validateQuotas,
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
const { errors } = require('arsenal');
|
||||
const constants = require('../../../constants');
|
||||
const services = require('../../services');
|
||||
const { standardMetadataValidateBucket } = require('../../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../../utapi/utilities');
|
||||
const monitoring = require('../../utilities/monitoringHandler');
|
||||
const { getLocationConstraintErrorMessage, processCurrents,
|
||||
validateMaxScannedEntries } = require('../apiUtils/object/lifecycle');
|
||||
const { config } = require('../../Config');
|
||||
|
||||
function handleResult(listParams, requestMaxKeys, authInfo,
|
||||
bucketName, list, isBucketVersioned, log, callback) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
listParams.maxKeys = requestMaxKeys;
|
||||
const res = processCurrents(bucketName, listParams, isBucketVersioned, list);
|
||||
|
||||
pushMetric('listLifecycleCurrents', log, { authInfo, bucket: bucketName });
|
||||
monitoring.promMetrics('GET', bucketName, '200', 'listLifecycleCurrents');
|
||||
return callback(null, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* listLifecycleCurrents - Return list of current versions/masters in bucket
|
||||
* @param {AuthInfo} authInfo - Instance of AuthInfo class with
|
||||
* requester's info
|
||||
* @param {array} locationConstraints - array of location contraint
|
||||
* @param {object} request - http request object
|
||||
* @param {function} log - Werelogs request logger
|
||||
* @param {function} callback - callback to respond to http request
|
||||
* with either error code or xml response body
|
||||
* @return {undefined}
|
||||
*/
|
||||
function listLifecycleCurrents(authInfo, locationConstraints, request, log, callback) {
|
||||
const params = request.query;
|
||||
const bucketName = request.bucketName;
|
||||
|
||||
log.debug('processing request', { method: 'listLifecycleCurrents' });
|
||||
const requestMaxKeys = params['max-keys'] ?
|
||||
Number.parseInt(params['max-keys'], 10) : 1000;
|
||||
if (Number.isNaN(requestMaxKeys) || requestMaxKeys < 0) {
|
||||
monitoring.promMetrics(
|
||||
'GET', bucketName, 400, 'listLifecycleCurrents');
|
||||
return callback(errors.InvalidArgument);
|
||||
}
|
||||
const actualMaxKeys = Math.min(constants.listingHardLimit, requestMaxKeys);
|
||||
|
||||
const minEntriesToBeScanned = 1;
|
||||
const { isValid, maxScannedLifecycleListingEntries } =
|
||||
validateMaxScannedEntries(params, config, minEntriesToBeScanned);
|
||||
if (!isValid) {
|
||||
monitoring.promMetrics('GET', bucketName, 400, 'listLifecycleCurrents');
|
||||
return callback(errors.InvalidArgument);
|
||||
}
|
||||
|
||||
const excludedDataStoreName = params['excluded-data-store-name'];
|
||||
if (excludedDataStoreName && !locationConstraints[excludedDataStoreName]) {
|
||||
const errMsg = getLocationConstraintErrorMessage(excludedDataStoreName);
|
||||
log.error(`locationConstraint is invalid - ${errMsg}`, { locationConstraint: excludedDataStoreName });
|
||||
monitoring.promMetrics('GET', bucketName, 400, 'listLifecycleCurrents');
|
||||
|
||||
return callback(errors.InvalidLocationConstraint.customizeDescription(errMsg));
|
||||
}
|
||||
|
||||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'listLifecycleCurrents',
|
||||
request,
|
||||
};
|
||||
const listParams = {
|
||||
listingType: 'DelimiterCurrent',
|
||||
maxKeys: actualMaxKeys,
|
||||
prefix: params.prefix,
|
||||
beforeDate: params['before-date'],
|
||||
marker: params.marker,
|
||||
excludedDataStoreName,
|
||||
maxScannedLifecycleListingEntries,
|
||||
};
|
||||
|
||||
return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
if (err) {
|
||||
log.debug('error processing request', { method: 'metadataValidateBucket', error: err });
|
||||
monitoring.promMetrics(
|
||||
'GET', bucketName, err.code, 'listLifecycleCurrents');
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
const vcfg = bucket.getVersioningConfiguration();
|
||||
const isBucketVersioned = vcfg && (vcfg.Status === 'Enabled' || vcfg.Status === 'Suspended');
|
||||
|
||||
if (!requestMaxKeys) {
|
||||
const emptyList = {
|
||||
Contents: [],
|
||||
IsTruncated: false,
|
||||
};
|
||||
return handleResult(listParams, requestMaxKeys, authInfo,
|
||||
bucketName, emptyList, isBucketVersioned, log, callback);
|
||||
}
|
||||
|
||||
return services.getLifecycleListing(bucketName, listParams, log,
|
||||
(err, list) => {
|
||||
if (err) {
|
||||
log.debug('error processing request', { method: 'services.getLifecycleListing', error: err });
|
||||
monitoring.promMetrics(
|
||||
'GET', bucketName, err.code, 'listLifecycleCurrents');
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
return handleResult(listParams, requestMaxKeys, authInfo,
|
||||
bucketName, list, isBucketVersioned, log, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listLifecycleCurrents,
|
||||
};
|
|
@ -0,0 +1,127 @@
|
|||
const { errors, versioning } = require('arsenal');
|
||||
const constants = require('../../../constants');
|
||||
const services = require('../../services');
|
||||
const { standardMetadataValidateBucket } = require('../../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../../utapi/utilities');
|
||||
const versionIdUtils = versioning.VersionID;
|
||||
const monitoring = require('../../utilities/monitoringHandler');
|
||||
const { getLocationConstraintErrorMessage, processNonCurrents,
|
||||
validateMaxScannedEntries } = require('../apiUtils/object/lifecycle');
|
||||
const { config } = require('../../Config');
|
||||
|
||||
function handleResult(listParams, requestMaxKeys, authInfo,
|
||||
bucketName, list, log, callback) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
listParams.maxKeys = requestMaxKeys;
|
||||
const res = processNonCurrents(bucketName, listParams, list);
|
||||
|
||||
pushMetric('listLifecycleNonCurrents', log, { authInfo, bucket: bucketName });
|
||||
monitoring.promMetrics('GET', bucketName, '200', 'listLifecycleNonCurrents');
|
||||
return callback(null, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* listLifecycleNonCurrents - Return list of non-current versions in bucket
|
||||
* @param {AuthInfo} authInfo - Instance of AuthInfo class with
|
||||
* requester's info
|
||||
* @param {array} locationConstraints - array of location contraint
|
||||
* @param {object} request - http request object
|
||||
* @param {function} log - Werelogs request logger
|
||||
* @param {function} callback - callback to respond to http request
|
||||
* with either error code or xml response body
|
||||
* @return {undefined}
|
||||
*/
|
||||
function listLifecycleNonCurrents(authInfo, locationConstraints, request, log, callback) {
|
||||
const params = request.query;
|
||||
const bucketName = request.bucketName;
|
||||
|
||||
log.debug('processing request', { method: 'listLifecycleNonCurrents' });
|
||||
const requestMaxKeys = params['max-keys'] ?
|
||||
Number.parseInt(params['max-keys'], 10) : 1000;
|
||||
if (Number.isNaN(requestMaxKeys) || requestMaxKeys < 0) {
|
||||
monitoring.promMetrics(
|
||||
'GET', bucketName, 400, 'listLifecycleNonCurrents');
|
||||
return callback(errors.InvalidArgument);
|
||||
}
|
||||
const actualMaxKeys = Math.min(constants.listingHardLimit, requestMaxKeys);
|
||||
|
||||
// 3 is required as a minimum because we must scan at least three entries to determine version eligibility.
|
||||
// Two entries representing the master key and the following one representing the non-current version.
|
||||
const minEntriesToBeScanned = 3;
|
||||
const { isValid, maxScannedLifecycleListingEntries } =
|
||||
validateMaxScannedEntries(params, config, minEntriesToBeScanned);
|
||||
if (!isValid) {
|
||||
monitoring.promMetrics('GET', bucketName, 400, 'listLifecycleNonCurrents');
|
||||
return callback(errors.InvalidArgument);
|
||||
}
|
||||
|
||||
const excludedDataStoreName = params['excluded-data-store-name'];
|
||||
if (excludedDataStoreName && !locationConstraints[excludedDataStoreName]) {
|
||||
const errMsg = getLocationConstraintErrorMessage(excludedDataStoreName);
|
||||
log.error(`locationConstraint is invalid - ${errMsg}`, { locationConstraint: excludedDataStoreName });
|
||||
monitoring.promMetrics('GET', bucketName, 400, 'listLifecycleCurrents');
|
||||
|
||||
return callback(errors.InvalidLocationConstraint.customizeDescription(errMsg));
|
||||
}
|
||||
|
||||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'listLifecycleNonCurrents',
|
||||
request,
|
||||
};
|
||||
const listParams = {
|
||||
listingType: 'DelimiterNonCurrent',
|
||||
maxKeys: actualMaxKeys,
|
||||
prefix: params.prefix,
|
||||
beforeDate: params['before-date'],
|
||||
keyMarker: params['key-marker'],
|
||||
excludedDataStoreName,
|
||||
maxScannedLifecycleListingEntries,
|
||||
};
|
||||
|
||||
listParams.versionIdMarker = params['version-id-marker'] ?
|
||||
versionIdUtils.decode(params['version-id-marker']) : undefined;
|
||||
|
||||
return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
if (err) {
|
||||
log.debug('error processing request', { method: 'metadataValidateBucket', error: err });
|
||||
monitoring.promMetrics(
|
||||
'GET', bucketName, err.code, 'listLifecycleNonCurrents');
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
const vcfg = bucket.getVersioningConfiguration();
|
||||
const isBucketVersioned = vcfg && (vcfg.Status === 'Enabled' || vcfg.Status === 'Suspended');
|
||||
if (!isBucketVersioned) {
|
||||
log.debug('bucket is not versioned');
|
||||
return callback(errors.InvalidRequest.customizeDescription(
|
||||
'bucket is not versioned'), null);
|
||||
}
|
||||
|
||||
if (!requestMaxKeys) {
|
||||
const emptyList = {
|
||||
Contents: [],
|
||||
IsTruncated: false,
|
||||
};
|
||||
return handleResult(listParams, requestMaxKeys, authInfo,
|
||||
bucketName, emptyList, log, callback);
|
||||
}
|
||||
|
||||
return services.getLifecycleListing(bucketName, listParams, log,
|
||||
(err, list) => {
|
||||
if (err) {
|
||||
log.debug('error processing request', { method: 'services.getLifecycleListing', error: err });
|
||||
monitoring.promMetrics(
|
||||
'GET', bucketName, err.code, 'listLifecycleNonCurrents');
|
||||
return callback(err, null);
|
||||
}
|
||||
return handleResult(listParams, requestMaxKeys, authInfo,
|
||||
bucketName, list, log, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listLifecycleNonCurrents,
|
||||
};
|
|
@ -0,0 +1,112 @@
|
|||
const { errors } = require('arsenal');
|
||||
const constants = require('../../../constants');
|
||||
const services = require('../../services');
|
||||
const { standardMetadataValidateBucket } = require('../../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../../utapi/utilities');
|
||||
const monitoring = require('../../utilities/monitoringHandler');
|
||||
const { processOrphans, validateMaxScannedEntries } = require('../apiUtils/object/lifecycle');
|
||||
const { config } = require('../../Config');
|
||||
|
||||
function handleResult(listParams, requestMaxKeys, authInfo,
|
||||
bucketName, list, log, callback) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
listParams.maxKeys = requestMaxKeys;
|
||||
const res = processOrphans(bucketName, listParams, list);
|
||||
|
||||
pushMetric('listLifecycleOrphanDeleteMarkers', log, { authInfo, bucket: bucketName });
|
||||
monitoring.promMetrics('GET', bucketName, '200', 'listLifecycleOrphanDeleteMarkers');
|
||||
return callback(null, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* listLifecycleOrphanDeleteMarkers - Return list of expired object delete marker in bucket
|
||||
* @param {AuthInfo} authInfo - Instance of AuthInfo class with
|
||||
* requester's info
|
||||
* @param {array} locationConstraints - array of location contraint
|
||||
* @param {object} request - http request object
|
||||
* @param {function} log - Werelogs request logger
|
||||
* @param {function} callback - callback to respond to http request
|
||||
* with either error code or xml response body
|
||||
* @return {undefined}
|
||||
*/
|
||||
function listLifecycleOrphanDeleteMarkers(authInfo, locationConstraints, request, log, callback) {
|
||||
const params = request.query;
|
||||
const bucketName = request.bucketName;
|
||||
|
||||
log.debug('processing request', { method: 'listLifecycleOrphanDeleteMarkers' });
|
||||
const requestMaxKeys = params['max-keys'] ?
|
||||
Number.parseInt(params['max-keys'], 10) : 1000;
|
||||
if (Number.isNaN(requestMaxKeys) || requestMaxKeys < 0) {
|
||||
monitoring.promMetrics(
|
||||
'GET', bucketName, 400, 'listLifecycleOrphanDeleteMarkers');
|
||||
return callback(errors.InvalidArgument);
|
||||
}
|
||||
const actualMaxKeys = Math.min(constants.listingHardLimit, requestMaxKeys);
|
||||
|
||||
// 3 is required as a minimum because we must scan at least three entries to determine version eligibility.
|
||||
// Two entries representing the master key and the following one representing the non-current version.
|
||||
const minEntriesToBeScanned = 3;
|
||||
const { isValid, maxScannedLifecycleListingEntries } =
|
||||
validateMaxScannedEntries(params, config, minEntriesToBeScanned);
|
||||
if (!isValid) {
|
||||
monitoring.promMetrics('GET', bucketName, 400, 'listLifecycleOrphanDeleteMarkers');
|
||||
return callback(errors.InvalidArgument);
|
||||
}
|
||||
|
||||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'listLifecycleOrphanDeleteMarkers',
|
||||
request,
|
||||
};
|
||||
const listParams = {
|
||||
listingType: 'DelimiterOrphanDeleteMarker',
|
||||
maxKeys: actualMaxKeys,
|
||||
prefix: params.prefix,
|
||||
beforeDate: params['before-date'],
|
||||
marker: params.marker,
|
||||
maxScannedLifecycleListingEntries,
|
||||
};
|
||||
|
||||
return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
if (err) {
|
||||
log.debug('error processing request', { method: 'metadataValidateBucket', error: err });
|
||||
monitoring.promMetrics(
|
||||
'GET', bucketName, err.code, 'listLifecycleOrphanDeleteMarkers');
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
const vcfg = bucket.getVersioningConfiguration();
|
||||
const isBucketVersioned = vcfg && (vcfg.Status === 'Enabled' || vcfg.Status === 'Suspended');
|
||||
if (!isBucketVersioned) {
|
||||
log.debug('bucket is not versioned or suspended');
|
||||
return callback(errors.InvalidRequest.customizeDescription(
|
||||
'bucket is not versioned'), null);
|
||||
}
|
||||
|
||||
if (!requestMaxKeys) {
|
||||
const emptyList = {
|
||||
Contents: [],
|
||||
IsTruncated: false,
|
||||
};
|
||||
return handleResult(listParams, requestMaxKeys, authInfo,
|
||||
bucketName, emptyList, log, callback);
|
||||
}
|
||||
|
||||
return services.getLifecycleListing(bucketName, listParams, log,
|
||||
(err, list) => {
|
||||
if (err) {
|
||||
log.debug('error processing request', { error: err });
|
||||
monitoring.promMetrics(
|
||||
'GET', bucketName, err.code, 'listLifecycleOrphanDeleteMarkers');
|
||||
return callback(err, null);
|
||||
}
|
||||
return handleResult(listParams, requestMaxKeys, authInfo,
|
||||
bucketName, list, log, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listLifecycleOrphanDeleteMarkers,
|
||||
};
|
|
@ -2,7 +2,7 @@ const { errors } = require('arsenal');
|
|||
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const deleteBucket = require('./apiUtils/bucket/bucketDeletion');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
|
||||
|
@ -34,7 +34,7 @@ function bucketDelete(authInfo, request, log, cb) {
|
|||
request,
|
||||
};
|
||||
|
||||
return metadataValidateBucket(metadataValParams, log,
|
||||
return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, bucketMD) => {
|
||||
const corsHeaders = collectCorsHeaders(request.headers.origin,
|
||||
request.method, bucketMD);
|
||||
|
@ -48,7 +48,7 @@ function bucketDelete(authInfo, request, log, cb) {
|
|||
log.trace('passed checks',
|
||||
{ method: 'metadataValidateBucket' });
|
||||
return deleteBucket(authInfo, bucketMD, bucketName,
|
||||
authInfo.getCanonicalID(), log, err => {
|
||||
authInfo.getCanonicalID(), request, log, err => {
|
||||
if (err) {
|
||||
monitoring.promMetrics(
|
||||
'DELETE', bucketName, err.code, 'deleteBucket');
|
||||
|
|
|
@ -38,7 +38,8 @@ function bucketDeleteCors(authInfo, request, log, callback) {
|
|||
}
|
||||
log.trace('found bucket in metadata');
|
||||
|
||||
if (!isBucketAuthorized(bucket, requestType, canonicalID, authInfo, log, request)) {
|
||||
if (!isBucketAuthorized(bucket, request.apiMethods || requestType, canonicalID,
|
||||
authInfo, log, request, request.actionImplicitDenies)) {
|
||||
log.debug('access denied for user on bucket', {
|
||||
requestType,
|
||||
method: 'bucketDeleteCors',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const async = require('async');
|
||||
|
||||
const metadata = require('../metadata/wrapper');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const { checkExpectedBucketOwner } = require('./apiUtils/authorization/bucketOwner');
|
||||
|
@ -21,12 +21,12 @@ function bucketDeleteEncryption(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketDeleteEncryption',
|
||||
requestType: request.apiMethods || 'bucketDeleteEncryption',
|
||||
request,
|
||||
};
|
||||
|
||||
return async.waterfall([
|
||||
next => metadataValidateBucket(metadataValParams, log, next),
|
||||
next => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, next),
|
||||
(bucket, next) => checkExpectedBucketOwner(request.headers, bucket, log, err => next(err, bucket)),
|
||||
(bucket, next) => {
|
||||
const sseConfig = bucket.getServerSideEncryption();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const metadata = require('../metadata/wrapper');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
|
@ -18,10 +18,10 @@ function bucketDeleteLifecycle(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketDeleteLifecycle',
|
||||
requestType: request.apiMethods || 'bucketDeleteLifecycle',
|
||||
request,
|
||||
};
|
||||
return metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(headers.origin, method, bucket);
|
||||
if (err) {
|
||||
log.debug('error processing request', {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const metadata = require('../metadata/wrapper');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
|
||||
/**
|
||||
|
@ -16,10 +16,10 @@ function bucketDeletePolicy(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketDeletePolicy',
|
||||
requestType: request.apiMethods || 'bucketDeletePolicy',
|
||||
request,
|
||||
};
|
||||
return metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(headers.origin, method, bucket);
|
||||
if (err) {
|
||||
log.debug('error processing request', {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
const { waterfall } = require('async');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const metadata = require('../metadata/wrapper');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
|
||||
const requestType = 'bucketDeleteQuota';
|
||||
|
||||
/**
|
||||
* Bucket Update Quota - Update bucket quota
|
||||
* @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info
|
||||
* @param {object} request - http request object
|
||||
* @param {object} log - Werelogs logger
|
||||
* @param {function} callback - callback to server
|
||||
* @return {undefined}
|
||||
*/
|
||||
function bucketDeleteQuota(authInfo, request, log, callback) {
|
||||
log.debug('processing request', { method: 'bucketDeleteQuota' });
|
||||
|
||||
const { bucketName } = request;
|
||||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: request.apiMethods || requestType,
|
||||
request,
|
||||
};
|
||||
return waterfall([
|
||||
next => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, bucket) => next(err, bucket)),
|
||||
(bucket, next) => {
|
||||
bucket.setQuota(0);
|
||||
metadata.updateBucket(bucket.getName(), bucket, log, err =>
|
||||
next(err, bucket));
|
||||
},
|
||||
], (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(request.headers.origin,
|
||||
request.method, bucket);
|
||||
if (err) {
|
||||
log.debug('error processing request', {
|
||||
error: err,
|
||||
method: 'bucketDeleteQuota'
|
||||
});
|
||||
monitoring.promMetrics('DELETE', bucketName, err.code,
|
||||
'bucketDeleteQuota');
|
||||
return callback(err, err.code, corsHeaders);
|
||||
}
|
||||
monitoring.promMetrics(
|
||||
'DELETE', bucketName, '204', 'bucketDeleteQuota');
|
||||
pushMetric('bucketDeleteQuota', log, {
|
||||
authInfo,
|
||||
bucket: bucketName,
|
||||
});
|
||||
return callback(null, 204, corsHeaders);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = bucketDeleteQuota;
|
|
@ -1,5 +1,5 @@
|
|||
const metadata = require('../metadata/wrapper');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
|
@ -18,10 +18,10 @@ function bucketDeleteReplication(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketDeleteReplication',
|
||||
requestType: request.apiMethods || 'bucketDeleteReplication',
|
||||
request,
|
||||
};
|
||||
return metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(headers.origin, method, bucket);
|
||||
if (err) {
|
||||
log.debug('error processing request', {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const { waterfall } = require('async');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
const metadata = require('../metadata/wrapper');
|
||||
|
@ -20,16 +20,20 @@ function bucketDeleteTagging(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketDeleteTagging',
|
||||
requestType: request.apiMethods || 'bucketDeleteTagging',
|
||||
request,
|
||||
};
|
||||
|
||||
let bucket = null;
|
||||
return waterfall([
|
||||
next => metadataValidateBucket(metadataValParams, log,
|
||||
next => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, b) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
bucket = b;
|
||||
bucket.setTags([]);
|
||||
return next(err);
|
||||
return next();
|
||||
}),
|
||||
next => metadata.updateBucket(bucket.getName(), bucket, log, next),
|
||||
], err => {
|
||||
|
|
|
@ -30,7 +30,8 @@ function bucketDeleteWebsite(authInfo, request, log, callback) {
|
|||
}
|
||||
log.trace('found bucket in metadata');
|
||||
|
||||
if (!isBucketAuthorized(bucket, requestType, canonicalID, authInfo, log, request)) {
|
||||
if (!isBucketAuthorized(bucket, request.apiMethods || requestType, canonicalID,
|
||||
authInfo, log, request, request.actionImplicitDenies)) {
|
||||
log.debug('access denied for user on bucket', {
|
||||
requestType,
|
||||
method: 'bucketDeleteWebsite',
|
||||
|
|
|
@ -2,7 +2,7 @@ const querystring = require('querystring');
|
|||
const { errors, versioning, s3middleware } = require('arsenal');
|
||||
const constants = require('../../constants');
|
||||
const services = require('../services');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const escapeForXml = s3middleware.escapeForXml;
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
|
@ -210,7 +210,7 @@ function processMasterVersions(bucketName, listParams, list) {
|
|||
xmlParams.forEach(p => {
|
||||
if (p.value && skipUrlEncoding.has(p.tag)) {
|
||||
xml.push(`<${p.tag}>${p.value}</${p.tag}>`);
|
||||
} else if (p.value || p.tag === 'KeyCount') {
|
||||
} else if (p.value || p.tag === 'KeyCount' || p.tag === 'MaxKeys') {
|
||||
xml.push(`<${p.tag}>${escapeXmlFn(p.value)}</${p.tag}>`);
|
||||
} else if (p.tag !== 'NextMarker' &&
|
||||
p.tag !== 'EncodingType' &&
|
||||
|
@ -322,7 +322,7 @@ function bucketGet(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketGet',
|
||||
requestType: request.apiMethods || 'bucketGet',
|
||||
request,
|
||||
};
|
||||
const listParams = {
|
||||
|
@ -345,7 +345,7 @@ function bucketGet(authInfo, request, log, callback) {
|
|||
listParams.marker = params.marker;
|
||||
}
|
||||
|
||||
metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(request.headers.origin,
|
||||
request.method, bucket);
|
||||
if (err) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const aclUtils = require('../utilities/aclUtils');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const vault = require('../auth/vault');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
|
@ -44,7 +44,7 @@ function bucketGetACL(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketGetACL',
|
||||
requestType: request.apiMethods || 'bucketGetACL',
|
||||
request,
|
||||
};
|
||||
const grantInfo = {
|
||||
|
@ -55,7 +55,7 @@ function bucketGetACL(authInfo, request, log, callback) {
|
|||
},
|
||||
};
|
||||
|
||||
metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(request.headers.origin,
|
||||
request.method, bucket);
|
||||
if (err) {
|
||||
|
|
|
@ -39,7 +39,8 @@ function bucketGetCors(authInfo, request, log, callback) {
|
|||
const corsHeaders = collectCorsHeaders(request.headers.origin,
|
||||
request.method, bucket);
|
||||
|
||||
if (!isBucketAuthorized(bucket, requestType, canonicalID, authInfo, log, request)) {
|
||||
if (!isBucketAuthorized(bucket, request.apiMethods || requestType, canonicalID,
|
||||
authInfo, log, request, request.actionImplicitDenies)) {
|
||||
log.debug('access denied for user on bucket', {
|
||||
requestType,
|
||||
method: 'bucketGetCors',
|
||||
|
|
|
@ -4,7 +4,7 @@ const async = require('async');
|
|||
const { pushMetric } = require('../utapi/utilities');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const { checkExpectedBucketOwner } = require('./apiUtils/authorization/bucketOwner');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const escapeForXml = s3middleware.escapeForXml;
|
||||
|
||||
/**
|
||||
|
@ -22,12 +22,12 @@ function bucketGetEncryption(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketGetEncryption',
|
||||
requestType: request.apiMethods || 'bucketGetEncryption',
|
||||
request,
|
||||
};
|
||||
|
||||
return async.waterfall([
|
||||
next => metadataValidateBucket(metadataValParams, log, next),
|
||||
next => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, next),
|
||||
(bucket, next) => checkExpectedBucketOwner(request.headers, bucket, log, err => next(err, bucket)),
|
||||
(bucket, next) => {
|
||||
// If sseInfo is present but the `mandatory` flag is not set
|
||||
|
|
|
@ -2,7 +2,7 @@ const { errors } = require('arsenal');
|
|||
const LifecycleConfiguration =
|
||||
require('arsenal').models.LifecycleConfiguration;
|
||||
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
|
@ -21,10 +21,10 @@ function bucketGetLifecycle(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketGetLifecycle',
|
||||
requestType: request.apiMethods || 'bucketGetLifecycle',
|
||||
request,
|
||||
};
|
||||
return metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(headers.origin, method, bucket);
|
||||
if (err) {
|
||||
log.debug('error processing request', {
|
||||
|
|
|
@ -41,7 +41,8 @@ function bucketGetLocation(authInfo, request, log, callback) {
|
|||
const corsHeaders = collectCorsHeaders(request.headers.origin,
|
||||
request.method, bucket);
|
||||
|
||||
if (!isBucketAuthorized(bucket, requestType, canonicalID, authInfo, log, request)) {
|
||||
if (!isBucketAuthorized(bucket, request.apiMethods || requestType, canonicalID,
|
||||
authInfo, log, request, request.actionImplicitDenies)) {
|
||||
log.debug('access denied for account on bucket', {
|
||||
requestType,
|
||||
method: 'bucketGetLocation',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const { NotificationConfiguration } = require('arsenal').models;
|
||||
|
@ -37,11 +37,11 @@ function bucketGetNotification(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketGetNotification',
|
||||
requestType: request.apiMethods || 'bucketGetNotification',
|
||||
request,
|
||||
};
|
||||
|
||||
return metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(headers.origin, method, bucket);
|
||||
if (err) {
|
||||
log.debug('error processing request', {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const { errors } = require('arsenal');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const ObjectLockConfiguration =
|
||||
|
@ -33,10 +33,10 @@ function bucketGetObjectLock(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketGetObjectLock',
|
||||
requestType: request.apiMethods || 'bucketGetObjectLock',
|
||||
request,
|
||||
};
|
||||
return metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(headers.origin, method, bucket);
|
||||
if (err) {
|
||||
log.debug('error processing request', {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const { errors } = require('arsenal');
|
||||
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
|
||||
/**
|
||||
|
@ -17,11 +17,11 @@ function bucketGetPolicy(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketGetPolicy',
|
||||
requestType: request.apiMethods || 'bucketGetPolicy',
|
||||
request,
|
||||
};
|
||||
|
||||
return metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(headers.origin, method, bucket);
|
||||
if (err) {
|
||||
log.debug('error processing request', {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
const { errors } = require('arsenal');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
|
||||
/**
|
||||
* bucketGetQuota - Get the bucket quota
|
||||
* @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info
|
||||
* @param {object} request - http request object
|
||||
* @param {object} log - Werelogs logger
|
||||
* @param {function} callback - callback to server
|
||||
* @return {undefined}
|
||||
*/
|
||||
function bucketGetQuota(authInfo, request, log, callback) {
|
||||
log.debug('processing request', { method: 'bucketGetQuota' });
|
||||
const { bucketName, headers, method } = request;
|
||||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: request.apiMethods || 'bucketGetQuota',
|
||||
request,
|
||||
};
|
||||
const xml = [];
|
||||
|
||||
return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(headers.origin, method, bucket);
|
||||
if (err) {
|
||||
log.debug('error processing request', {
|
||||
error: err,
|
||||
method: 'bucketGetQuota',
|
||||
});
|
||||
return callback(err, null, corsHeaders);
|
||||
}
|
||||
xml.push(
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<GetBucketQuota>',
|
||||
'<Name>', bucket.getName(), '</Name>',
|
||||
);
|
||||
const bucketQuota = bucket.getQuota();
|
||||
if (!bucketQuota) {
|
||||
log.debug('bucket has no quota', {
|
||||
method: 'bucketGetQuota',
|
||||
});
|
||||
return callback(errors.NoSuchQuota, null,
|
||||
corsHeaders);
|
||||
}
|
||||
xml.push('<Quota>', bucketQuota, '</Quota>',
|
||||
'</GetBucketQuota>');
|
||||
|
||||
pushMetric('getBucketQuota', log, {
|
||||
authInfo,
|
||||
bucket: bucketName,
|
||||
});
|
||||
return callback(null, xml.join(''), corsHeaders);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = bucketGetQuota;
|
|
@ -1,6 +1,6 @@
|
|||
const { errors } = require('arsenal');
|
||||
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const { getReplicationConfigurationXML } =
|
||||
require('./apiUtils/bucket/getReplicationConfiguration');
|
||||
|
@ -21,10 +21,10 @@ function bucketGetReplication(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketGetReplication',
|
||||
requestType: request.apiMethods || 'bucketGetReplication',
|
||||
request,
|
||||
};
|
||||
return metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(headers.origin, method, bucket);
|
||||
if (err) {
|
||||
log.debug('error processing request', {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const { checkExpectedBucketOwner } = require('./apiUtils/authorization/bucketOwner');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
|
@ -37,7 +37,7 @@ const escapeForXml = s3middleware.escapeForXml;
|
|||
function tagsToXml(tags) {
|
||||
const xml = [];
|
||||
|
||||
xml.push('<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Tagging><TagSet>');
|
||||
xml.push('<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Tagging> <TagSet>');
|
||||
|
||||
tags.forEach(tag => {
|
||||
xml.push('<Tag>');
|
||||
|
@ -46,7 +46,7 @@ function tagsToXml(tags) {
|
|||
xml.push('</Tag>');
|
||||
});
|
||||
|
||||
xml.push('</TagSet></Tagging>');
|
||||
xml.push('</TagSet> </Tagging>');
|
||||
|
||||
return xml.join('');
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ function bucketGetTagging(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketGetTagging',
|
||||
requestType: request.apiMethods || 'bucketGetTagging',
|
||||
request,
|
||||
};
|
||||
let bucket = null;
|
||||
|
@ -75,7 +75,7 @@ function bucketGetTagging(authInfo, request, log, callback) {
|
|||
let tags = null;
|
||||
|
||||
return waterfall([
|
||||
next => metadataValidateBucket(metadataValParams, log,
|
||||
next => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, b) => {
|
||||
bucket = b;
|
||||
return next(err);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
|
@ -54,11 +54,11 @@ function bucketGetVersioning(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketGetVersioning',
|
||||
requestType: request.apiMethods || 'bucketGetVersioning',
|
||||
request,
|
||||
};
|
||||
|
||||
metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(request.headers.origin,
|
||||
request.method, bucket);
|
||||
if (err) {
|
||||
|
|
|
@ -39,7 +39,8 @@ function bucketGetWebsite(authInfo, request, log, callback) {
|
|||
|
||||
const corsHeaders = collectCorsHeaders(request.headers.origin,
|
||||
request.method, bucket);
|
||||
if (!isBucketAuthorized(bucket, requestType, canonicalID, authInfo, log, request)) {
|
||||
if (!isBucketAuthorized(bucket, request.apiMethods || requestType, canonicalID,
|
||||
authInfo, log, request, request.actionImplicitDenies)) {
|
||||
log.debug('access denied for user on bucket', {
|
||||
requestType,
|
||||
method: 'bucketGetWebsite',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
|
@ -19,10 +19,10 @@ function bucketHead(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketHead',
|
||||
requestType: request.apiMethods || 'bucketHead',
|
||||
request,
|
||||
};
|
||||
metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(request.headers.origin,
|
||||
request.method, bucket);
|
||||
if (err) {
|
||||
|
|
|
@ -45,9 +45,8 @@ function checkLocationConstraint(request, locationConstraint, log) {
|
|||
} else if (parsedHost && restEndpoints[parsedHost]) {
|
||||
locationConstraintChecked = restEndpoints[parsedHost];
|
||||
} else {
|
||||
log.trace('no location constraint provided on bucket put;' +
|
||||
'setting us-east-1');
|
||||
locationConstraintChecked = 'us-east-1';
|
||||
locationConstraintChecked = Object.keys(locationConstrains)[0];
|
||||
log.trace('no location constraint provided on bucket put; setting '+locationConstraintChecked);
|
||||
}
|
||||
|
||||
if (!locationConstraints[locationConstraintChecked]) {
|
||||
|
|
|
@ -6,7 +6,7 @@ const aclUtils = require('../utilities/aclUtils');
|
|||
const { cleanUpBucket } = require('./apiUtils/bucket/bucketCreation');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const constants = require('../../constants');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const vault = require('../auth/vault');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
|
@ -44,7 +44,7 @@ const monitoring = require('../utilities/monitoringHandler');
|
|||
function bucketPutACL(authInfo, request, log, callback) {
|
||||
log.debug('processing request', { method: 'bucketPutACL' });
|
||||
|
||||
const bucketName = request.bucketName;
|
||||
const { bucketName } = request;
|
||||
const canonicalID = authInfo.getCanonicalID();
|
||||
const newCannedACL = request.headers['x-amz-acl'];
|
||||
const possibleCannedACL = [
|
||||
|
@ -54,19 +54,6 @@ function bucketPutACL(authInfo, request, log, callback) {
|
|||
'authenticated-read',
|
||||
'log-delivery-write',
|
||||
];
|
||||
if (newCannedACL && possibleCannedACL.indexOf(newCannedACL) === -1) {
|
||||
log.trace('invalid canned acl argument', {
|
||||
acl: newCannedACL,
|
||||
method: 'bucketPutACL',
|
||||
});
|
||||
monitoring.promMetrics('PUT', bucketName, 400, 'bucketPutACL');
|
||||
return callback(errors.InvalidArgument);
|
||||
}
|
||||
if (!aclUtils.checkGrantHeaderValidity(request.headers)) {
|
||||
log.trace('invalid acl header');
|
||||
monitoring.promMetrics('PUT', bucketName, 400, 'bucketPutACL');
|
||||
return callback(errors.InvalidArgument);
|
||||
}
|
||||
const possibleGroups = [constants.allAuthedUsersId,
|
||||
constants.publicId,
|
||||
constants.logId,
|
||||
|
@ -74,7 +61,7 @@ function bucketPutACL(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketPutACL',
|
||||
requestType: request.apiMethods || 'bucketPutACL',
|
||||
request,
|
||||
};
|
||||
const possibleGrants = ['FULL_CONTROL', 'WRITE',
|
||||
|
@ -105,7 +92,7 @@ function bucketPutACL(authInfo, request, log, callback) {
|
|||
|
||||
return async.waterfall([
|
||||
function waterfall1(next) {
|
||||
metadataValidateBucket(metadataValParams, log,
|
||||
standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, bucket) => {
|
||||
if (err) {
|
||||
log.trace('request authorization failed', {
|
||||
|
@ -114,6 +101,18 @@ function bucketPutACL(authInfo, request, log, callback) {
|
|||
});
|
||||
return next(err, bucket);
|
||||
}
|
||||
// if the API call is allowed, ensure that the parameters are valid
|
||||
if (newCannedACL && possibleCannedACL.indexOf(newCannedACL) === -1) {
|
||||
log.trace('invalid canned acl argument', {
|
||||
acl: newCannedACL,
|
||||
method: 'bucketPutACL',
|
||||
});
|
||||
return next(errors.InvalidArgument);
|
||||
}
|
||||
if (!aclUtils.checkGrantHeaderValidity(request.headers)) {
|
||||
log.trace('invalid acl header');
|
||||
return next(errors.InvalidArgument);
|
||||
}
|
||||
return next(null, bucket);
|
||||
});
|
||||
},
|
||||
|
|
|
@ -23,7 +23,7 @@ const requestType = 'bucketPutCors';
|
|||
*/
|
||||
function bucketPutCors(authInfo, request, log, callback) {
|
||||
log.debug('processing request', { method: 'bucketPutCors' });
|
||||
const bucketName = request.bucketName;
|
||||
const { bucketName } = request;
|
||||
const canonicalID = authInfo.getCanonicalID();
|
||||
|
||||
if (!request.post) {
|
||||
|
@ -70,7 +70,8 @@ function bucketPutCors(authInfo, request, log, callback) {
|
|||
});
|
||||
},
|
||||
function validateBucketAuthorization(bucket, rules, corsHeaders, next) {
|
||||
if (!isBucketAuthorized(bucket, requestType, canonicalID, authInfo, log, request)) {
|
||||
if (!isBucketAuthorized(bucket, request.apiMethods || requestType, canonicalID,
|
||||
authInfo, log, request, request.actionImplicitDenies)) {
|
||||
log.debug('access denied for account on bucket', {
|
||||
requestType,
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ const async = require('async');
|
|||
const { parseEncryptionXml } = require('./apiUtils/bucket/bucketEncryption');
|
||||
const { checkExpectedBucketOwner } = require('./apiUtils/authorization/bucketOwner');
|
||||
const metadata = require('../metadata/wrapper');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const kms = require('../kms/wrapper');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
|
@ -18,17 +18,17 @@ const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
|||
*/
|
||||
|
||||
function bucketPutEncryption(authInfo, request, log, callback) {
|
||||
const bucketName = request.bucketName;
|
||||
const { bucketName } = request;
|
||||
|
||||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketPutEncryption',
|
||||
requestType: request.apiMethods || 'bucketPutEncryption',
|
||||
request,
|
||||
};
|
||||
|
||||
return async.waterfall([
|
||||
next => metadataValidateBucket(metadataValParams, log, next),
|
||||
next => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, next),
|
||||
(bucket, next) => checkExpectedBucketOwner(request.headers, bucket, log, err => next(err, bucket)),
|
||||
(bucket, next) => {
|
||||
log.trace('parsing encryption config', { method: 'bucketPutEncryption' });
|
||||
|
|
|
@ -7,7 +7,7 @@ const config = require('../Config').config;
|
|||
const parseXML = require('../utilities/parseXML');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const metadata = require('../metadata/wrapper');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
|
||||
|
@ -23,11 +23,11 @@ const monitoring = require('../utilities/monitoringHandler');
|
|||
function bucketPutLifecycle(authInfo, request, log, callback) {
|
||||
log.debug('processing request', { method: 'bucketPutLifecycle' });
|
||||
|
||||
const bucketName = request.bucketName;
|
||||
const { bucketName } = request;
|
||||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketPutLifecycle',
|
||||
requestType: request.apiMethods || 'bucketPutLifecycle',
|
||||
request,
|
||||
};
|
||||
return waterfall([
|
||||
|
@ -45,7 +45,7 @@ function bucketPutLifecycle(authInfo, request, log, callback) {
|
|||
return next(null, configObj);
|
||||
});
|
||||
},
|
||||
(lcConfig, next) => metadataValidateBucket(metadataValParams, log,
|
||||
(lcConfig, next) => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, bucket) => {
|
||||
if (err) {
|
||||
return next(err, bucket);
|
||||
|
|
|
@ -4,7 +4,7 @@ const parseXML = require('../utilities/parseXML');
|
|||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const getNotificationConfiguration = require('./apiUtils/bucket/getNotificationConfiguration');
|
||||
const metadata = require('../metadata/wrapper');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
|
||||
/**
|
||||
|
@ -19,11 +19,11 @@ const { pushMetric } = require('../utapi/utilities');
|
|||
function bucketPutNotification(authInfo, request, log, callback) {
|
||||
log.debug('processing request', { method: 'bucketPutNotification' });
|
||||
|
||||
const bucketName = request.bucketName;
|
||||
const { bucketName } = request;
|
||||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketPutNotification',
|
||||
requestType: request.apiMethods || 'bucketPutNotification',
|
||||
request,
|
||||
};
|
||||
|
||||
|
@ -34,7 +34,7 @@ function bucketPutNotification(authInfo, request, log, callback) {
|
|||
const notifConfig = notificationConfig.error ? undefined : notificationConfig;
|
||||
process.nextTick(() => next(notificationConfig.error, notifConfig));
|
||||
},
|
||||
(notifConfig, next) => metadataValidateBucket(metadataValParams, log,
|
||||
(notifConfig, next) => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, bucket) => next(err, bucket, notifConfig)),
|
||||
(bucket, notifConfig, next) => {
|
||||
bucket.setNotificationConfiguration(notifConfig);
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
const { waterfall } = require('async');
|
||||
const arsenal = require('arsenal');
|
||||
|
||||
const errors = arsenal.errors;
|
||||
const ObjectLockConfiguration = arsenal.models.ObjectLockConfiguration;
|
||||
const { errors } = arsenal;
|
||||
const { models: { ObjectLockConfiguration } } = arsenal;
|
||||
|
||||
const parseXML = require('../utilities/parseXML');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const metadata = require('../metadata/wrapper');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
|
||||
/**
|
||||
|
@ -26,7 +26,7 @@ function bucketPutObjectLock(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketPutObjectLock',
|
||||
requestType: request.apiMethods || 'bucketPutObjectLock',
|
||||
request,
|
||||
};
|
||||
return waterfall([
|
||||
|
@ -41,7 +41,7 @@ function bucketPutObjectLock(authInfo, request, log, callback) {
|
|||
return next(configObj.error || null, configObj);
|
||||
});
|
||||
},
|
||||
(objectLockConfig, next) => metadataValidateBucket(metadataValParams,
|
||||
(objectLockConfig, next) => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies,
|
||||
log, (err, bucket) => {
|
||||
if (err) {
|
||||
return next(err, bucket);
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
const async = require('async');
|
||||
const { errors, models } = require('arsenal');
|
||||
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const metadata = require('../metadata/wrapper');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { validatePolicyResource } =
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { validatePolicyResource, validatePolicyConditions } =
|
||||
require('./apiUtils/authorization/permissionChecks');
|
||||
const { BucketPolicy } = models;
|
||||
|
||||
|
@ -17,9 +16,8 @@ const { BucketPolicy } = models;
|
|||
function _checkNotImplementedPolicy(policyString) {
|
||||
// bucket names and key names cannot include "", so including those
|
||||
// isolates not implemented keys
|
||||
return policyString.includes('"Condition"')
|
||||
|| policyString.includes('"Service"')
|
||||
|| policyString.includes('"Federated"');
|
||||
return policyString.includes('"Service"')
|
||||
|| policyString.includes('"Federated"');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -37,7 +35,7 @@ function bucketPutPolicy(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketPutPolicy',
|
||||
requestType: request.apiMethods || 'bucketPutPolicy',
|
||||
request,
|
||||
};
|
||||
|
||||
|
@ -67,10 +65,10 @@ function bucketPutPolicy(authInfo, request, log, callback) {
|
|||
return next(errors.MalformedPolicy.customizeDescription(
|
||||
'Policy has invalid resource'));
|
||||
}
|
||||
return next(null, bucketPolicy);
|
||||
return next(validatePolicyConditions(bucketPolicy), bucketPolicy);
|
||||
});
|
||||
},
|
||||
(bucketPolicy, next) => metadataValidateBucket(metadataValParams, log,
|
||||
(bucketPolicy, next) => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, bucket) => {
|
||||
if (err) {
|
||||
return next(err, bucket);
|
||||
|
|
|
@ -2,7 +2,7 @@ const { waterfall } = require('async');
|
|||
const { errors } = require('arsenal');
|
||||
|
||||
const metadata = require('../metadata/wrapper');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const { getReplicationConfiguration } =
|
||||
require('./apiUtils/bucket/getReplicationConfiguration');
|
||||
|
@ -30,7 +30,7 @@ function bucketPutReplication(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketPutReplication',
|
||||
requestType: request.apiMethods || 'bucketPutReplication',
|
||||
request,
|
||||
};
|
||||
return waterfall([
|
||||
|
@ -39,7 +39,7 @@ function bucketPutReplication(authInfo, request, log, callback) {
|
|||
// Check bucket user privileges and ensure versioning is 'Enabled'.
|
||||
(config, next) =>
|
||||
// TODO: Validate that destination bucket exists and has versioning.
|
||||
metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ const { s3middleware } = require('arsenal');
|
|||
|
||||
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const metadata = require('../metadata/wrapper');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const { checkExpectedBucketOwner } = require('./apiUtils/authorization/bucketOwner');
|
||||
|
@ -38,11 +38,12 @@ function bucketPutTagging(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketPutTagging',
|
||||
requestType: request.apiMethods || 'bucketPutTagging',
|
||||
request,
|
||||
};
|
||||
let bucket = null;
|
||||
return waterfall([
|
||||
next => metadataValidateBucket(metadataValParams, log,
|
||||
next => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, b) => {
|
||||
bucket = b;
|
||||
return next(err);
|
||||
|
|
|
@ -4,7 +4,7 @@ const { errors } = require('arsenal');
|
|||
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const metadata = require('../metadata/wrapper');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const versioningNotImplBackends =
|
||||
require('../../constants').versioningNotImplBackends;
|
||||
|
@ -119,12 +119,12 @@ function bucketPutVersioning(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketPutVersioning',
|
||||
requestType: request.apiMethods || 'bucketPutVersioning',
|
||||
request,
|
||||
};
|
||||
return waterfall([
|
||||
next => _parseXML(request, log, next),
|
||||
next => metadataValidateBucket(metadataValParams, log,
|
||||
next => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, bucket) => next(err, bucket)), // ignore extra null object,
|
||||
(bucket, next) => parseString(request.post, (err, result) => {
|
||||
// just for linting; there should not be any parsing error here
|
||||
|
|
|
@ -22,7 +22,7 @@ const requestType = 'bucketPutWebsite';
|
|||
*/
|
||||
function bucketPutWebsite(authInfo, request, log, callback) {
|
||||
log.debug('processing request', { method: 'bucketPutWebsite' });
|
||||
const bucketName = request.bucketName;
|
||||
const { bucketName } = request;
|
||||
const canonicalID = authInfo.getCanonicalID();
|
||||
|
||||
if (!request.post) {
|
||||
|
@ -49,7 +49,8 @@ function bucketPutWebsite(authInfo, request, log, callback) {
|
|||
});
|
||||
},
|
||||
function validateBucketAuthorization(bucket, config, next) {
|
||||
if (!isBucketAuthorized(bucket, requestType, canonicalID, authInfo, log, request)) {
|
||||
if (!isBucketAuthorized(bucket, request.apiMethods || requestType, canonicalID,
|
||||
authInfo, log, request, request.actionImplicitDenies)) {
|
||||
log.debug('access denied for user on bucket', {
|
||||
requestType,
|
||||
method: 'bucketPutWebsite',
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
const { waterfall } = require('async');
|
||||
const { errors } = require('arsenal');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const metadata = require('../metadata/wrapper');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
const { parseString } = require('xml2js');
|
||||
|
||||
function validateBucketQuotaProperty(requestBody, next) {
|
||||
const quota = requestBody.quota;
|
||||
const quotaValue = parseInt(quota, 10);
|
||||
if (Number.isNaN(quotaValue)) {
|
||||
return next(errors.InvalidArgument.customizeDescription('Quota Value should be a number'));
|
||||
}
|
||||
if (quotaValue <= 0) {
|
||||
return next(errors.InvalidArgument.customizeDescription('Quota value must be a positive number'));
|
||||
}
|
||||
return next(null, quotaValue);
|
||||
}
|
||||
|
||||
function parseRequestBody(requestBody, next) {
|
||||
try {
|
||||
const jsonData = JSON.parse(requestBody);
|
||||
if (typeof jsonData !== 'object') {
|
||||
throw new Error('Invalid JSON');
|
||||
}
|
||||
return next(null, jsonData);
|
||||
} catch (jsonError) {
|
||||
return parseString(requestBody, (xmlError, xmlData) => {
|
||||
if (xmlError) {
|
||||
return next(errors.InvalidArgument.customizeDescription('Request body must be a JSON object'));
|
||||
}
|
||||
return next(null, xmlData);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function bucketUpdateQuota(authInfo, request, log, callback) {
|
||||
log.debug('processing request', { method: 'bucketUpdateQuota' });
|
||||
|
||||
const { bucketName } = request;
|
||||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: request.apiMethods || 'bucketUpdateQuota',
|
||||
request,
|
||||
};
|
||||
let bucket = null;
|
||||
return waterfall([
|
||||
next => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, b) => {
|
||||
bucket = b;
|
||||
return next(err, bucket);
|
||||
}),
|
||||
(bucket, next) => parseRequestBody(request.post, (err, requestBody) => next(err, bucket, requestBody)),
|
||||
(bucket, requestBody, next) => validateBucketQuotaProperty(requestBody, (err, quotaValue) =>
|
||||
next(err, bucket, quotaValue)),
|
||||
(bucket, quotaValue, next) => {
|
||||
bucket.setQuota(quotaValue);
|
||||
return metadata.updateBucket(bucket.getName(), bucket, log, next);
|
||||
},
|
||||
], (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(request.headers.origin,
|
||||
request.method, bucket);
|
||||
if (err) {
|
||||
log.debug('error processing request', {
|
||||
error: err,
|
||||
method: 'bucketUpdateQuota'
|
||||
});
|
||||
monitoring.promMetrics('PUT', bucketName, err.code,
|
||||
'updateBucketQuota');
|
||||
return callback(err, err.code, corsHeaders);
|
||||
}
|
||||
monitoring.promMetrics(
|
||||
'PUT', bucketName, '200', 'updateBucketQuota');
|
||||
pushMetric('updateBucketQuota', log, {
|
||||
authInfo,
|
||||
bucket: bucketName,
|
||||
});
|
||||
return callback(null, corsHeaders);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = bucketUpdateQuota;
|
|
@ -12,7 +12,7 @@ const constants = require('../../constants');
|
|||
const { versioningPreprocessing, checkQueryVersionId, decodeVID, overwritingVersioning }
|
||||
= require('./apiUtils/object/versioning');
|
||||
const services = require('../services');
|
||||
const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const locationConstraintCheck
|
||||
= require('./apiUtils/object/locationConstraintCheck');
|
||||
const { skipMpuPartProcessing } = storage.data.external.backendUtils;
|
||||
|
@ -21,8 +21,6 @@ const { validateAndFilterMpuParts, generateMpuPartStorageInfo } =
|
|||
const locationKeysHaveChanged
|
||||
= require('./apiUtils/object/locationKeysHaveChanged');
|
||||
const { setExpirationHeaders } = require('./apiUtils/object/expirationHeaders');
|
||||
|
||||
const logger = require('../utilities/logger');
|
||||
const { validatePutVersionId } = require('./apiUtils/object/coldStorage');
|
||||
|
||||
const versionIdUtils = versioning.VersionID;
|
||||
|
@ -82,7 +80,7 @@ function completeMultipartUpload(authInfo, request, log, callback) {
|
|||
uploadId,
|
||||
// Note: permissions for completing a multipart upload are the
|
||||
// same as putting a part.
|
||||
requestType: 'putPart or complete',
|
||||
requestType: request.apiMethods || 'putPart or complete',
|
||||
log,
|
||||
request,
|
||||
};
|
||||
|
@ -133,10 +131,11 @@ function completeMultipartUpload(authInfo, request, log, callback) {
|
|||
bucketName,
|
||||
// Required permissions for this action
|
||||
// at the destinationBucket level are same as objectPut
|
||||
requestType: 'objectPut',
|
||||
requestType: request.apiMethods || 'completeMultipartUpload',
|
||||
versionId,
|
||||
request,
|
||||
};
|
||||
metadataValidateBucketAndObj(metadataValParams, log, next);
|
||||
standardMetadataValidateBucketAndObj(metadataValParams, request.actionImplicitDenies, log, next);
|
||||
},
|
||||
function validateMultipart(destBucket, objMD, next) {
|
||||
if (objMD) {
|
||||
|
@ -214,9 +213,14 @@ function completeMultipartUpload(authInfo, request, log, callback) {
|
|||
const mdInfo = { storedParts, mpuOverviewKey, splitter };
|
||||
const mpuInfo =
|
||||
{ objectKey, uploadId, jsonList, bucketName, destBucket };
|
||||
const originalIdentityImpDenies = request.actionImplicitDenies;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
delete request.actionImplicitDenies;
|
||||
return data.completeMPU(request, mpuInfo, mdInfo, location,
|
||||
null, null, null, locationConstraintCheck, log,
|
||||
(err, completeObjData) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
request.actionImplicitDenies = originalIdentityImpDenies;
|
||||
if (err) {
|
||||
return next(err, destBucket);
|
||||
}
|
||||
|
@ -325,6 +329,7 @@ function completeMultipartUpload(authInfo, request, log, callback) {
|
|||
replicationInfo: getReplicationInfo(objectKey, destBucket,
|
||||
false, calculatedSize, REPLICATION_ACTION),
|
||||
originOp: 's3:ObjectCreated:CompleteMultipartUpload',
|
||||
overheadField: constants.overheadField,
|
||||
log,
|
||||
};
|
||||
// If key already exists
|
||||
|
@ -394,8 +399,10 @@ function completeMultipartUpload(authInfo, request, log, callback) {
|
|||
metaStoreParams.versionId = options.versionId;
|
||||
metaStoreParams.versioning = options.versioning;
|
||||
metaStoreParams.isNull = options.isNull;
|
||||
metaStoreParams.nullVersionId = options.nullVersionId;
|
||||
metaStoreParams.nullUploadId = options.nullUploadId;
|
||||
metaStoreParams.deleteNullKey = options.deleteNullKey;
|
||||
if (options.extraMD) {
|
||||
Object.assign(metaStoreParams, options.extraMD);
|
||||
}
|
||||
/* eslint-enable no-param-reassign */
|
||||
|
||||
// For external backends (where completeObjData is not
|
||||
|
@ -467,12 +474,9 @@ function completeMultipartUpload(authInfo, request, log, callback) {
|
|||
const newDataStoreName =
|
||||
Array.isArray(dataLocations) && dataLocations[0] ?
|
||||
dataLocations[0].dataStoreName : null;
|
||||
const delLog =
|
||||
logger.newRequestLoggerFromSerializedUids(log
|
||||
.getSerializedUids());
|
||||
return data.batchDelete(dataToDelete,
|
||||
request.method,
|
||||
newDataStoreName, delLog, err => {
|
||||
newDataStoreName, log, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
@ -495,10 +499,8 @@ function completeMultipartUpload(authInfo, request, log, callback) {
|
|||
function batchDeleteExtraParts(extraPartLocations, destinationBucket,
|
||||
aggregateETag, generatedVersionId, next) {
|
||||
if (extraPartLocations && extraPartLocations.length > 0) {
|
||||
const delLog = logger.newRequestLoggerFromSerializedUids(
|
||||
log.getSerializedUids());
|
||||
return data.batchDelete(extraPartLocations, request.method,
|
||||
null, delLog, err => {
|
||||
null, log, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
|
|
@ -6,10 +6,11 @@ const convertToXml = s3middleware.convertToXml;
|
|||
const { pushMetric } = require('../utapi/utilities');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const { hasNonPrintables } = require('../utilities/stringChecks');
|
||||
const { config } = require('../Config');
|
||||
const { cleanUpBucket } = require('./apiUtils/bucket/bucketCreation');
|
||||
const constants = require('../../constants');
|
||||
const services = require('../services');
|
||||
const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const locationConstraintCheck
|
||||
= require('./apiUtils/object/locationConstraintCheck');
|
||||
const validateWebsiteHeader = require('./apiUtils/object/websiteServing')
|
||||
|
@ -65,7 +66,7 @@ function initiateMultipartUpload(authInfo, request, log, callback) {
|
|||
const websiteRedirectHeader =
|
||||
request.headers['x-amz-website-redirect-location'];
|
||||
if (request.headers['x-amz-storage-class'] &&
|
||||
!constants.validStorageClasses.includes(request.headers['x-amz-storage-class'])) {
|
||||
!config.locationConstraints[request.headers['x-amz-storage-class']]) {
|
||||
log.trace('invalid storage-class header');
|
||||
monitoring.promMetrics('PUT', bucketName,
|
||||
errors.InvalidStorageClass.code, 'initiateMultipartUpload');
|
||||
|
@ -81,7 +82,7 @@ function initiateMultipartUpload(authInfo, request, log, callback) {
|
|||
if (metaHeaders instanceof Error) {
|
||||
log.debug('user metadata validation failed', {
|
||||
error: metaHeaders,
|
||||
method: 'createAndStoreObject',
|
||||
method: 'initiateMultipartUpload',
|
||||
});
|
||||
return process.nextTick(() => callback(metaHeaders));
|
||||
}
|
||||
|
@ -105,7 +106,7 @@ function initiateMultipartUpload(authInfo, request, log, callback) {
|
|||
authInfo,
|
||||
bucketName,
|
||||
// Required permissions for this action are same as objectPut
|
||||
requestType: 'objectPut',
|
||||
requestType: request.apiMethods || 'initiateMultipartUpload',
|
||||
request,
|
||||
};
|
||||
const accountCanonicalID = authInfo.getCanonicalID();
|
||||
|
@ -274,7 +275,7 @@ function initiateMultipartUpload(authInfo, request, log, callback) {
|
|||
}
|
||||
|
||||
async.waterfall([
|
||||
next => metadataValidateBucketAndObj(metadataValParams, log,
|
||||
next => standardMetadataValidateBucketAndObj(metadataValParams, request.actionImplicitDenies, log,
|
||||
(error, destinationBucket) => {
|
||||
const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, destinationBucket);
|
||||
if (error) {
|
||||
|
|
|
@ -6,7 +6,7 @@ const convertToXml = s3middleware.convertToXml;
|
|||
const constants = require('../../constants');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const services = require('../services');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
|
||||
|
@ -96,8 +96,8 @@ function listMultipartUploads(authInfo, request, log, callback) {
|
|||
// to list the multipart uploads so we have provided here that
|
||||
// the authorization to list multipart uploads is the same
|
||||
// as listing objects in a bucket.
|
||||
requestType: 'bucketGet',
|
||||
preciseRequestType: 'listMultipartUploads',
|
||||
requestType: request.apiMethods || 'bucketGet',
|
||||
preciseRequestType: request.apiMethods || 'listMultipartUploads',
|
||||
request,
|
||||
};
|
||||
|
||||
|
@ -105,7 +105,7 @@ function listMultipartUploads(authInfo, request, log, callback) {
|
|||
function waterfall1(next) {
|
||||
// Check final destination bucket for authorization rather
|
||||
// than multipart upload bucket
|
||||
metadataValidateBucket(metadataValParams, log,
|
||||
standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, bucket) => next(err, bucket));
|
||||
},
|
||||
function getMPUBucket(bucket, next) {
|
||||
|
|
|
@ -8,7 +8,7 @@ const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
|||
const locationConstraintCheck =
|
||||
require('./apiUtils/object/locationConstraintCheck');
|
||||
const services = require('../services');
|
||||
const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const escapeForXml = s3middleware.escapeForXml;
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
|
@ -97,7 +97,7 @@ function listParts(authInfo, request, log, callback) {
|
|||
bucketName,
|
||||
objectKey,
|
||||
uploadId,
|
||||
preciseRequestType: 'listParts',
|
||||
preciseRequestType: request.apiMethods || 'listParts',
|
||||
request,
|
||||
};
|
||||
// For validating the request at the destinationBucket level
|
||||
|
@ -114,7 +114,7 @@ function listParts(authInfo, request, log, callback) {
|
|||
|
||||
async.waterfall([
|
||||
function checkDestBucketVal(next) {
|
||||
metadataValidateBucketAndObj(metadataValParams, log,
|
||||
standardMetadataValidateBucketAndObj(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, destinationBucket) => {
|
||||
if (err) {
|
||||
return next(err, destinationBucket, null);
|
||||
|
@ -152,8 +152,13 @@ function listParts(authInfo, request, log, callback) {
|
|||
mpuOverviewObj,
|
||||
destBucket,
|
||||
};
|
||||
const originalIdentityImpDenies = request.actionImplicitDenies;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
delete request.actionImplicitDenies;
|
||||
return data.listParts(mpuInfo, request, locationConstraintCheck,
|
||||
log, (err, backendPartList) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
request.actionImplicitDenies = originalIdentityImpDenies;
|
||||
if (err) {
|
||||
return next(err, destBucket);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const { errors, versioning } = require('arsenal');
|
||||
const constants = require('../../constants');
|
||||
const services = require('../services');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const validateSearchParams = require('../api/apiUtils/bucket/validateSearch');
|
||||
|
@ -71,7 +71,7 @@ function metadataSearch(authInfo, request, log, callback) {
|
|||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'metadataSearch',
|
||||
requestType: request.apiMethods || 'metadataSearch',
|
||||
request,
|
||||
};
|
||||
const listParams = {
|
||||
|
@ -103,7 +103,7 @@ function metadataSearch(authInfo, request, log, callback) {
|
|||
listParams.marker = params.marker;
|
||||
}
|
||||
|
||||
metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(request.headers.origin,
|
||||
request.method, bucket);
|
||||
if (err) {
|
||||
|
|
|
@ -11,13 +11,13 @@ const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
|||
const metadata = require('../metadata/wrapper');
|
||||
const services = require('../services');
|
||||
const vault = require('../auth/vault');
|
||||
const { isBucketAuthorized } =
|
||||
const { isBucketAuthorized, evaluateBucketPolicyWithIAM } =
|
||||
require('./apiUtils/authorization/permissionChecks');
|
||||
const { preprocessingVersioningDelete }
|
||||
= require('./apiUtils/object/versioning');
|
||||
const createAndStoreObject = require('./apiUtils/object/createAndStoreObject');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
const { metadataGetObject, addIsNonversionedBucket } = require('../metadata/metadataUtils');
|
||||
const metadataUtils = require('../metadata/metadataUtils');
|
||||
const { config } = require('../Config');
|
||||
const { isRequesterNonAccountUser } = require('./apiUtils/authorization/permissionChecks');
|
||||
const { hasGovernanceBypassHeader, checkUserGovernanceBypass, ObjectLockInfo }
|
||||
|
@ -25,9 +25,13 @@ const { hasGovernanceBypassHeader, checkUserGovernanceBypass, ObjectLockInfo }
|
|||
const requestUtils = policies.requestUtils;
|
||||
const { validObjectKeys } = require('../routes/routeVeeam');
|
||||
const { deleteVeeamCapabilities } = require('../routes/veeam/delete');
|
||||
const { _bucketRequiresOplogUpdate } = require('./apiUtils/object/deleteObject');
|
||||
const { overheadField } = require('../../constants');
|
||||
|
||||
const versionIdUtils = versioning.VersionID;
|
||||
|
||||
const { data } = require('../data/wrapper');
|
||||
const logger = require('../utilities/logger');
|
||||
const { validateQuotas } = require('./apiUtils/quotas/quotaUtils');
|
||||
|
||||
/*
|
||||
Format of xml request:
|
||||
|
@ -169,6 +173,63 @@ function _parseXml(xmlToParse, next) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* decodeObjectVersion - decode object version to be deleted
|
||||
* @param {object} entry - entry from data model
|
||||
* @param {function} next - callback to call with error or decoded version
|
||||
* @return {undefined}
|
||||
**/
|
||||
function decodeObjectVersion(entry) {
|
||||
let decodedVersionId;
|
||||
if (entry.versionId) {
|
||||
decodedVersionId = entry.versionId === 'null' ?
|
||||
'null' : versionIdUtils.decode(entry.versionId);
|
||||
}
|
||||
if (decodedVersionId instanceof Error) {
|
||||
return [errors.NoSuchVersion];
|
||||
}
|
||||
return [null, decodedVersionId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization function for the MultiObjectDelete API that will, based on the
|
||||
* current metadata backend, assess if metadata READ batching is supported. If
|
||||
* yes, the initialization step will call the metadataGetObjects function from
|
||||
* the MetadataWrapper.
|
||||
* @param {string} bucketName - bucket name
|
||||
* @param {string []} inPlay - list of object keys still in play
|
||||
* @param {object} log - logger object
|
||||
* @param {function} callback - callback to call with error or list of objects
|
||||
* @return {undefined}
|
||||
*/
|
||||
function initializeMultiObjectDeleteWithBatchingSupport(bucketName, inPlay, log, callback) {
|
||||
if (config.multiObjectDeleteEnableOptimizations === false) {
|
||||
return callback(null, {});
|
||||
}
|
||||
// If the backend supports batching, we want to optimize the API latency by
|
||||
// first getting all the objects metadata, stored in memory, for later use
|
||||
// in the API. This approach does not change the API architecture, but
|
||||
// transplants an additional piece of code that can greatly improve the API
|
||||
// latency when the database supports batching.
|
||||
const objectKeys = Object.values(inPlay).map(entry => {
|
||||
const [err, versionId] = decodeObjectVersion(entry, bucketName);
|
||||
if (err) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
versionId,
|
||||
inPlay: entry,
|
||||
};
|
||||
});
|
||||
return metadataUtils.metadataGetObjects(bucketName, objectKeys, log, (err, cache) => {
|
||||
// This optional step is read-only, so any error can be safely ignored
|
||||
if (err) {
|
||||
return callback(null, {});
|
||||
}
|
||||
return callback(null, cache);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* gets object metadata and deletes object
|
||||
* @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info
|
||||
|
@ -194,43 +255,26 @@ function getObjMetadataAndDelete(authInfo, canonicalID, request,
|
|||
let numOfObjectsRemoved = 0;
|
||||
const skipError = new Error('skip');
|
||||
const objectLockedError = new Error('object locked');
|
||||
let deleteFromStorage = [];
|
||||
|
||||
// doing 5 requests at a time. note that the data wrapper
|
||||
// will do 5 parallel requests to data backend to delete parts
|
||||
return async.forEachLimit(inPlay, 5, (entry, moveOn) => {
|
||||
async.waterfall([
|
||||
callback => {
|
||||
let decodedVersionId;
|
||||
if (entry.versionId) {
|
||||
decodedVersionId = entry.versionId === 'null' ?
|
||||
'null' : versionIdUtils.decode(entry.versionId);
|
||||
}
|
||||
if (decodedVersionId instanceof Error) {
|
||||
monitoring.promMetrics('DELETE', bucketName, 404,
|
||||
'multiObjectDelete');
|
||||
return callback(errors.NoSuchVersion);
|
||||
}
|
||||
const getOptions = addIsNonversionedBucket({}, bucket);
|
||||
return callback(null, decodedVersionId, getOptions);
|
||||
},
|
||||
// for obj deletes, no need to check acl's at object level
|
||||
// (authority is at the bucket level for obj deletes)
|
||||
(versionId, getOptions, callback) => metadataGetObject(bucketName, entry.key,
|
||||
versionId, getOptions, log, (err, objMD) => {
|
||||
// if general error from metadata return error
|
||||
if (err && !err.is.NoSuchKey) {
|
||||
monitoring.promMetrics('DELETE', bucketName, err.code,
|
||||
'multiObjectDelete');
|
||||
return callback(err);
|
||||
}
|
||||
if (err?.is.NoSuchKey) {
|
||||
return async.waterfall([
|
||||
callback => initializeMultiObjectDeleteWithBatchingSupport(bucketName, inPlay, log, callback),
|
||||
(cache, callback) => async.forEachLimit(inPlay, config.multiObjectDeleteConcurrency, (entry, moveOn) => {
|
||||
async.waterfall([
|
||||
callback => callback(...decodeObjectVersion(entry, bucketName)),
|
||||
// for obj deletes, no need to check acl's at object level
|
||||
// (authority is at the bucket level for obj deletes)
|
||||
(versionId, callback) => metadataUtils.metadataGetObject(bucketName, entry.key,
|
||||
versionId, cache, log, (err, objMD) => callback(err, objMD, versionId)),
|
||||
(objMD, versionId, callback) => {
|
||||
if (!objMD) {
|
||||
const verCfg = bucket.getVersioningConfiguration();
|
||||
// To adhere to AWS behavior, create a delete marker
|
||||
// if trying to delete an object that does not exist
|
||||
// when versioning has been configured
|
||||
if (verCfg && !entry.versionId) {
|
||||
log.debug('trying to delete specific version ' +
|
||||
' that does not exist');
|
||||
'that does not exist');
|
||||
return callback(null, objMD, versionId);
|
||||
}
|
||||
// otherwise if particular key does not exist, AWS
|
||||
|
@ -246,111 +290,165 @@ function getObjMetadataAndDelete(authInfo, canonicalID, request,
|
|||
objMD.location[0].deleteVersion = true;
|
||||
}
|
||||
return callback(null, objMD, versionId);
|
||||
}),
|
||||
(objMD, versionId, callback) => {
|
||||
// AWS only returns an object lock error if a version id
|
||||
// is specified, else continue to create a delete marker
|
||||
if (!versionId || !bucket.isObjectLockEnabled()) {
|
||||
return callback(null, null, objMD, versionId);
|
||||
}
|
||||
const hasGovernanceBypass = hasGovernanceBypassHeader(request.headers);
|
||||
if (hasGovernanceBypass && isRequesterNonAccountUser(authInfo)) {
|
||||
return checkUserGovernanceBypass(request, authInfo, bucket, entry.key, log, error => {
|
||||
if (error && error.is.AccessDenied) {
|
||||
log.debug('user does not have BypassGovernanceRetention and object is locked', { error });
|
||||
return callback(objectLockedError);
|
||||
}
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
return callback(null, hasGovernanceBypass, objMD, versionId);
|
||||
},
|
||||
(objMD, versionId, callback) => {
|
||||
// AWS only returns an object lock error if a version id
|
||||
// is specified, else continue to create a delete marker
|
||||
if (!versionId || !bucket.isObjectLockEnabled()) {
|
||||
return callback(null, null, objMD, versionId);
|
||||
}
|
||||
const hasGovernanceBypass = hasGovernanceBypassHeader(request.headers);
|
||||
if (hasGovernanceBypass && isRequesterNonAccountUser(authInfo)) {
|
||||
return checkUserGovernanceBypass(request, authInfo, bucket, entry.key, log, error => {
|
||||
if (error && error.is.AccessDenied) {
|
||||
log.debug('user does not have BypassGovernanceRetention and object is locked',
|
||||
{ error });
|
||||
return callback(objectLockedError);
|
||||
}
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
return callback(null, hasGovernanceBypass, objMD, versionId);
|
||||
});
|
||||
}
|
||||
return callback(null, hasGovernanceBypass, objMD, versionId);
|
||||
},
|
||||
(hasGovernanceBypass, objMD, versionId, callback) => {
|
||||
// AWS only returns an object lock error if a version id
|
||||
// is specified, else continue to create a delete marker
|
||||
if (!versionId || !bucket.isObjectLockEnabled()) {
|
||||
return callback(null, objMD, versionId);
|
||||
}
|
||||
const objLockInfo = new ObjectLockInfo({
|
||||
mode: objMD.retentionMode,
|
||||
date: objMD.retentionDate,
|
||||
legalHold: objMD.legalHold || false,
|
||||
});
|
||||
}
|
||||
return callback(null, hasGovernanceBypass, objMD, versionId);
|
||||
},
|
||||
(hasGovernanceBypass, objMD, versionId, callback) => {
|
||||
// AWS only returns an object lock error if a version id
|
||||
// is specified, else continue to create a delete marker
|
||||
if (!versionId || !bucket.isObjectLockEnabled()) {
|
||||
|
||||
// If the object can not be deleted raise an error
|
||||
if (!objLockInfo.canModifyObject(hasGovernanceBypass)) {
|
||||
log.debug('trying to delete locked object');
|
||||
return callback(objectLockedError);
|
||||
}
|
||||
|
||||
return callback(null, objMD, versionId);
|
||||
},
|
||||
(objMD, versionId, callback) => validateQuotas(
|
||||
request, bucket, request.accountQuotas, ['objectDelete'], 'objectDelete',
|
||||
-objMD?.['content-length'] || 0, false, log, err => callback(err, objMD, versionId)),
|
||||
(objMD, versionId, callback) => {
|
||||
const options = preprocessingVersioningDelete(
|
||||
bucketName, bucket, objMD, versionId, config.nullVersionCompatMode);
|
||||
const deleteInfo = {};
|
||||
if (options && options.deleteData) {
|
||||
options.overheadField = overheadField;
|
||||
deleteInfo.deleted = true;
|
||||
if (!_bucketRequiresOplogUpdate(bucket)) {
|
||||
options.doesNotNeedOpogUpdate = true;
|
||||
}
|
||||
if (objMD.uploadId) {
|
||||
// eslint-disable-next-line
|
||||
options.replayId = objMD.uploadId;
|
||||
}
|
||||
return services.deleteObject(bucketName, objMD,
|
||||
entry.key, options, config.multiObjectDeleteEnableOptimizations, log,
|
||||
's3:ObjectRemoved:Delete', (err, toDelete) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (toDelete) {
|
||||
deleteFromStorage = deleteFromStorage.concat(toDelete);
|
||||
}
|
||||
return callback(null, objMD, deleteInfo);
|
||||
});
|
||||
}
|
||||
deleteInfo.newDeleteMarker = true;
|
||||
// This call will create a delete-marker
|
||||
return createAndStoreObject(bucketName, bucket, entry.key,
|
||||
objMD, authInfo, canonicalID, null, request,
|
||||
deleteInfo.newDeleteMarker, null, overheadField, log,
|
||||
's3:ObjectRemoved:DeleteMarkerCreated', (err, result) =>
|
||||
callback(err, objMD, deleteInfo, result.versionId));
|
||||
},
|
||||
], (err, objMD, deleteInfo, versionId) => {
|
||||
if (err === skipError) {
|
||||
return moveOn();
|
||||
} else if (err === objectLockedError) {
|
||||
errorResults.push({ entry, error: errors.AccessDenied, objectLocked: true });
|
||||
return moveOn();
|
||||
} else if (err) {
|
||||
log.error('error deleting object', { error: err, entry });
|
||||
errorResults.push({ entry, error: err });
|
||||
return moveOn();
|
||||
}
|
||||
const objLockInfo = new ObjectLockInfo({
|
||||
mode: objMD.retentionMode,
|
||||
date: objMD.retentionDate,
|
||||
legalHold: objMD.legalHold || false,
|
||||
if (deleteInfo.deleted && objMD['content-length']) {
|
||||
numOfObjectsRemoved++;
|
||||
totalContentLengthDeleted += objMD['content-length'];
|
||||
}
|
||||
let isDeleteMarker;
|
||||
let deleteMarkerVersionId;
|
||||
// - If trying to delete an object that does not exist (if a new
|
||||
// delete marker was created)
|
||||
// - Or if an object exists but no version was specified
|
||||
// return DeleteMarkerVersionId equals the versionID of the marker
|
||||
// you just generated and DeleteMarker tag equals true
|
||||
if (deleteInfo.newDeleteMarker) {
|
||||
isDeleteMarker = true;
|
||||
deleteMarkerVersionId = versionIdUtils.encode(versionId);
|
||||
// In this case we are putting a new object (i.e., the delete
|
||||
// marker), so we decrement the numOfObjectsRemoved value.
|
||||
numOfObjectsRemoved--;
|
||||
// If trying to delete a delete marker, DeleteMarkerVersionId equals
|
||||
// deleteMarker's versionID and DeleteMarker equals true
|
||||
} else if (objMD && objMD.isDeleteMarker) {
|
||||
isDeleteMarker = true;
|
||||
deleteMarkerVersionId = entry.versionId;
|
||||
}
|
||||
successfullyDeleted.push({
|
||||
entry, isDeleteMarker,
|
||||
deleteMarkerVersionId,
|
||||
});
|
||||
return moveOn();
|
||||
});
|
||||
},
|
||||
// end of forEach func
|
||||
err => {
|
||||
// Batch delete all objects
|
||||
const onDone = () => callback(err, quietSetting, errorResults, numOfObjectsRemoved,
|
||||
successfullyDeleted, totalContentLengthDeleted, bucket);
|
||||
|
||||
// If the object can not be deleted raise an error
|
||||
if (!objLockInfo.canModifyObject(hasGovernanceBypass)) {
|
||||
log.debug('trying to delete locked object');
|
||||
return callback(objectLockedError);
|
||||
if (err && deleteFromStorage.length === 0) {
|
||||
log.trace('no objects to delete from data backend');
|
||||
return onDone();
|
||||
}
|
||||
// If error but we have objects in the list, delete them to ensure
|
||||
// consistent state.
|
||||
log.trace('deleting objects from data backend');
|
||||
|
||||
// Split the array into chunks
|
||||
const chunks = [];
|
||||
while (deleteFromStorage.length > 0) {
|
||||
chunks.push(deleteFromStorage.splice(0, config.multiObjectDeleteConcurrency));
|
||||
}
|
||||
|
||||
return callback(null, objMD, versionId);
|
||||
},
|
||||
(objMD, versionId, callback) =>
|
||||
preprocessingVersioningDelete(bucketName, bucket, objMD,
|
||||
versionId, log, (err, options) => callback(err, options,
|
||||
objMD)),
|
||||
(options, objMD, callback) => {
|
||||
const deleteInfo = {};
|
||||
if (options && options.deleteData) {
|
||||
deleteInfo.deleted = true;
|
||||
return services.deleteObject(bucketName, objMD,
|
||||
entry.key, options, log, err =>
|
||||
callback(err, objMD, deleteInfo));
|
||||
}
|
||||
deleteInfo.newDeleteMarker = true;
|
||||
// This call will create a delete-marker
|
||||
return createAndStoreObject(bucketName, bucket, entry.key,
|
||||
objMD, authInfo, canonicalID, null, request,
|
||||
deleteInfo.newDeleteMarker, null, log, (err, result) =>
|
||||
callback(err, objMD, deleteInfo, result.versionId));
|
||||
},
|
||||
], (err, objMD, deleteInfo, versionId) => {
|
||||
if (err === skipError) {
|
||||
return moveOn();
|
||||
} else if (err === objectLockedError) {
|
||||
errorResults.push({ entry, error: errors.AccessDenied, objectLocked: true });
|
||||
return moveOn();
|
||||
} else if (err) {
|
||||
log.error('error deleting object', { error: err, entry });
|
||||
errorResults.push({ entry, error: err });
|
||||
return moveOn();
|
||||
}
|
||||
if (deleteInfo.deleted && objMD['content-length']) {
|
||||
numOfObjectsRemoved++;
|
||||
totalContentLengthDeleted += objMD['content-length'];
|
||||
}
|
||||
let isDeleteMarker;
|
||||
let deleteMarkerVersionId;
|
||||
// - If trying to delete an object that does not exist (if a new
|
||||
// delete marker was created)
|
||||
// - Or if an object exists but no version was specified
|
||||
// return DeleteMarkerVersionId equals the versionID of the marker
|
||||
// you just generated and DeleteMarker tag equals true
|
||||
if (deleteInfo.newDeleteMarker) {
|
||||
isDeleteMarker = true;
|
||||
deleteMarkerVersionId = versionIdUtils.encode(versionId);
|
||||
// In this case we are putting a new object (i.e., the delete
|
||||
// marker), so we decrement the numOfObjectsRemoved value.
|
||||
numOfObjectsRemoved--;
|
||||
// If trying to delete a delete marker, DeleteMarkerVersionId equals
|
||||
// deleteMarker's versionID and DeleteMarker equals true
|
||||
} else if (objMD && objMD.isDeleteMarker) {
|
||||
isDeleteMarker = true;
|
||||
deleteMarkerVersionId = entry.versionId;
|
||||
}
|
||||
successfullyDeleted.push({ entry, isDeleteMarker,
|
||||
deleteMarkerVersionId });
|
||||
return moveOn();
|
||||
});
|
||||
},
|
||||
// end of forEach func
|
||||
err => {
|
||||
log.trace('finished deleting objects', { numOfObjectsRemoved });
|
||||
return next(err, quietSetting, errorResults, numOfObjectsRemoved,
|
||||
successfullyDeleted, totalContentLengthDeleted, bucket);
|
||||
return async.each(chunks, (chunk, done) => data.batchDelete(chunk, null, null,
|
||||
logger.newRequestLoggerFromSerializedUids(log.getSerializedUids()), done),
|
||||
err => {
|
||||
if (err) {
|
||||
log.error('error deleting objects from data backend', { error: err });
|
||||
return onDone(err);
|
||||
}
|
||||
return onDone();
|
||||
});
|
||||
}),
|
||||
], (err, ...results) => {
|
||||
// if general error from metadata return error
|
||||
if (err) {
|
||||
monitoring.promMetrics('DELETE', bucketName, err.code,
|
||||
'multiObjectDelete');
|
||||
return next(err);
|
||||
}
|
||||
return next(null, ...results);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -398,15 +496,47 @@ function multiObjectDelete(authInfo, request, log, callback) {
|
|||
return next(null, quietSetting, objects);
|
||||
});
|
||||
},
|
||||
function checkPolicies(quietSetting, objects, next) {
|
||||
function checkBucketMetadata(quietSetting, objects, next) {
|
||||
const errorResults = [];
|
||||
return metadata.getBucket(bucketName, log, (err, bucketMD) => {
|
||||
if (err) {
|
||||
log.trace('error retrieving bucket metadata',
|
||||
{ error: err });
|
||||
return next(err);
|
||||
}
|
||||
// check whether bucket has transient or deleted flag
|
||||
if (bucketShield(bucketMD, 'objectDelete')) {
|
||||
return next(errors.NoSuchBucket);
|
||||
}
|
||||
// The implicit deny flag is ignored in the DeleteObjects API, as authorization only
|
||||
// affects the objects.
|
||||
if (!isBucketAuthorized(bucketMD, 'objectDelete', canonicalID, authInfo, log, request)) {
|
||||
log.trace("access denied due to bucket acl's");
|
||||
// if access denied at the bucket level, no access for
|
||||
// any of the objects so all results will be error results
|
||||
objects.forEach(entry => {
|
||||
errorResults.push({
|
||||
entry,
|
||||
error: errors.AccessDenied,
|
||||
});
|
||||
});
|
||||
// by sending an empty array as the objects array
|
||||
// async.forEachLimit below will not actually
|
||||
// make any calls to metadata or data but will continue on
|
||||
// to the next step to build xml
|
||||
return next(null, quietSetting, errorResults, [], bucketMD);
|
||||
}
|
||||
return next(null, quietSetting, errorResults, objects, bucketMD);
|
||||
});
|
||||
},
|
||||
function checkPolicies(quietSetting, errorResults, objects, bucketMD, next) {
|
||||
// track keys that are still on track to be deleted
|
||||
const inPlay = [];
|
||||
const errorResults = [];
|
||||
// if request from account, no need to check policies
|
||||
// all objects are inPlay so send array of object keys
|
||||
// as inPlay argument
|
||||
if (!isRequesterNonAccountUser(authInfo)) {
|
||||
return next(null, quietSetting, errorResults, objects);
|
||||
return next(null, quietSetting, errorResults, objects, bucketMD);
|
||||
}
|
||||
|
||||
// TODO: once arsenal's extractParams is separated from doAuth
|
||||
|
@ -450,7 +580,7 @@ function multiObjectDelete(authInfo, request, log, callback) {
|
|||
error: errors.AccessDenied });
|
||||
});
|
||||
// send empty array for inPlay
|
||||
return next(null, quietSetting, errorResults, []);
|
||||
return next(null, quietSetting, errorResults, [], bucketMD);
|
||||
}
|
||||
if (err) {
|
||||
log.trace('error checking policies', {
|
||||
|
@ -468,6 +598,13 @@ function multiObjectDelete(authInfo, request, log, callback) {
|
|||
});
|
||||
return next(errors.InternalError);
|
||||
}
|
||||
// Convert authorization results into an easier to handle format
|
||||
const actionImplicitDenies = authorizationResults.reduce((acc, curr, idx) => {
|
||||
const apiMethod = authorizationResults[idx].action;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
acc[apiMethod] = curr.isImplicit;
|
||||
return acc;
|
||||
}, {});
|
||||
for (let i = 0; i < authorizationResults.length; i++) {
|
||||
const result = authorizationResults[i];
|
||||
// result is { isAllowed: true,
|
||||
|
@ -483,7 +620,26 @@ function multiObjectDelete(authInfo, request, log, callback) {
|
|||
key: result.arn.slice(slashIndex + 1),
|
||||
versionId: result.versionId,
|
||||
};
|
||||
if (result.isAllowed) {
|
||||
// Deny immediately if there is an explicit deny
|
||||
if (!result.isImplicit && !result.isAllowed) {
|
||||
errorResults.push({
|
||||
entry,
|
||||
error: errors.AccessDenied,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Evaluate against the bucket policies
|
||||
const areAllActionsAllowed = evaluateBucketPolicyWithIAM(
|
||||
bucketMD,
|
||||
Object.keys(actionImplicitDenies),
|
||||
canonicalID,
|
||||
authInfo,
|
||||
actionImplicitDenies,
|
||||
log,
|
||||
request);
|
||||
|
||||
if (areAllActionsAllowed) {
|
||||
if (validObjectKeys.includes(entry.key)) {
|
||||
inPlayInternal.push(entry.key);
|
||||
} else {
|
||||
|
@ -496,50 +652,9 @@ function multiObjectDelete(authInfo, request, log, callback) {
|
|||
});
|
||||
}
|
||||
}
|
||||
return next(null, quietSetting, errorResults, inPlay);
|
||||
return next(null, quietSetting, errorResults, inPlay, bucketMD);
|
||||
});
|
||||
},
|
||||
function checkBucketMetadata(quietSetting, errorResults, inPlay, next) {
|
||||
// if no objects in play, no need to check ACLs / get metadata,
|
||||
// just move on if there is no Origin header
|
||||
if (inPlay.length === 0 && !request.headers.origin) {
|
||||
return next(null, quietSetting, errorResults, inPlay,
|
||||
undefined);
|
||||
}
|
||||
return metadata.getBucket(bucketName, log, (err, bucketMD) => {
|
||||
if (err) {
|
||||
log.trace('error retrieving bucket metadata',
|
||||
{ error: err });
|
||||
return next(err);
|
||||
}
|
||||
// check whether bucket has transient or deleted flag
|
||||
if (bucketShield(bucketMD, 'objectDelete')) {
|
||||
return next(errors.NoSuchBucket);
|
||||
}
|
||||
// if no objects in play, no need to check ACLs
|
||||
if (inPlay.length === 0) {
|
||||
return next(null, quietSetting, errorResults, inPlay,
|
||||
bucketMD);
|
||||
}
|
||||
if (!isBucketAuthorized(bucketMD, 'objectDelete', canonicalID, authInfo, log, request)) {
|
||||
log.trace("access denied due to bucket acl's");
|
||||
// if access denied at the bucket level, no access for
|
||||
// any of the objects so all results will be error results
|
||||
inPlay.forEach(entry => {
|
||||
errorResults.push({
|
||||
entry,
|
||||
error: errors.AccessDenied,
|
||||
});
|
||||
});
|
||||
// by sending an empty array as the inPlay array
|
||||
// async.forEachLimit below will not actually
|
||||
// make any calls to metadata or data but will continue on
|
||||
// to the next step to build xml
|
||||
return next(null, quietSetting, errorResults, [], bucketMD);
|
||||
}
|
||||
return next(null, quietSetting, errorResults, inPlay, bucketMD);
|
||||
});
|
||||
},
|
||||
function handleInternalFiles(quietSetting, errorResults, inPlay, bucketMD, next) {
|
||||
return async.each(inPlayInternal,
|
||||
(localInPlay, next) => deleteVeeamCapabilities(bucketName, localInPlay, bucketMD, log, next),
|
||||
|
@ -587,4 +702,6 @@ function multiObjectDelete(authInfo, request, log, callback) {
|
|||
module.exports = {
|
||||
getObjMetadataAndDelete,
|
||||
multiObjectDelete,
|
||||
decodeObjectVersion,
|
||||
initializeMultiObjectDeleteWithBatchingSupport,
|
||||
};
|
||||
|
|
|
@ -12,11 +12,10 @@ const { checkQueryVersionId, versioningPreprocessing }
|
|||
= require('./apiUtils/object/versioning');
|
||||
const getReplicationInfo = require('./apiUtils/object/getReplicationInfo');
|
||||
const { data } = require('../data/wrapper');
|
||||
const logger = require('../utilities/logger');
|
||||
const services = require('../services');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const removeAWSChunked = require('./apiUtils/object/removeAWSChunked');
|
||||
const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const validateWebsiteHeader = require('./apiUtils/object/websiteServing')
|
||||
.validateWebsiteHeader;
|
||||
const { config } = require('../Config');
|
||||
|
@ -24,6 +23,7 @@ const monitoring = require('../utilities/monitoringHandler');
|
|||
const applyZenkoUserMD = require('./apiUtils/object/applyZenkoUserMD');
|
||||
const { getObjectSSEConfiguration } = require('./apiUtils/bucket/bucketEncryption');
|
||||
const { setExpirationHeaders } = require('./apiUtils/object/expirationHeaders');
|
||||
const { verifyColdObjectAvailable } = require('./apiUtils/object/coldStorage');
|
||||
|
||||
const versionIdUtils = versioning.VersionID;
|
||||
const locationHeader = constants.objectLocationConstraintHeader;
|
||||
|
@ -218,7 +218,16 @@ function objectCopy(authInfo, request, sourceBucket,
|
|||
bucketName: sourceBucket,
|
||||
objectKey: sourceObject,
|
||||
versionId: sourceVersionId,
|
||||
getDeleteMarker: true,
|
||||
requestType: 'objectGet',
|
||||
/**
|
||||
* Authorization will first check the target object, with an objectPut
|
||||
* action. But in this context, the source object metadata is still
|
||||
* unknown. In the context of quotas, to know the number of bytes that
|
||||
* are being written, we explicitly enable the quota evaluation logic
|
||||
* during the objectGet action instead.
|
||||
*/
|
||||
checkQuota: true,
|
||||
request,
|
||||
};
|
||||
const valPutParams = {
|
||||
|
@ -226,6 +235,7 @@ function objectCopy(authInfo, request, sourceBucket,
|
|||
bucketName: destBucketName,
|
||||
objectKey: destObjectKey,
|
||||
requestType: 'objectPut',
|
||||
checkQuota: false,
|
||||
request,
|
||||
};
|
||||
const dataStoreContext = {
|
||||
|
@ -239,7 +249,7 @@ function objectCopy(authInfo, request, sourceBucket,
|
|||
const responseHeaders = {};
|
||||
|
||||
if (request.headers['x-amz-storage-class'] &&
|
||||
!constants.validStorageClasses.includes(request.headers['x-amz-storage-class'])) {
|
||||
!config.locationConstraints[request.headers['x-amz-storage-class']]) {
|
||||
log.trace('invalid storage-class header');
|
||||
monitoring.promMetrics('PUT', destBucketName,
|
||||
errors.InvalidStorageClass.code, 'copyObject');
|
||||
|
@ -259,7 +269,7 @@ function objectCopy(authInfo, request, sourceBucket,
|
|||
}
|
||||
return async.waterfall([
|
||||
function checkDestAuth(next) {
|
||||
return metadataValidateBucketAndObj(valPutParams, log,
|
||||
return standardMetadataValidateBucketAndObj(valPutParams, request.actionImplicitDenies, log,
|
||||
(err, destBucketMD, destObjMD) => {
|
||||
if (err) {
|
||||
log.debug('error validating put part of request',
|
||||
|
@ -277,7 +287,10 @@ function objectCopy(authInfo, request, sourceBucket,
|
|||
});
|
||||
},
|
||||
function checkSourceAuthorization(destBucketMD, destObjMD, next) {
|
||||
return metadataValidateBucketAndObj(valGetParams, log,
|
||||
return standardMetadataValidateBucketAndObj({
|
||||
...valGetParams,
|
||||
destObjMD,
|
||||
}, request.actionImplicitDenies, log,
|
||||
(err, sourceBucketMD, sourceObjMD) => {
|
||||
if (err) {
|
||||
log.debug('error validating get part of request',
|
||||
|
@ -290,6 +303,11 @@ function objectCopy(authInfo, request, sourceBucket,
|
|||
log.debug('no source object', { sourceObject });
|
||||
return next(err, null, destBucketMD);
|
||||
}
|
||||
// check if object data is in a cold storage
|
||||
const coldErr = verifyColdObjectAvailable(sourceObjMD);
|
||||
if (coldErr) {
|
||||
return next(coldErr, null);
|
||||
}
|
||||
if (sourceObjMD.isDeleteMarker) {
|
||||
log.debug('delete marker on source object',
|
||||
{ sourceObject });
|
||||
|
@ -324,6 +342,10 @@ function objectCopy(authInfo, request, sourceBucket,
|
|||
dataStoreContext.metaHeaders =
|
||||
storeMetadataParams.metaHeaders;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
storeMetadataParams.overheadField = constants.overheadField;
|
||||
|
||||
let dataLocator;
|
||||
// If 0 byte object just set dataLocator to empty array
|
||||
if (!sourceObjMD.location) {
|
||||
|
@ -439,10 +461,15 @@ function objectCopy(authInfo, request, sourceBucket,
|
|||
return next(null, storeMetadataParams, dataLocator, destObjMD,
|
||||
serverSideEncryption, destBucketMD);
|
||||
}
|
||||
const originalIdentityImpDenies = request.actionImplicitDenies;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
delete request.actionImplicitDenies;
|
||||
return data.copyObject(request, sourceLocationConstraintName,
|
||||
storeMetadataParams, dataLocator, dataStoreContext,
|
||||
backendInfoDest, sourceBucketMD, destBucketMD, serverSideEncryption, log,
|
||||
(err, results) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
request.actionImplicitDenies = originalIdentityImpDenies;
|
||||
if (err) {
|
||||
return next(err, destBucketMD);
|
||||
}
|
||||
|
@ -466,10 +493,9 @@ function objectCopy(authInfo, request, sourceBucket,
|
|||
storeMetadataParams.versioning = options.versioning;
|
||||
// eslint-disable-next-line
|
||||
storeMetadataParams.isNull = options.isNull;
|
||||
// eslint-disable-next-line
|
||||
storeMetadataParams.nullVersionId = options.nullVersionId;
|
||||
// eslint-disable-next-line
|
||||
storeMetadataParams.nullUploadId = options.nullUploadId;
|
||||
if (options.extraMD) {
|
||||
Object.assign(storeMetadataParams, options.extraMD);
|
||||
}
|
||||
const dataToDelete = options.dataToDelete;
|
||||
return next(null, storeMetadataParams, destDataGetInfoArr,
|
||||
destObjMD, serverSideEncryption, destBucketMD,
|
||||
|
@ -478,6 +504,11 @@ function objectCopy(authInfo, request, sourceBucket,
|
|||
},
|
||||
function storeNewMetadata(storeMetadataParams, destDataGetInfoArr,
|
||||
destObjMD, serverSideEncryption, destBucketMD, dataToDelete, next) {
|
||||
if (destObjMD && destObjMD.uploadId) {
|
||||
// eslint-disable-next-line
|
||||
storeMetadataParams.oldReplayId = destObjMD.uploadId;
|
||||
}
|
||||
|
||||
return services.metadataStoreObject(destBucketName,
|
||||
destDataGetInfoArr, serverSideEncryption,
|
||||
storeMetadataParams, (err, result) => {
|
||||
|
@ -513,10 +544,8 @@ function objectCopy(authInfo, request, sourceBucket,
|
|||
// the same as the destination
|
||||
if (!sourceIsDestination && dataToDelete) {
|
||||
const newDataStoreName = storeMetadataParams.dataStoreName;
|
||||
const delLog = logger.newRequestLoggerFromSerializedUids(
|
||||
log.getSerializedUids());
|
||||
return data.batchDelete(dataToDelete, request.method,
|
||||
newDataStoreName, delLog, err => {
|
||||
newDataStoreName, log, err => {
|
||||
if (err) {
|
||||
// if error, log the error and move on as it is not
|
||||
// relevant to the client as the client's
|
||||
|
|
|
@ -8,26 +8,30 @@ const { pushMetric } = require('../utapi/utilities');
|
|||
const createAndStoreObject = require('./apiUtils/object/createAndStoreObject');
|
||||
const { decodeVersionId, preprocessingVersioningDelete }
|
||||
= require('./apiUtils/object/versioning');
|
||||
const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
const { hasGovernanceBypassHeader, checkUserGovernanceBypass, ObjectLockInfo }
|
||||
= require('./apiUtils/object/objectLockHelpers');
|
||||
const { isRequesterNonAccountUser } = require('./apiUtils/authorization/permissionChecks');
|
||||
const { config } = require('../Config');
|
||||
const { _bucketRequiresOplogUpdate } = require('./apiUtils/object/deleteObject');
|
||||
|
||||
const versionIdUtils = versioning.VersionID;
|
||||
const objectLockedError = new Error('object locked');
|
||||
const { overheadField } = require('../../constants');
|
||||
|
||||
/**
|
||||
* objectDelete - DELETE an object from a bucket
|
||||
* objectDeleteInternal - DELETE an object from a bucket
|
||||
* @param {AuthInfo} authInfo - requester's infos
|
||||
* @param {object} request - request object given by router,
|
||||
* includes normalized headers
|
||||
* @param {Logger} log - werelogs request instance
|
||||
* @param {boolean} isExpiration - true if the call comes from LifecycleExpiration
|
||||
* @param {function} cb - final cb to call with the result and response headers
|
||||
* @return {undefined}
|
||||
*/
|
||||
function objectDelete(authInfo, request, log, cb) {
|
||||
log.debug('processing request', { method: 'objectDelete' });
|
||||
function objectDeleteInternal(authInfo, request, log, isExpiration, cb) {
|
||||
log.debug('processing request', { method: 'objectDeleteInternal' });
|
||||
if (authInfo.isRequesterPublicUser()) {
|
||||
log.debug('operation not available for public user');
|
||||
monitoring.promMetrics(
|
||||
|
@ -52,14 +56,14 @@ function objectDelete(authInfo, request, log, cb) {
|
|||
bucketName,
|
||||
objectKey,
|
||||
versionId: reqVersionId,
|
||||
requestType: 'objectDelete',
|
||||
requestType: request.apiMethods || 'objectDelete',
|
||||
request,
|
||||
};
|
||||
|
||||
const canonicalID = authInfo.getCanonicalID();
|
||||
return async.waterfall([
|
||||
function validateBucketAndObj(next) {
|
||||
return metadataValidateBucketAndObj(valParams, log,
|
||||
return standardMetadataValidateBucketAndObj(valParams, request.actionImplicitDenies, log,
|
||||
(err, bucketMD, objMD) => {
|
||||
if (err) {
|
||||
return next(err, bucketMD);
|
||||
|
@ -136,19 +140,7 @@ function objectDelete(authInfo, request, log, cb) {
|
|||
|
||||
return next(null, bucketMD, objectMD);
|
||||
},
|
||||
function getVersioningInfo(bucketMD, objectMD, next) {
|
||||
return preprocessingVersioningDelete(bucketName,
|
||||
bucketMD, objectMD, reqVersionId, log,
|
||||
(err, options) => {
|
||||
if (err) {
|
||||
log.error('err processing versioning info',
|
||||
{ error: err });
|
||||
return next(err, bucketMD);
|
||||
}
|
||||
return next(null, bucketMD, objectMD, options);
|
||||
});
|
||||
},
|
||||
function validateHeaders(bucketMD, objectMD, options, next) {
|
||||
function validateHeaders(bucketMD, objectMD, next) {
|
||||
if (objectMD) {
|
||||
const lastModified = objectMD['last-modified'];
|
||||
const { modifiedSinceRes, unmodifiedSinceRes } =
|
||||
|
@ -159,13 +151,14 @@ function objectDelete(authInfo, request, log, cb) {
|
|||
}
|
||||
}
|
||||
return process.nextTick(() =>
|
||||
next(null, bucketMD, objectMD, options));
|
||||
next(null, bucketMD, objectMD));
|
||||
},
|
||||
function deleteOperation(bucketMD, objectMD, delOptions, next) {
|
||||
function deleteOperation(bucketMD, objectMD, next) {
|
||||
const delOptions = preprocessingVersioningDelete(
|
||||
bucketName, bucketMD, objectMD, reqVersionId, config.nullVersionCompatMode);
|
||||
const deleteInfo = {
|
||||
removeDeleteMarker: false,
|
||||
newDeleteMarker: false,
|
||||
isNull: delOptions.isNull,
|
||||
};
|
||||
if (delOptions && delOptions.deleteData && bucketMD.isNFS() &&
|
||||
bucketMD.getReplicationConfiguration()) {
|
||||
|
@ -174,7 +167,10 @@ function objectDelete(authInfo, request, log, cb) {
|
|||
// source does not have versioning.
|
||||
return createAndStoreObject(bucketName, bucketMD, objectKey,
|
||||
objectMD, authInfo, canonicalID, null, request, true, null,
|
||||
log, err => {
|
||||
log, isExpiration ?
|
||||
's3:LifecycleExpiration:DeleteMarkerCreated' :
|
||||
's3:ObjectRemoved:DeleteMarkerCreated',
|
||||
err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
@ -184,26 +180,45 @@ function objectDelete(authInfo, request, log, cb) {
|
|||
deleteInfo.removeDeleteMarker = true;
|
||||
}
|
||||
return services.deleteObject(bucketName, objectMD,
|
||||
objectKey, delOptions, log, (err, delResult) =>
|
||||
next(err, bucketMD, objectMD, delResult,
|
||||
deleteInfo));
|
||||
objectKey, delOptions, false, log, isExpiration ?
|
||||
's3:LifecycleExpiration:Delete' :
|
||||
's3:ObjectRemoved:Delete',
|
||||
(err, delResult) =>
|
||||
next(err, bucketMD, objectMD, delResult, deleteInfo));
|
||||
});
|
||||
}
|
||||
if (delOptions && delOptions.deleteData) {
|
||||
delOptions.overheadField = overheadField;
|
||||
if (objectMD.isDeleteMarker) {
|
||||
// record that we deleted a delete marker to set
|
||||
// response headers accordingly
|
||||
deleteInfo.removeDeleteMarker = true;
|
||||
}
|
||||
|
||||
if (objectMD.uploadId) {
|
||||
// eslint-disable-next-line
|
||||
delOptions.replayId = objectMD.uploadId;
|
||||
}
|
||||
|
||||
if (!_bucketRequiresOplogUpdate(bucketMD)) {
|
||||
delOptions.doesNotNeedOpogUpdate = true;
|
||||
}
|
||||
|
||||
return services.deleteObject(bucketName, objectMD, objectKey,
|
||||
delOptions, log, (err, delResult) => next(err, bucketMD,
|
||||
objectMD, delResult, deleteInfo));
|
||||
delOptions, false, log, isExpiration ?
|
||||
's3:LifecycleExpiration:Delete' :
|
||||
's3:ObjectRemoved:Delete',
|
||||
(err, delResult) => next(err, bucketMD,
|
||||
objectMD, delResult, deleteInfo));
|
||||
}
|
||||
// putting a new delete marker
|
||||
deleteInfo.newDeleteMarker = true;
|
||||
return createAndStoreObject(bucketName, bucketMD,
|
||||
objectKey, objectMD, authInfo, canonicalID, null, request,
|
||||
deleteInfo.newDeleteMarker, null, log, (err, newDelMarkerRes) => {
|
||||
deleteInfo.newDeleteMarker, null, overheadField, log, isExpiration ?
|
||||
's3:LifecycleExpiration:DeleteMarkerCreated' :
|
||||
's3:ObjectRemoved:DeleteMarkerCreated',
|
||||
(err, newDelMarkerRes) => {
|
||||
next(err, bucketMD, objectMD, newDelMarkerRes, deleteInfo);
|
||||
});
|
||||
},
|
||||
|
@ -255,9 +270,10 @@ function objectDelete(authInfo, request, log, cb) {
|
|||
* The decrement accounts for the deletion of the master key when utapi reports
|
||||
* on the number of objects.
|
||||
*/
|
||||
// FIXME: byteLength may be incorrect, see S3C-7440
|
||||
const versioningSuspended = bucketMD.getVersioningConfiguration()
|
||||
&& bucketMD.getVersioningConfiguration().Status === 'Suspended';
|
||||
const deletedSuspendedMasterVersion = versioningSuspended && !!objectMD && deleteInfo.isNull;
|
||||
const deletedSuspendedMasterVersion = versioningSuspended && !!objectMD;
|
||||
// Default to 0 content-length to cover deleting a DeleteMarker
|
||||
const objectByteLength = (objectMD && objectMD['content-length']) || 0;
|
||||
const byteLength = deletedSuspendedMasterVersion ? Number.parseInt(objectByteLength, 10) : null;
|
||||
|
@ -291,4 +307,21 @@ function objectDelete(authInfo, request, log, cb) {
|
|||
});
|
||||
}
|
||||
|
||||
module.exports = objectDelete;
|
||||
/**
|
||||
* This function is used to delete an object from a bucket. The bucket must
|
||||
* already exist and the user must have permission to delete the object.
|
||||
* @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info
|
||||
* @param {object} request - http request object
|
||||
* @param {werelogs.Logger} log - Logger object
|
||||
* @param {function} cb - callback to server
|
||||
* @return {undefined}
|
||||
*/
|
||||
function objectDelete(authInfo, request, log, cb) {
|
||||
log.debug('processing request', { method: 'objectDelete' });
|
||||
return objectDeleteInternal(authInfo, request, log, false, cb);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
objectDelete,
|
||||
objectDeleteInternal,
|
||||
};
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
const async = require('async');
|
||||
const { errors } = require('arsenal');
|
||||
|
||||
const { decodeVersionId, getVersionIdResHeader }
|
||||
const { decodeVersionId, getVersionIdResHeader, getVersionSpecificMetadataOptions }
|
||||
= require('./apiUtils/object/versioning');
|
||||
|
||||
const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const metadata = require('../metadata/wrapper');
|
||||
const getReplicationInfo = require('./apiUtils/object/getReplicationInfo');
|
||||
const { data } = require('../data/wrapper');
|
||||
const { config } = require('../Config');
|
||||
const REPLICATION_ACTION = 'DELETE_TAGGING';
|
||||
|
||||
/**
|
||||
|
@ -41,13 +42,14 @@ function objectDeleteTagging(authInfo, request, log, callback) {
|
|||
authInfo,
|
||||
bucketName,
|
||||
objectKey,
|
||||
requestType: 'objectDeleteTagging',
|
||||
versionId: reqVersionId,
|
||||
getDeleteMarker: true,
|
||||
requestType: request.apiMethods || 'objectDeleteTagging',
|
||||
request,
|
||||
};
|
||||
|
||||
return async.waterfall([
|
||||
next => metadataValidateBucketAndObj(metadataValParams, log,
|
||||
next => standardMetadataValidateBucketAndObj(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, bucket, objectMD) => {
|
||||
if (err) {
|
||||
log.trace('request authorization failed',
|
||||
|
@ -64,6 +66,8 @@ function objectDeleteTagging(authInfo, request, log, callback) {
|
|||
if (objectMD.isDeleteMarker) {
|
||||
log.trace('version is a delete marker',
|
||||
{ method: 'objectDeleteTagging' });
|
||||
// FIXME we should return a `x-amz-delete-marker: true` header,
|
||||
// see S3C-7592
|
||||
return next(errors.MethodNotAllowed, bucket);
|
||||
}
|
||||
return next(null, bucket, objectMD);
|
||||
|
@ -71,8 +75,7 @@ function objectDeleteTagging(authInfo, request, log, callback) {
|
|||
(bucket, objectMD, next) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
objectMD.tags = {};
|
||||
const params = objectMD.versionId ? { versionId:
|
||||
objectMD.versionId } : {};
|
||||
const params = getVersionSpecificMetadataOptions(objectMD, config.nullVersionCompatMode);
|
||||
const replicationInfo = getReplicationInfo(objectKey, bucket, true,
|
||||
0, REPLICATION_ACTION, objectMD);
|
||||
if (replicationInfo) {
|
||||
|
@ -88,7 +91,7 @@ function objectDeleteTagging(authInfo, request, log, callback) {
|
|||
},
|
||||
(bucket, objectMD, next) =>
|
||||
// if external backends handles tagging
|
||||
data.objectTagging('Delete', objectKey, bucket, objectMD,
|
||||
data.objectTagging('Delete', objectKey, bucket.getName(), objectMD,
|
||||
log, err => next(err, bucket, objectMD)),
|
||||
], (err, bucket, objectMD) => {
|
||||
const additionalResHeaders = collectCorsHeaders(request.headers.origin,
|
||||
|
|
|
@ -15,12 +15,13 @@ const getReplicationBackendDataLocator =
|
|||
require('./apiUtils/object/getReplicationBackendDataLocator');
|
||||
const checkReadLocation = require('./apiUtils/object/checkReadLocation');
|
||||
|
||||
const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { config } = require('../Config');
|
||||
const { locationConstraints } = config;
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
const { getPartCountFromMd5 } = require('./apiUtils/object/partInfo');
|
||||
const { setExpirationHeaders } = require('./apiUtils/object/expirationHeaders');
|
||||
const { verifyColdObjectAvailable } = require('./apiUtils/object/coldStorage');
|
||||
|
||||
const validateHeaders = s3middleware.validateConditionalHeaders;
|
||||
|
||||
|
@ -64,11 +65,12 @@ function objectGet(authInfo, request, returnTagCount, log, callback) {
|
|||
bucketName,
|
||||
objectKey,
|
||||
versionId,
|
||||
requestType: 'objectGet',
|
||||
getDeleteMarker: true,
|
||||
requestType: request.apiMethods || 'objectGet',
|
||||
request,
|
||||
};
|
||||
|
||||
return metadataValidateBucketAndObj(mdValParams, log,
|
||||
return standardMetadataValidateBucketAndObj(mdValParams, request.actionImplicitDenies, log,
|
||||
(err, bucket, objMD) => {
|
||||
const corsHeaders = collectCorsHeaders(request.headers.origin,
|
||||
request.method, bucket);
|
||||
|
@ -88,16 +90,12 @@ function objectGet(authInfo, request, returnTagCount, log, callback) {
|
|||
return callback(err, null, corsHeaders);
|
||||
}
|
||||
const verCfg = bucket.getVersioningConfiguration();
|
||||
if (objMD.archive &&
|
||||
// Object is in cold backend
|
||||
(!objMD.archive.restoreRequestedAt ||
|
||||
// Object is being restored
|
||||
(objMD.archive.restoreRequestedAt &&
|
||||
!objMD.archive.restoreCompletedAt))) {
|
||||
const error = errors.InvalidObjectState;
|
||||
// check if object data is in a cold storage
|
||||
const coldErr = verifyColdObjectAvailable(objMD);
|
||||
if (coldErr) {
|
||||
monitoring.promMetrics(
|
||||
'GET', bucketName, error.code, 'getObject');
|
||||
return callback(error, null, corsHeaders);
|
||||
'GET', bucketName, coldErr.code, 'getObject');
|
||||
return callback(coldErr, null, corsHeaders);
|
||||
}
|
||||
if (objMD.isDeleteMarker) {
|
||||
const responseMetaHeaders = Object.assign({},
|
||||
|
|
|
@ -7,7 +7,7 @@ const { pushMetric } = require('../utapi/utilities');
|
|||
const { decodeVersionId, getVersionIdResHeader }
|
||||
= require('./apiUtils/object/versioning');
|
||||
const vault = require('../auth/vault');
|
||||
const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const monitoring = require('../utilities/monitoringHandler');
|
||||
|
||||
// Sample XML response:
|
||||
|
@ -54,12 +54,14 @@ function objectGetACL(authInfo, request, log, callback) {
|
|||
}
|
||||
const versionId = decodedVidResult;
|
||||
|
||||
// FIXME pass 'getDeleteMarker: true' option to set
|
||||
// 'x-amz-delete-marker' header (see S3C-7592)
|
||||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
objectKey,
|
||||
versionId,
|
||||
requestType: 'objectGetACL',
|
||||
requestType: request.apiMethods || 'objectGetACL',
|
||||
request,
|
||||
};
|
||||
const grantInfo = {
|
||||
|
@ -72,7 +74,7 @@ function objectGetACL(authInfo, request, log, callback) {
|
|||
|
||||
return async.waterfall([
|
||||
function validateBucketAndObj(next) {
|
||||
return metadataValidateBucketAndObj(metadataValParams, log,
|
||||
return standardMetadataValidateBucketAndObj(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, bucket, objectMD) => {
|
||||
if (err) {
|
||||
log.trace('request authorization failed',
|
||||
|
@ -90,10 +92,14 @@ function objectGetACL(authInfo, request, log, callback) {
|
|||
if (versionId) {
|
||||
log.trace('requested version is delete marker',
|
||||
{ method: 'objectGetACL' });
|
||||
// FIXME we should return a `x-amz-delete-marker: true` header,
|
||||
// see S3C-7592
|
||||
return next(errors.MethodNotAllowed);
|
||||
}
|
||||
log.trace('most recent version is delete marker',
|
||||
{ method: 'objectGetACL' });
|
||||
// FIXME we should return a `x-amz-delete-marker: true` header,
|
||||
// see S3C-7592
|
||||
return next(errors.NoSuchKey);
|
||||
}
|
||||
return next(null, bucket, objectMD);
|
||||
|
|
|
@ -4,7 +4,7 @@ const { errors, s3middleware } = require('arsenal');
|
|||
const { decodeVersionId, getVersionIdResHeader }
|
||||
= require('./apiUtils/object/versioning');
|
||||
|
||||
const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
|
||||
|
@ -33,17 +33,19 @@ function objectGetLegalHold(authInfo, request, log, callback) {
|
|||
}
|
||||
const versionId = decodedVidResult;
|
||||
|
||||
// FIXME pass 'getDeleteMarker: true' option to set
|
||||
// 'x-amz-delete-marker' header (see S3C-7592)
|
||||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
objectKey,
|
||||
requestType: 'objectGetLegalHold',
|
||||
versionId,
|
||||
requestType: request.apiMethods || 'objectGetLegalHold',
|
||||
request,
|
||||
};
|
||||
|
||||
return async.waterfall([
|
||||
next => metadataValidateBucketAndObj(metadataValParams, log,
|
||||
next => standardMetadataValidateBucketAndObj(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, bucket, objectMD) => {
|
||||
if (err) {
|
||||
log.trace('request authorization failed',
|
||||
|
@ -61,10 +63,14 @@ function objectGetLegalHold(authInfo, request, log, callback) {
|
|||
if (versionId) {
|
||||
log.trace('requested version is delete marker',
|
||||
{ method: 'objectGetLegalHold' });
|
||||
// FIXME we should return a `x-amz-delete-marker: true` header,
|
||||
// see S3C-7592
|
||||
return next(errors.MethodNotAllowed);
|
||||
}
|
||||
log.trace('most recent version is delete marker',
|
||||
{ method: 'objectGetLegalHold' });
|
||||
// FIXME we should return a `x-amz-delete-marker: true` header,
|
||||
// see S3C-7592
|
||||
return next(errors.NoSuchKey);
|
||||
}
|
||||
if (!bucket.isObjectLockEnabled()) {
|
||||
|
|
|
@ -4,7 +4,7 @@ const { errors, s3middleware } = require('arsenal');
|
|||
const { decodeVersionId, getVersionIdResHeader }
|
||||
= require('./apiUtils/object/versioning');
|
||||
|
||||
const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
|
||||
|
@ -33,17 +33,19 @@ function objectGetRetention(authInfo, request, log, callback) {
|
|||
}
|
||||
const reqVersionId = decodedVidResult;
|
||||
|
||||
// FIXME pass 'getDeleteMarker: true' option to set
|
||||
// 'x-amz-delete-marker' header (see S3C-7592)
|
||||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
objectKey,
|
||||
requestType: 'objectGetRetention',
|
||||
versionId: reqVersionId,
|
||||
requestType: request.apiMethods || 'objectGetRetention',
|
||||
request,
|
||||
};
|
||||
|
||||
return async.waterfall([
|
||||
next => metadataValidateBucketAndObj(metadataValParams, log,
|
||||
next => standardMetadataValidateBucketAndObj(metadataValParams, request.actionImplicitDenies, log,
|
||||
(err, bucket, objectMD) => {
|
||||
if (err) {
|
||||
log.trace('request authorization failed',
|
||||
|
@ -61,10 +63,14 @@ function objectGetRetention(authInfo, request, log, callback) {
|
|||
if (reqVersionId) {
|
||||
log.trace('requested version is delete marker',
|
||||
{ method: 'objectGetRetention' });
|
||||
// FIXME we should return a `x-amz-delete-marker: true` header,
|
||||
// see S3C-7592
|
||||
return next(errors.MethodNotAllowed);
|
||||
}
|
||||
log.trace('most recent version is delete marker',
|
||||
{ method: 'objectGetRetention' });
|
||||
// FIXME we should return a `x-amz-delete-marker: true` header,
|
||||
// see S3C-7592
|
||||
return next(errors.NoSuchKey);
|
||||
}
|
||||
if (!bucket.isObjectLockEnabled()) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue