Compare commits

...

36 Commits

Author SHA1 Message Date
kannappanr 7881791a91 CopyObject:Set Content-Type to application/octet-stream if it is not set (#6958) 2018-12-19 14:31:45 -08:00
Harshavardhana d2f8f8c7ee Fix ETag handling with auto-encryption with CopyObject conditions (#7000)
minio-java tests were failing under multiple places when
auto encryption was turned on, handle all the cases properly

This PR fixes

 - CopyObject should decrypt ETag before it does if-match
 - CopyObject should not try to preserve metadata of source
   when rotating keys, unless explicitly asked by the user.
 - We should not try to decrypt Compressed object etag, the
   potential case was if user sets encryption headers along
   with compression enabled.
2018-12-19 14:12:53 -08:00
kannappanr 8c32311b80 Change lock name to include names instead of hash. (#6886)
Previously lockname included the hash of the bucket, object
and uploadid.

This is a part of fix required to list oldest locks on the server.
2018-12-19 13:57:51 -08:00
Ashish Kumar Sinha 9bb88e610e Deletion of subfolders of multipart (#6961)
Delete subfolders under multipart folder upon completion of CompleteMultipartUpload, AbortMultipartUpload and cleanupStaleMultipartUploads functions
2018-12-19 11:27:10 -08:00
Harshavardhana d1e41695fe Add support for federation on browser (#6891) 2018-12-19 18:43:47 +05:30
Aditya Manthramurthy 2aeb3fbe86 Fix csv output delimiter bug (#6994) 2018-12-19 11:49:06 +05:30
Anis Elleuch 99b843a64e Add anonymous flag to prevent logging sensitive information (#6899) 2018-12-18 16:08:11 -08:00
Harshavardhana 4f31a9a33b Reload users upon AddUser on peers (#6975)
Also migrate ReloadFormat to notification subsystem,
remove GetConfig() we do not use this API anymore
2018-12-18 14:39:21 -08:00
Nitish Tiwari 65ddff8899 Fix NAS Gateway Docker command example (#6967)
Fixes #6965
2018-12-18 14:37:17 -08:00
Harshavardhana e7c902bbbc Return proper errors when admin API is not initialized (#6988)
Especially in gateway IAM admin APIs are not enabled
if etcd is not enabled, we should enable admin API though
but only enable IAM and Config APIs with etcd configured.
2018-12-18 13:03:26 -08:00
Andreas Auernhammer 5a5895203b add howto generate a master key and add master key disclaimer (#6992)
This commit adds a section to the master key documentation
describing how to generate a random 256 bit master key.

Further this commit adds a warning that master keys are not
recommended for production systems because it's (currently)
not possible to replace a master key (e.g. in case of compromise).
2018-12-18 13:00:32 -08:00
poornas 7da0336ac8 Make sure env are loaded before gateway layer initialization (#6989) 2018-12-18 10:42:09 -08:00
Harshavardhana 3be616de3f Send deployment ID in notification event response elements (#6991) 2018-12-18 10:05:26 -08:00
Harshavardhana c5bf22fd90 Turn off printing IPv6 endpoints when listening on all interfaces (#6986)
By default when we listen on all interfaces, we print all the
endpoints that at local to all interfaces including IPv6
addresses. Remove IPv6 addresses in endpoint list to be
printed in endpoints unless explicitly specified with '--address'
2018-12-18 21:56:30 +05:30
poornas 7c9f934875 Disallow SSE requests when object layer has encryption disabled (#6981) 2018-12-14 21:39:59 -08:00
Eco b6f9b24b30 Small corrections and example for auto-encryption (#6982) 2018-12-14 16:21:41 -08:00
poornas 13cb814a0e update KMS README.md to set approle env (#6978) 2018-12-14 14:03:16 -08:00
Andreas Auernhammer d264d2c899 add auto-encryption feature (#6523)
This commit adds an auto-encryption feature which allows
the Minio operator to ensure that uploaded objects are
always encrypted.

This change adds the `autoEncryption` configuration option
as part of the KMS conifguration and the ENV. variable
`MINIO_SSE_AUTO_ENCRYPTION:{on,off}`.

It also updates the KMS documentation according to the
changes.

Fixes #6502
2018-12-14 13:35:48 -08:00
Harshavardhana bebaff269c Support IPv6 in minio command line (#6947)
Fixes #6946
2018-12-14 13:07:46 +05:30
Harshavardhana 52b159b1db Allow versionId to be null for Delete,CopyObjectPart (#6972) 2018-12-14 11:34:37 +05:30
Nitish Tiwari 324834e4da Remove duplicate switch case (#6966)
Fixes #6948
2018-12-13 21:58:48 -08:00
Harshavardhana c2ed1347d9 Do not list objects unless specified in policy (#6970)
Currently we use GetObject to check if we are allowed to list,
this might be a security problem since there are many users now
who actively disable a publicly readable listing, anyone who
can guess the browser URL can list the objects.

This PR turns off this behavior and provides a more expected way
based on the policies.

This PR also additionally improves the Download() object
implementation to use a more streamlined code.

These are precursor changes to facilitate federation and web
identity support in browser.
2018-12-14 09:45:09 +05:30
Anis Elleuch 50f6f9fe58 S3 api: Ignore encoding in xml body (#6953)
One user reported having discovered the following error:

API: SYSTEM()
Time: 20:06:17 UTC 12/06/2018
Error: xml: encoding "US-ASCII" declared but Decoder.CharsetReader is nil
1: cmd/handler-utils.go:43:cmd.parseLocationConstraint()
2: cmd/auth-handler.go:250:cmd.checkRequestAuthType()
3: cmd/bucket-handlers.go:411:cmd.objectAPIHandlers.PutBucketHandler()
4: cmd/api-router.go100cmd.(objectAPIHandlers).PutBucketHandler-fm()
5: net/http/server.go:1947:http.HandlerFunc.ServeHTTP()

Hence, adding support of different xml encoding. Although there
is no clear specification about it, even setting "GARBAGE" as an xml
encoding won't change the behavior of AWS, hence the encoding seems
to be ignored.

This commit will follow that behavior and will ignore encoding field
and consider all xml as utf8 encoded.
2018-12-13 12:09:50 -08:00
Minio Trusted 48cb0ea34b Update yaml files to latest version RELEASE.2018-12-13T02-04-19Z 2018-12-13 02:09:53 +00:00
Harshavardhana 6f7c99a333 Allow versionId to be null for Copy,Get,Head API calls (#6942)
Fixes #6935
2018-12-12 11:43:44 -08:00
Harshavardhana 3498f5b0ec List exact DNS entries for a requested bucketName (#6936)
Currently we would end up considering common prefix
buckets to be part of the same DNS service record,
which leads to Minio server wrongly forwarding the
records to incorrect IPs.
2018-12-12 10:47:03 -08:00
Andreas Auernhammer 21d8c0fd13 refactor vault configuration and add master-key KMS (#6488)
This refactors the vault configuration by moving the
vault-related environment variables to `environment.go`
(Other ENV should follow in the future to have a central
place for adding / handling ENV instead of magic constants
and handling across different files)

Further this commit adds master-key SSE-S3 support.
The operator can specify a SSE-S3 master key using
`MINIO_SSE_MASTER_KEY` which will be used as master key
to derive and encrypt per-object keys for SSE-S3
requests.

This commit is also a pre-condition for SSE-S3
auto-encyption support.

Fixes #6329
2018-12-12 12:20:29 +05:30
Kale Blankenship 79b9a9ce46 Provide actual size in events instead of compressed size. (#6950)
Previous behavior did not check if the object was compressed and
incorrectly reported the stored size rather than the actual object
size.
2018-12-11 17:30:15 -08:00
Harshavardhana b9b353db4b Add env to support synchronous ops for all calls (#6877) 2018-12-11 16:22:56 -08:00
poornas 11a9b317a3 Disable ListenBucket notifications for NAS gateway (#6954) 2018-12-11 16:16:09 -08:00
Praveen raj Mani 9af7d627ac Preserve the compression headers while copying (#6952)
Fixes #6951
2018-12-11 12:05:41 -08:00
Harshavardhana 76d9d54603 Filter listing buckets based on user level access (#6940)
Fixes #6701
2018-12-10 22:57:22 +05:30
Harshavardhana 4c7c571875 Support JSON to CSV and CSV to JSON output format conversion (#6910)
This PR implements one of the pending items in issue #6286
in S3 API a user can request CSV output for a JSON document
and a JSON output for a CSV document. This PR refactors
the code a little bit to bring this feature.
2018-12-07 14:55:32 -08:00
James Neiman, President 313ba74b09 Update to Minio GCS Gateway (#6887) 2018-12-06 10:09:37 -08:00
Harshavardhana 3e124315c8 Increase the keep alive timeout to 30 secs (#6924)
Go by default uses a 3 * minute, we should
atleast use 30 secs as 10 secs is too aggressive.
2018-12-06 22:56:16 +05:30
Minio Trusted 78a0fd951e Update yaml files to latest version RELEASE.2018-12-06T01-27-43Z 2018-12-06 01:35:43 +00:00
72 changed files with 2011 additions and 923 deletions
+5
View File
@@ -34,6 +34,11 @@ matrix:
- for d in $(go list ./... | grep -v browser); do go test -v -race --timeout 20m "$d"; done
- bash buildscripts/go-coverage.sh
before_script:
# Add an IPv6 config - see the corresponding Travis issue
# https://github.com/travis-ci/travis-ci/issues/8361
- if [[ "${TRAVIS_OS_NAME}" == "linux" ]]; then sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; fi
before_install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then nvm install stable ; fi
+67
View File
@@ -68,6 +68,36 @@ function start_minio_erasure_sets()
echo "$minio_pid"
}
function start_minio_dist_erasure_sets_ipv6()
{
declare -a minio_pids
export MINIO_ACCESS_KEY=$ACCESS_KEY
export MINIO_SECRET_KEY=$SECRET_KEY
"${MINIO[@]}" server --address="[::1]:9000" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9000.log" 2>&1 &
minio_pids[0]=$!
"${MINIO[@]}" server --address="[::1]:9001" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9001.log" 2>&1 &
minio_pids[1]=$!
"${MINIO[@]}" server --address="[::1]:9002" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9002.log" 2>&1 &
minio_pids[2]=$!
"${MINIO[@]}" server --address="[::1]:9003" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9003.log" 2>&1 &
minio_pids[3]=$!
"${MINIO[@]}" server --address="[::1]:9004" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9004.log" 2>&1 &
minio_pids[4]=$!
"${MINIO[@]}" server --address="[::1]:9005" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9005.log" 2>&1 &
minio_pids[5]=$!
"${MINIO[@]}" server --address="[::1]:9006" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9006.log" 2>&1 &
minio_pids[6]=$!
"${MINIO[@]}" server --address="[::1]:9007" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9007.log" 2>&1 &
minio_pids[7]=$!
"${MINIO[@]}" server --address="[::1]:9008" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9008.log" 2>&1 &
minio_pids[8]=$!
"${MINIO[@]}" server --address="[::1]:9009" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9009.log" 2>&1 &
minio_pids[9]=$!
sleep 35
echo "${minio_pids[@]}"
}
function start_minio_dist_erasure_sets()
{
declare -a minio_pids
@@ -161,6 +191,34 @@ function run_test_erasure_sets() {
return "$rv"
}
function run_test_dist_erasure_sets_ipv6()
{
minio_pids=( $(start_minio_dist_erasure_sets_ipv6) )
export SERVER_ENDPOINT="[::1]:9000"
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
rv=$?
for pid in "${minio_pids[@]}"; do
kill "$pid"
done
sleep 3
if [ "$rv" -ne 0 ]; then
for i in $(seq 0 9); do
echo "server$i log:"
cat "$WORK_DIR/dist-minio-v6-900$i.log"
done
fi
for i in $(seq 0 9); do
rm -f "$WORK_DIR/dist-minio-v6-900$i.log"
done
return "$rv"
}
function run_test_dist_erasure_sets()
{
minio_pids=( $(start_minio_dist_erasure_sets) )
@@ -237,6 +295,7 @@ function run_test_gateway_s3()
{
minio_pid="$(start_minio_gateway_s3)"
export SERVER_ENDPOINT="127.0.0.1:9000"
export ACCESS_KEY=Q3AM3UQ867SPQQA43P2F
export SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
@@ -279,6 +338,7 @@ function __init__()
exit 1
fi
sed -i 's|-sS|-sSg|g' "$FUNCTIONAL_TESTS"
chmod a+x "$FUNCTIONAL_TESTS"
}
@@ -319,6 +379,13 @@ function main()
exit 1
fi
echo "Testing in Distributed Erasure setup as sets with ipv6"
if ! run_test_dist_erasure_sets_ipv6; then
echo "FAILED"
rm -fr "$WORK_DIR"
exit 1
fi
echo "Testing in Gateway S3 setup"
if ! run_test_gateway_s3; then
echo "FAILED"
+40
View File
@@ -867,6 +867,14 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
return
}
// Notify all other Minio peers to reload users
for host, err := range globalNotificationSys.LoadUsers() {
if err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
logger.LogIf(ctx, err)
}
}
}
// AddUser - PUT /minio/admin/v1/add-user?accessKey=<access_key>
@@ -927,6 +935,14 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
return
}
// Notify all other Minio peers to reload users
for host, err := range globalNotificationSys.LoadUsers() {
if err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
logger.LogIf(ctx, err)
}
}
}
// ListCannedPolicies - GET /minio/admin/v1/list-canned-policies
@@ -992,6 +1008,14 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
return
}
// Notify all other Minio peers to reload users
for host, err := range globalNotificationSys.LoadUsers() {
if err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
logger.LogIf(ctx, err)
}
}
}
// AddCannedPolicy - PUT /minio/admin/v1/add-canned-policy?name=<policy_name>
@@ -1049,6 +1073,14 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
return
}
// Notify all other Minio peers to reload users
for host, err := range globalNotificationSys.LoadUsers() {
if err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
logger.LogIf(ctx, err)
}
}
}
// SetUserPolicy - PUT /minio/admin/v1/set-user-policy?accessKey=<access_key>&name=<policy_name>
@@ -1088,6 +1120,14 @@ func (a adminAPIHandlers) SetUserPolicy(w http.ResponseWriter, r *http.Request)
if err := globalIAMSys.SetUserPolicy(accessKey, policyName); err != nil {
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
}
// Notify all other Minio peers to reload users
for host, err := range globalNotificationSys.LoadUsers() {
if err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
logger.LogIf(ctx, err)
}
}
}
// SetConfigHandler - PUT /minio/admin/v1/config
+1 -1
View File
@@ -277,7 +277,7 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
// Setup admin mgmt REST API handlers.
adminRouter := mux.NewRouter()
registerAdminRouter(adminRouter)
registerAdminRouter(adminRouter, true)
return &adminXLTestBed{
xlDirs: xlDirs,
+6 -1
View File
@@ -641,7 +641,12 @@ func (h *healSequence) healDiskFormat() error {
// Healing succeeded notify the peers to reload format and re-initialize disks.
// We will not notify peers only if healing succeeded.
if err == nil {
peersReInitFormat(globalAdminPeers, h.settings.DryRun)
for host, rerr := range globalNotificationSys.ReloadFormat(h.settings.DryRun) {
if rerr != nil {
logger.GetReqInfo(h.ctx).SetTags("peerAddress", host.String())
logger.LogIf(h.ctx, rerr)
}
}
}
// Push format heal result
+32 -30
View File
@@ -31,7 +31,7 @@ type adminAPIHandlers struct {
}
// registerAdminRouter - Add handler functions for each service REST API routes.
func registerAdminRouter(router *mux.Router) {
func registerAdminRouter(router *mux.Router, enableIAM bool) {
adminAPI := adminAPIHandlers{}
// Admin router
@@ -69,42 +69,44 @@ func registerAdminRouter(router *mux.Router) {
/// Config operations
// Update credentials
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceHdrs(adminAPI.UpdateAdminCredentialsHandler))
// Get config
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
// Set config
adminV1Router.Methods(http.MethodPut).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler))
if enableIAM {
// Update credentials
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceHdrs(adminAPI.UpdateAdminCredentialsHandler))
// Get config
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
// Set config
adminV1Router.Methods(http.MethodPut).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler))
// Get config keys/values
adminV1Router.Methods(http.MethodGet).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKeysHandler))
// Set config keys/values
adminV1Router.Methods(http.MethodPut).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKeysHandler))
// Get config keys/values
adminV1Router.Methods(http.MethodGet).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKeysHandler))
// Set config keys/values
adminV1Router.Methods(http.MethodPut).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKeysHandler))
// -- IAM APIs --
// -- IAM APIs --
// Add policy IAM
adminV1Router.Methods(http.MethodPut).Path("/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}")
// Add policy IAM
adminV1Router.Methods(http.MethodPut).Path("/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}")
// Add user IAM
adminV1Router.Methods(http.MethodPut).Path("/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
adminV1Router.Methods(http.MethodPut).Path("/set-user-policy").HandlerFunc(httpTraceHdrs(adminAPI.SetUserPolicy)).
Queries("accessKey", "{accessKey:.*}").Queries("name", "{name:.*}")
adminV1Router.Methods(http.MethodPut).Path("/set-user-status").HandlerFunc(httpTraceHdrs(adminAPI.SetUserStatus)).
Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}")
// Add user IAM
adminV1Router.Methods(http.MethodPut).Path("/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
adminV1Router.Methods(http.MethodPut).Path("/set-user-policy").HandlerFunc(httpTraceHdrs(adminAPI.SetUserPolicy)).
Queries("accessKey", "{accessKey:.*}").Queries("name", "{name:.*}")
adminV1Router.Methods(http.MethodPut).Path("/set-user-status").HandlerFunc(httpTraceHdrs(adminAPI.SetUserStatus)).
Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}")
// Remove policy IAM
adminV1Router.Methods(http.MethodDelete).Path("/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}")
// Remove policy IAM
adminV1Router.Methods(http.MethodDelete).Path("/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}")
// Remove user IAM
adminV1Router.Methods(http.MethodDelete).Path("/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")
// Remove user IAM
adminV1Router.Methods(http.MethodDelete).Path("/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")
// List users
adminV1Router.Methods(http.MethodGet).Path("/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers))
// List users
adminV1Router.Methods(http.MethodGet).Path("/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers))
// List policies
adminV1Router.Methods(http.MethodGet).Path("/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies))
// List policies
adminV1Router.Methods(http.MethodGet).Path("/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies))
}
// If none of the routes match.
adminV1Router.NotFoundHandler = http.HandlerFunc(httpTraceHdrs(notFoundHandler))
// If none of the routes match, return error.
adminV1Router.NotFoundHandler = http.HandlerFunc(httpTraceHdrs(notFoundHandlerJSON))
}
+5 -39
View File
@@ -45,29 +45,12 @@ func (rpcClient *AdminRPCClient) SignalService(signal serviceSignal) (err error)
return rpcClient.Call(adminServiceName+".SignalService", &args, &reply)
}
// ReInitFormat - re-initialize disk format, remotely.
func (rpcClient *AdminRPCClient) ReInitFormat(dryRun bool) error {
args := ReInitFormatArgs{DryRun: dryRun}
reply := VoidReply{}
return rpcClient.Call(adminServiceName+".ReInitFormat", &args, &reply)
}
// ServerInfo - returns the server info of the server to which the RPC call is made.
func (rpcClient *AdminRPCClient) ServerInfo() (sid ServerInfoData, err error) {
err = rpcClient.Call(adminServiceName+".ServerInfo", &AuthArgs{}, &sid)
return sid, err
}
// GetConfig - returns config.json of the remote server.
func (rpcClient *AdminRPCClient) GetConfig() ([]byte, error) {
args := AuthArgs{}
var reply []byte
err := rpcClient.Call(adminServiceName+".GetConfig", &args, &reply)
return reply, err
}
// StartProfiling - starts profiling in the remote server.
func (rpcClient *AdminRPCClient) StartProfiling(profiler string) error {
args := StartProfilingArgs{Profiler: profiler}
@@ -125,9 +108,7 @@ func NewAdminRPCClient(host *xnet.Host) (*AdminRPCClient, error) {
// commands like service stop and service restart.
type adminCmdRunner interface {
SignalService(s serviceSignal) error
ReInitFormat(dryRun bool) error
ServerInfo() (ServerInfoData, error)
GetConfig() ([]byte, error)
StartProfiling(string) error
DownloadProfilingData() ([]byte, error)
}
@@ -149,6 +130,11 @@ func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
// Use first IPv4 instead of loopback address.
localAddr = net.JoinHostPort(sortIPs(localIP4.ToSlice())[0], globalMinioPort)
}
if strings.HasPrefix(localAddr, "[::1]:") {
// Use first IPv4 instead of loopback address.
localAddr = net.JoinHostPort(localIP6.ToSlice()[0], globalMinioPort)
}
adminPeerList = append(adminPeerList, adminPeer{
addr: localAddr,
cmdRunner: localAdminClient{},
@@ -169,26 +155,6 @@ func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
return adminPeerList
}
// peersReInitFormat - reinitialize remote object layers to new format.
func peersReInitFormat(peers adminPeers, dryRun bool) error {
errs := make([]error, len(peers))
// Send ReInitFormat RPC call to all nodes.
// for local adminPeer this is a no-op.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
if !peer.isLocal {
errs[idx] = peer.cmdRunner.ReInitFormat(dryRun)
}
}(i, peer)
}
wg.Wait()
return nil
}
// Initialize global adminPeer collection.
func initGlobalAdminPeers(endpoints EndpointList) {
globalAdminPeers = makeAdminPeers(endpoints)
-17
View File
@@ -68,23 +68,6 @@ func (receiver *adminRPCReceiver) DownloadProfilingData(args *AuthArgs, reply *[
return
}
// GetConfig - returns the config.json of this server.
func (receiver *adminRPCReceiver) GetConfig(args *AuthArgs, reply *[]byte) (err error) {
*reply, err = receiver.local.GetConfig()
return err
}
// ReInitFormatArgs - provides dry-run information to re-initialize format.json
type ReInitFormatArgs struct {
AuthArgs
DryRun bool
}
// ReInitFormat - re-init 'format.json'
func (receiver *adminRPCReceiver) ReInitFormat(args *ReInitFormatArgs, reply *VoidReply) error {
return receiver.local.ReInitFormat(args.DryRun)
}
// NewAdminRPCServer - returns new admin RPC server.
func NewAdminRPCServer() (*xrpc.Server, error) {
rpcServer := xrpc.NewServer()
+1 -79
View File
@@ -40,11 +40,8 @@ import (
///////////////////////////////////////////////////////////////////////////////
func testAdminCmdRunnerSignalService(t *testing.T, client adminCmdRunner) {
tmpGlobalServiceSignalCh := globalServiceSignalCh
defer func(sigChan chan serviceSignal) { globalServiceSignalCh = sigChan }(globalServiceSignalCh)
globalServiceSignalCh = make(chan serviceSignal, 10)
defer func() {
globalServiceSignalCh = tmpGlobalServiceSignalCh
}()
testCases := []struct {
signal serviceSignal
@@ -66,34 +63,6 @@ func testAdminCmdRunnerSignalService(t *testing.T, client adminCmdRunner) {
}
}
func testAdminCmdRunnerReInitFormat(t *testing.T, client adminCmdRunner) {
tmpGlobalObjectAPI := globalObjectAPI
defer func() {
globalObjectAPI = tmpGlobalObjectAPI
}()
testCases := []struct {
objectAPI ObjectLayer
dryRun bool
expectErr bool
}{
{&DummyObjectLayer{}, true, false},
{&DummyObjectLayer{}, false, false},
{nil, true, true},
{nil, false, true},
}
for i, testCase := range testCases {
globalObjectAPI = testCase.objectAPI
err := client.ReInitFormat(testCase.dryRun)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}
func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
tmpGlobalBootTime := globalBootTime
tmpGlobalObjectAPI := globalObjectAPI
@@ -140,33 +109,6 @@ func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
}
}
func testAdminCmdRunnerGetConfig(t *testing.T, client adminCmdRunner) {
tmpGlobalServerConfig := globalServerConfig
defer func() {
globalServerConfig = tmpGlobalServerConfig
}()
config := newServerConfig()
testCases := []struct {
config *serverConfig
expectErr bool
}{
{globalServerConfig, false},
{config, false},
}
for i, testCase := range testCases {
globalServerConfig = testCase.config
_, err := client.GetConfig()
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}
func newAdminRPCHTTPServerClient(t *testing.T) (*httptest.Server, *AdminRPCClient, *serverConfig) {
rpcServer, err := NewAdminRPCServer()
if err != nil {
@@ -208,16 +150,6 @@ func TestAdminRPCClientSignalService(t *testing.T) {
testAdminCmdRunnerSignalService(t, rpcClient)
}
func TestAdminRPCClientReInitFormat(t *testing.T) {
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
defer httpServer.Close()
defer func() {
globalServerConfig = prevGlobalServerConfig
}()
testAdminCmdRunnerReInitFormat(t, rpcClient)
}
func TestAdminRPCClientServerInfo(t *testing.T) {
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
defer httpServer.Close()
@@ -227,13 +159,3 @@ func TestAdminRPCClientServerInfo(t *testing.T) {
testAdminCmdRunnerServerInfo(t, rpcClient)
}
func TestAdminRPCClientGetConfig(t *testing.T) {
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
defer httpServer.Close()
defer func() {
globalServerConfig = prevGlobalServerConfig
}()
testAdminCmdRunnerGetConfig(t, rpcClient)
}
+6
View File
@@ -87,6 +87,7 @@ const (
ErrNoSuchBucketPolicy
ErrNoSuchKey
ErrNoSuchUpload
ErrNoSuchVersion
ErrNotImplemented
ErrPreconditionFailed
ErrRequestTimeTooSkewed
@@ -447,6 +448,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "The specified multipart upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed.",
HTTPStatusCode: http.StatusNotFound,
},
ErrNoSuchVersion: {
Code: "NoSuchVersion",
Description: "Indicates that the version ID specified in the request does not match an existing version.",
HTTPStatusCode: http.StatusNotFound,
},
ErrNotImplemented: {
Code: "NotImplemented",
Description: "A header you provided implies functionality that is not implemented",
+12 -1
View File
@@ -482,7 +482,14 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
return
}
if crypto.S3KMS.IsRequested(r.Header) { // SSE-KMS is not supported
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
return
}
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
return
}
bucket := mux.Vars(r)["bucket"]
// Require Content-Length to be set in the request
@@ -619,6 +626,10 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
rawReader := hashReader
pReader := NewPutObjReader(rawReader, nil, nil)
var objectEncryptionKey []byte
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
}
if objectAPI.IsEncryptionSupported() {
if hasServerSideEncryptionHeader(formValues) && !hasSuffix(object, slashSeparator) { // handle SSE-C and SSE-S3 requests
var reader io.Reader
+25 -15
View File
@@ -30,7 +30,6 @@ import (
dns2 "github.com/miekg/dns"
"github.com/minio/cli"
"github.com/minio/minio-go/pkg/set"
"github.com/minio/minio/cmd/crypto"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/cmd/logger/target/console"
"github.com/minio/minio/cmd/logger/target/http"
@@ -81,6 +80,31 @@ func loadLoggers() {
func handleCommonCmdArgs(ctx *cli.Context) {
// Get "json" flag from command line argument and
// enable json and quite modes if jason flag is turned on.
globalCLIContext.JSON = ctx.IsSet("json") || ctx.GlobalIsSet("json")
if globalCLIContext.JSON {
logger.EnableJSON()
}
// Get quiet flag from command line argument.
globalCLIContext.Quiet = ctx.IsSet("quiet") || ctx.GlobalIsSet("quiet")
if globalCLIContext.Quiet {
logger.EnableQuiet()
}
// Get anonymous flag from command line argument.
globalCLIContext.Anonymous = ctx.IsSet("anonymous") || ctx.GlobalIsSet("anonymous")
if globalCLIContext.Anonymous {
logger.EnableAnonymous()
}
// Fetch address option
globalCLIContext.Addr = ctx.GlobalString("address")
if globalCLIContext.Addr == "" || globalCLIContext.Addr == ":"+globalMinioDefaultPort {
globalCLIContext.Addr = ctx.String("address")
}
var configDir string
switch {
@@ -322,20 +346,6 @@ func handleCommonEnvVars() {
globalWORMEnabled = bool(wormFlag)
}
kmsConf, err := crypto.NewVaultConfig()
if err != nil {
logger.Fatal(err, "Unable to initialize hashicorp vault")
}
if kmsConf.Vault.Endpoint != "" {
kms, err := crypto.NewVault(kmsConf)
if err != nil {
logger.Fatal(err, "Unable to initialize KMS")
}
globalKMS = kms
globalKMSKeyID = kmsConf.Vault.Key.Name
globalKMSConfig = kmsConf
}
if compress := os.Getenv("MINIO_COMPRESS"); compress != "" {
globalIsCompressionEnabled = strings.EqualFold(compress, "true")
}
+4 -8
View File
@@ -271,8 +271,8 @@ func (s *serverConfig) loadFromEnvs() {
s.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
}
if globalKMS != nil {
s.KMS = globalKMSConfig
if err := Environment.LookupKMSConfig(s.KMS); err != nil {
logger.FatalIf(err, "Unable to setup the KMS")
}
if globalIsEnvCompression {
@@ -534,12 +534,8 @@ func (s *serverConfig) loadToCachedConfigs() {
globalCacheExpiry = cacheConf.Expiry
globalCacheMaxUse = cacheConf.MaxUse
}
if globalKMS == nil {
globalKMSConfig = s.KMS
if kms, err := crypto.NewVault(globalKMSConfig); err == nil {
globalKMS = kms
globalKMSKeyID = globalKMSConfig.Vault.Key.Name
}
if err := Environment.LookupKMSConfig(s.KMS); err != nil {
logger.FatalIf(err, "Unable to setup the KMS")
}
if !globalIsCompressionEnabled {
+2 -1
View File
@@ -16,5 +16,6 @@ package crypto
// KMSConfig has the KMS config for hashicorp vault
type KMSConfig struct {
Vault VaultConfig `json:"vault"`
AutoEncryption bool `json:"-"`
Vault VaultConfig `json:"vault"`
}
+123 -184
View File
@@ -19,200 +19,133 @@ import (
"encoding/base64"
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
vault "github.com/hashicorp/vault/api"
)
const (
// vaultEndpointEnv Vault endpoint environment variable
vaultEndpointEnv = "MINIO_SSE_VAULT_ENDPOINT"
// vaultAuthTypeEnv type of vault auth to be used
vaultAuthTypeEnv = "MINIO_SSE_VAULT_AUTH_TYPE"
// vaultAppRoleIDEnv Vault AppRole ID environment variable
vaultAppRoleIDEnv = "MINIO_SSE_VAULT_APPROLE_ID"
// vaultAppSecretIDEnv Vault AppRole Secret environment variable
vaultAppSecretIDEnv = "MINIO_SSE_VAULT_APPROLE_SECRET"
// vaultKeyVersionEnv Vault Key Version environment variable
vaultKeyVersionEnv = "MINIO_SSE_VAULT_KEY_VERSION"
// vaultKeyNameEnv Vault Encryption Key Name environment variable
vaultKeyNameEnv = "MINIO_SSE_VAULT_KEY_NAME"
// vaultCAPath is the path to a directory of PEM-encoded CA
// cert files to verify the Vault server SSL certificate.
vaultCAPath = "MINIO_SSE_VAULT_CAPATH"
)
var (
//ErrKMSAuthLogin is raised when there is a failure authenticating to KMS
ErrKMSAuthLogin = errors.New("Vault service did not return auth info")
)
// VaultKey represents vault encryption key-ring.
type VaultKey struct {
Name string `json:"name"` // The name of the encryption key-ring
Version int `json:"version"` // The key version
}
// VaultAuth represents vault authentication type.
// Currently the only supported authentication type is AppRole.
type VaultAuth struct {
Type string `json:"type"` // The authentication type
AppRole VaultAppRole `json:"approle"` // The AppRole authentication credentials
}
// VaultAppRole represents vault AppRole authentication credentials
type VaultAppRole struct {
ID string `json:"id"` // The AppRole access ID
Secret string `json:"secret"` // The AppRole secret
}
// VaultConfig represents vault configuration.
type VaultConfig struct {
Endpoint string `json:"endpoint"` // The vault API endpoint as URL
CAPath string `json:"-"` // The path to PEM-encoded certificate files used for mTLS. Currently not used in config file.
Auth VaultAuth `json:"auth"` // The vault authentication configuration
Key VaultKey `json:"key-id"` // The named key used for key-generation / decryption.
Namespace string `json:"-"` // The vault namespace of enterprise vault instances
}
// vaultService represents a connection to a vault KMS.
type vaultService struct {
config *VaultConfig
client *vault.Client
leaseDuration time.Duration
}
// return transit secret engine's path for generate data key operation
func (v *vaultService) genDataKeyEndpoint(key string) string {
return "/transit/datakey/plaintext/" + key
var _ KMS = (*vaultService)(nil) // compiler check that *vaultService implements KMS
// empty/default vault configuration used to check whether a particular is empty.
var emptyVaultConfig = VaultConfig{}
// IsEmpty returns true if the vault config struct is an
// empty configuration.
func (v *VaultConfig) IsEmpty() bool { return *v == emptyVaultConfig }
// Verify returns a nil error if the vault configuration
// is valid. A valid configuration is either empty or
// contains valid non-default values.
func (v *VaultConfig) Verify() (err error) {
if v.IsEmpty() {
return // an empty configuration is valid
}
switch {
case v.Endpoint == "":
err = errors.New("crypto: missing hashicorp vault endpoint")
case strings.ToLower(v.Auth.Type) != "approle":
err = fmt.Errorf("crypto: invalid hashicorp vault authentication type: %s is not supported", v.Auth.Type)
case v.Auth.AppRole.ID == "":
err = errors.New("crypto: missing hashicorp vault AppRole ID")
case v.Auth.AppRole.Secret == "":
err = errors.New("crypto: missing hashicorp vault AppSecret ID")
case v.Key.Name == "":
err = errors.New("crypto: missing hashicorp vault key name")
case v.Key.Version < 0:
err = errors.New("crypto: invalid hashicorp vault key version: The key version must not be negative")
}
return
}
// return transit secret engine's path for decrypt operation
func (v *vaultService) decryptEndpoint(key string) string {
return "/transit/decrypt/" + key
}
// NewVault initializes Hashicorp Vault KMS by authenticating
// to Vault with the credentials in config and gets a client
// token for future api calls.
func NewVault(config VaultConfig) (KMS, error) {
if config.IsEmpty() {
return nil, errors.New("crypto: the hashicorp vault configuration must not be empty")
}
if err := config.Verify(); err != nil {
return nil, err
}
// VaultKey represents vault encryption key-id name & version
type VaultKey struct {
Name string `json:"name"`
Version int `json:"version"`
}
vaultCfg := vault.Config{Address: config.Endpoint}
if err := vaultCfg.ConfigureTLS(&vault.TLSConfig{CAPath: config.CAPath}); err != nil {
return nil, err
}
client, err := vault.NewClient(&vaultCfg)
if err != nil {
return nil, err
}
if config.Namespace != "" {
client.SetNamespace(config.Namespace)
}
// VaultAuth represents vault auth type to use. For now, AppRole is the only supported
// auth type.
type VaultAuth struct {
Type string `json:"type"`
AppRole VaultAppRole `json:"approle"`
}
// VaultAppRole represents vault approle credentials
type VaultAppRole struct {
ID string `json:"id"`
Secret string `json:"secret"`
}
// VaultConfig holds config required to start vault service
type VaultConfig struct {
Endpoint string `json:"endpoint"`
Auth VaultAuth `json:"auth"`
Key VaultKey `json:"key-id"`
}
// validate whether all required env variables needed to start vault service have
// been set
func validateVaultConfig(c *VaultConfig) error {
if c.Endpoint == "" {
return fmt.Errorf("Missing hashicorp vault endpoint - %s is empty", vaultEndpointEnv)
payload := map[string]interface{}{
"role_id": config.Auth.AppRole.ID,
"secret_id": config.Auth.AppRole.Secret,
}
if strings.ToLower(c.Auth.Type) != "approle" {
return fmt.Errorf("Unsupported hashicorp vault auth type - %s", vaultAuthTypeEnv)
}
if c.Auth.AppRole.ID == "" {
return fmt.Errorf("Missing hashicorp vault AppRole ID - %s is empty", vaultAppRoleIDEnv)
}
if c.Auth.AppRole.Secret == "" {
return fmt.Errorf("Missing hashicorp vault AppSecret ID - %s is empty", vaultAppSecretIDEnv)
}
if c.Key.Name == "" {
return fmt.Errorf("Invalid value set in environment variable %s", vaultKeyNameEnv)
}
if c.Key.Version < 0 {
return fmt.Errorf("Invalid value set in environment variable %s", vaultKeyVersionEnv)
}
return nil
}
// authenticate to vault with app role id and app role secret, and get a client access token, lease duration
func getVaultAccessToken(client *vault.Client, appRoleID, appSecret string) (token string, duration int, err error) {
data := map[string]interface{}{
"role_id": appRoleID,
"secret_id": appSecret,
}
resp, e := client.Logical().Write("auth/approle/login", data)
if e != nil {
return token, duration, e
resp, err := client.Logical().Write("auth/approle/login", payload)
if err != nil {
return nil, err
}
if resp.Auth == nil {
return token, duration, ErrKMSAuthLogin
return nil, ErrKMSAuthLogin
}
return resp.Auth.ClientToken, resp.Auth.LeaseDuration, nil
client.SetToken(resp.Auth.ClientToken)
v := &vaultService{client: client, config: &config, leaseDuration: time.Duration(resp.Auth.LeaseDuration)}
v.renewToken()
return v, nil
}
// NewVaultConfig sets KMSConfig from environment
// variables and performs validations.
func NewVaultConfig() (KMSConfig, error) {
kc := KMSConfig{}
endpoint := os.Getenv(vaultEndpointEnv)
roleID := os.Getenv(vaultAppRoleIDEnv)
roleSecret := os.Getenv(vaultAppSecretIDEnv)
keyName := os.Getenv(vaultKeyNameEnv)
keyVersion := 0
authType := "approle"
if versionStr := os.Getenv(vaultKeyVersionEnv); versionStr != "" {
version, err := strconv.Atoi(versionStr)
if err != nil {
return kc, fmt.Errorf("Unable to parse %s value (`%s`)", vaultKeyVersionEnv, versionStr)
}
keyVersion = version
}
// return if none of the vault env variables are configured
if (endpoint == "") && (roleID == "") && (roleSecret == "") && (keyName == "") && (keyVersion == 0) {
return kc, nil
}
c := VaultConfig{
Endpoint: endpoint,
Auth: VaultAuth{
Type: authType,
AppRole: VaultAppRole{
ID: roleID,
Secret: roleSecret,
},
},
Key: VaultKey{
Version: keyVersion,
Name: keyName,
},
}
if err := validateVaultConfig(&c); err != nil {
return kc, err
}
kc.Vault = c
return kc, nil
}
// NewVault initializes Hashicorp Vault KMS by
// authenticating to Vault with the credentials in KMSConfig,
// and gets a client token for future api calls.
func NewVault(kmsConf KMSConfig) (KMS, error) {
config := kmsConf.Vault
vconfig := &vault.Config{
Address: config.Endpoint,
}
if err := vconfig.ConfigureTLS(&vault.TLSConfig{
CAPath: os.Getenv(vaultCAPath),
}); err != nil {
return nil, err
}
c, err := vault.NewClient(vconfig)
if err != nil {
return nil, err
}
if ns, ok := os.LookupEnv("MINIO_SSE_VAULT_NAMESPACE"); ok {
c.SetNamespace(ns)
}
accessToken, leaseDuration, err := getVaultAccessToken(c, config.Auth.AppRole.ID, config.Auth.AppRole.Secret)
if err != nil {
return nil, err
}
// authenticate and get the access token
c.SetToken(accessToken)
v := vaultService{client: c, config: &config, leaseDuration: time.Duration(leaseDuration)}
v.renewToken(c)
return &v, nil
}
func (v *vaultService) renewToken(c *vault.Client) {
// renewToken starts a new go-routine which renews
// the vault authentication token periodically.
func (v *vaultService) renewToken() {
retryDelay := 1 * time.Minute
go func() {
for {
s, err := c.Auth().Token().RenewSelf(int(v.leaseDuration))
s, err := v.client.Auth().Token().RenewSelf(int(v.leaseDuration))
if err != nil {
time.Sleep(retryDelay)
continue
@@ -223,48 +156,54 @@ func (v *vaultService) renewToken(c *vault.Client) {
}()
}
// Generates a random plain text key, sealed plain text key from
// Vault. It returns the plaintext key and sealed plaintext key on success
// GenerateKey returns a new plaintext key, generated by the KMS,
// and a sealed version of this plaintext key encrypted using the
// named key referenced by keyID. It also binds the generated key
// cryptographically to the provided context.
func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
contextStream := new(bytes.Buffer)
ctx.WriteTo(contextStream)
var contextStream bytes.Buffer
ctx.WriteTo(&contextStream)
payload := map[string]interface{}{
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
}
s, err1 := v.client.Logical().Write(v.genDataKeyEndpoint(keyID), payload)
if err1 != nil {
return key, sealedKey, err1
s, err := v.client.Logical().Write(fmt.Sprintf("/transit/datakey/plaintext/%s", keyID), payload)
if err != nil {
return key, sealedKey, err
}
sealKey := s.Data["ciphertext"].(string)
plainKey, err := base64.StdEncoding.DecodeString(s.Data["plaintext"].(string))
if err != nil {
return key, sealedKey, err1
return key, sealedKey, err
}
copy(key[:], []byte(plainKey))
return key, []byte(sealKey), nil
}
// unsealKMSKey unseals the sealedKey using the Vault master key
// referenced by the keyID. The plain text key is returned on success.
// UnsealKey returns the decrypted sealedKey as plaintext key.
// Therefore it sends the sealedKey to the KMS which decrypts
// it using the named key referenced by keyID and responses with
// the plaintext key.
//
// The context must be same context as the one provided while
// generating the plaintext key / sealedKey.
func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
contextStream := new(bytes.Buffer)
ctx.WriteTo(contextStream)
var contextStream bytes.Buffer
ctx.WriteTo(&contextStream)
payload := map[string]interface{}{
"ciphertext": string(sealedKey),
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
}
s, err1 := v.client.Logical().Write(v.decryptEndpoint(keyID), payload)
if err1 != nil {
return key, err1
s, err := v.client.Logical().Write(fmt.Sprintf("/transit/decrypt/%s", keyID), payload)
if err != nil {
return key, err
}
base64Key := s.Data["plaintext"].(string)
plainKey, err1 := base64.StdEncoding.DecodeString(base64Key)
if err1 != nil {
return key, err1
plainKey, err := base64.StdEncoding.DecodeString(base64Key)
if err != nil {
return key, err
}
copy(key[:], []byte(plainKey))
return key, nil
}
+57
View File
@@ -226,6 +226,17 @@ func TestGetSetIndexes(t *testing.T) {
}
}
func getHexSequences(start int, number int, paddinglen int) (seq []string) {
for i := start; i <= number; i++ {
if paddinglen == 0 {
seq = append(seq, fmt.Sprintf("%x", i))
} else {
seq = append(seq, fmt.Sprintf(fmt.Sprintf("%%0%dx", paddinglen), i))
}
}
return seq
}
func getSequences(start int, number int, paddinglen int) (seq []string) {
for i := start; i <= number; i++ {
if paddinglen == 0 {
@@ -472,6 +483,52 @@ func TestParseEndpointSet(t *testing.T) {
},
true,
},
// IPv6 ellipses with hexadecimal expansion
{
"http://[2001:3984:3989::{1...a}]/disk{1...10}",
endpointSet{
[]ellipses.ArgPattern{
[]ellipses.Pattern{
{
Prefix: "",
Suffix: "",
Seq: getSequences(1, 10, 0),
},
{
Prefix: "http://[2001:3984:3989::",
Suffix: "]/disk",
Seq: getHexSequences(1, 10, 0),
},
},
},
nil,
[][]uint64{{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}},
},
true,
},
// IPv6 ellipses with hexadecimal expansion with 3 position numerics.
{
"http://[2001:3984:3989::{001...00a}]/disk{1...10}",
endpointSet{
[]ellipses.ArgPattern{
[]ellipses.Pattern{
{
Prefix: "",
Suffix: "",
Seq: getSequences(1, 10, 0),
},
{
Prefix: "http://[2001:3984:3989::",
Suffix: "]/disk",
Seq: getHexSequences(1, 10, 3),
},
},
},
nil,
[][]uint64{{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}},
},
true,
},
}
for i, testCase := range testCases {
+12 -10
View File
@@ -112,6 +112,9 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
return ep, fmt.Errorf("invalid URL endpoint format: port number must be between 1 to 65535")
}
}
if i := strings.Index(host, "%"); i > -1 {
host = host[:i]
}
if host == "" {
return ep, fmt.Errorf("invalid URL endpoint format: empty host name")
@@ -152,7 +155,7 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
// Only check if the arg is an ip address and ask for scheme since its absent.
// localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as
// /mnt/export1. So we go ahead and start the minio server in FS modes in these cases.
if isHostIPv4(arg) {
if isHostIP(arg) {
return ep, fmt.Errorf("invalid URL endpoint format: missing scheme http or https")
}
u = &url.URL{Path: path.Clean(arg)}
@@ -224,7 +227,6 @@ func NewEndpointList(args ...string) (endpoints EndpointList, err error) {
uniqueArgs.Add(arg)
endpoints = append(endpoints, endpoint)
}
return endpoints, nil
}
@@ -341,7 +343,7 @@ func CreateEndpoints(serverAddr string, args ...[]string) (string, EndpointList,
if err != nil {
host = endpoint.Host
}
hostIPSet, _ := getHostIP4(host)
hostIPSet, _ := getHostIP(host)
if IPSet, ok := pathIPMap[endpoint.Path]; ok {
if !IPSet.Intersection(hostIPSet).IsEmpty() {
return serverAddr, endpoints, setupType,
@@ -411,12 +413,12 @@ func CreateEndpoints(serverAddr string, args ...[]string) (string, EndpointList,
host = localServerAddr
}
ipList, err := getHostIP4(host)
ipList, err := getHostIP(host)
logger.FatalIf(err, "unexpected error when resolving host '%s'", host)
// Filter ipList by IPs those start with '127.'.
// Filter ipList by IPs those start with '127.' or '::1'
loopBackIPs := ipList.FuncMatch(func(ip string, matchString string) bool {
return strings.HasPrefix(ip, "127.")
return strings.HasPrefix(ip, "127.") || strings.HasPrefix(ip, "::1")
}, "")
// If loop back IP is found and ipList contains only loop back IPs, then error out.
@@ -485,10 +487,10 @@ func GetLocalPeer(endpoints EndpointList) (localPeer string) {
// Local peer can be empty in FS or Erasure coded mode.
// If so, return globalMinioHost + globalMinioPort value.
if globalMinioHost != "" {
return globalMinioHost + ":" + globalMinioPort
return net.JoinHostPort(globalMinioHost, globalMinioPort)
}
return "127.0.0.1:" + globalMinioPort
return net.JoinHostPort("127.0.0.1", globalMinioPort)
}
return peerSet.ToSlice()[0]
}
@@ -525,10 +527,10 @@ func updateDomainIPs(endPoints set.StringSet) {
continue
}
}
IPs, _ := getHostIP4(host)
IPs, _ := getHostIP(host)
ipList = ipList.Union(IPs)
}
globalDomainIPs = ipList.FuncMatch(func(ip string, matchString string) bool {
return !strings.HasPrefix(ip, "127.")
return !strings.HasPrefix(ip, "127.") || strings.HasPrefix(ip, "::1")
}, "")
}
+7 -7
View File
@@ -304,27 +304,27 @@ func TestCreateEndpoints(t *testing.T) {
}, DistXLSetupType, nil},
}
for _, testCase := range testCases {
for i, testCase := range testCases {
serverAddr, endpoints, setupType, err := CreateEndpoints(testCase.serverAddr, testCase.args...)
if err == nil {
if testCase.expectedErr != nil {
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
t.Fatalf("Test (%d) error: expected = %v, got = <nil>", i+1, testCase.expectedErr)
} else {
if serverAddr != testCase.expectedServerAddr {
t.Fatalf("serverAddr: expected = %v, got = %v", testCase.expectedServerAddr, serverAddr)
t.Fatalf("Test (%d) serverAddr: expected = %v, got = %v", i+1, testCase.expectedServerAddr, serverAddr)
}
if !reflect.DeepEqual(endpoints, testCase.expectedEndpoints) {
t.Fatalf("endpoints: expected = %v, got = %v", testCase.expectedEndpoints, endpoints)
t.Fatalf("Test (%d) endpoints: expected = %v, got = %v", i+1, testCase.expectedEndpoints, endpoints)
}
if setupType != testCase.expectedSetupType {
t.Fatalf("setupType: expected = %v, got = %v", testCase.expectedSetupType, setupType)
t.Fatalf("Test (%d) setupType: expected = %v, got = %v", i+1, testCase.expectedSetupType, setupType)
}
}
} else if testCase.expectedErr == nil {
t.Fatalf("error: expected = <nil>, got = %v", err)
t.Fatalf("Test (%d) error: expected = <nil>, got = %v", i+1, err)
} else if err.Error() != testCase.expectedErr.Error() {
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
t.Fatalf("Test (%d) error: expected = %v, got = %v", i+1, testCase.expectedErr, err)
}
}
}
+182
View File
@@ -0,0 +1,182 @@
// Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"encoding/hex"
"errors"
"fmt"
"os"
"strconv"
"strings"
"github.com/minio/minio/cmd/crypto"
)
const (
// EnvKMSMasterKey is the environment variable used to specify
// a KMS master key used to protect SSE-S3 per-object keys.
// Valid values must be of the from: "KEY_ID:32_BYTE_HEX_VALUE".
EnvKMSMasterKey = "MINIO_SSE_MASTER_KEY"
// EnvAutoEncryption is the environment variable used to en/disable
// SSE-S3 auto-encryption. SSE-S3 auto-encryption, if enabled,
// requires a valid KMS configuration and turns any non-SSE-C
// request into an SSE-S3 request.
// If present EnvAutoEncryption must be either "on" or "off".
EnvAutoEncryption = "MINIO_SSE_AUTO_ENCRYPTION"
)
const (
// EnvVaultEndpoint is the environment variable used to specify
// the vault HTTPS endpoint.
EnvVaultEndpoint = "MINIO_SSE_VAULT_ENDPOINT"
// EnvVaultAuthType is the environment variable used to specify
// the authentication type for vault.
EnvVaultAuthType = "MINIO_SSE_VAULT_AUTH_TYPE"
// EnvVaultAppRoleID is the environment variable used to specify
// the vault AppRole ID.
EnvVaultAppRoleID = "MINIO_SSE_VAULT_APPROLE_ID"
// EnvVaultAppSecretID is the environment variable used to specify
// the vault AppRole secret corresponding to the AppRole ID.
EnvVaultAppSecretID = "MINIO_SSE_VAULT_APPROLE_SECRET"
// EnvVaultKeyVersion is the environment variable used to specify
// the vault key version.
EnvVaultKeyVersion = "MINIO_SSE_VAULT_KEY_VERSION"
// EnvVaultKeyName is the environment variable used to specify
// the vault named key-ring. In the S3 context it's referred as
// customer master key ID (CMK-ID).
EnvVaultKeyName = "MINIO_SSE_VAULT_KEY_NAME"
// EnvVaultCAPath is the environment variable used to specify the
// path to a directory of PEM-encoded CA cert files. These CA cert
// files are used to authenticate Minio to Vault over mTLS.
EnvVaultCAPath = "MINIO_SSE_VAULT_CAPATH"
// EnvVaultNamespace is the environment variable used to specify
// vault namespace. The vault namespace is used if the enterprise
// version of Hashicorp Vault is used.
EnvVaultNamespace = "MINIO_SSE_VAULT_NAMESPACE"
)
// Environment provides functions for accessing environment
// variables.
var Environment = environment{}
type environment struct{}
// Get retrieves the value of the environment variable named
// by the key. If the variable is present in the environment the
// value (which may be empty) is returned. Otherwise it returns
// the specified default value.
func (environment) Get(key, defaultValue string) string {
if v, ok := os.LookupEnv(key); ok {
return v
}
return defaultValue
}
// Lookup retrieves the value of the environment variable named
// by the key. If the variable is present in the environment the
// value (which may be empty) is returned and the boolean is true.
// Otherwise the returned value will be empty and the boolean will
// be false.
func (environment) Lookup(key string) (string, bool) { return os.LookupEnv(key) }
// LookupKMSConfig extracts the KMS configuration provided by environment
// variables and merge them with the provided KMS configuration. The
// merging follows the following rules:
//
// 1. A valid value provided as environment variable is higher prioritized
// than the provided configuration and overwrites the value from the
// configuration file.
//
// 2. A value specified as environment variable never changes the configuration
// file. So it is never made a persistent setting.
//
// It sets the global KMS configuration according to the merged configuration
// on success.
func (env environment) LookupKMSConfig(config crypto.KMSConfig) (err error) {
// Lookup Hashicorp-Vault configuration & overwrite config entry if ENV var is present
config.Vault.Endpoint = env.Get(EnvVaultEndpoint, config.Vault.Endpoint)
config.Vault.CAPath = env.Get(EnvVaultCAPath, config.Vault.CAPath)
config.Vault.Auth.Type = env.Get(EnvVaultAuthType, config.Vault.Auth.Type)
config.Vault.Auth.AppRole.ID = env.Get(EnvVaultAppRoleID, config.Vault.Auth.AppRole.ID)
config.Vault.Auth.AppRole.Secret = env.Get(EnvVaultAppSecretID, config.Vault.Auth.AppRole.Secret)
config.Vault.Key.Name = env.Get(EnvVaultKeyName, config.Vault.Key.Name)
config.Vault.Namespace = env.Get(EnvVaultNamespace, config.Vault.Namespace)
keyVersion := env.Get(EnvVaultKeyVersion, strconv.Itoa(config.Vault.Key.Version))
config.Vault.Key.Version, err = strconv.Atoi(keyVersion)
if err != nil {
return fmt.Errorf("Invalid ENV variable: Unable to parse %s value (`%s`)", EnvVaultKeyVersion, keyVersion)
}
if err = config.Vault.Verify(); err != nil {
return err
}
// Lookup KMS master keys - only available through ENV.
if masterKey, ok := env.Lookup(EnvKMSMasterKey); ok {
if !config.Vault.IsEmpty() { // Vault and KMS master key provided
return errors.New("Ambiguous KMS configuration: vault configuration and a master key are provided at the same time")
}
globalKMSKeyID, globalKMS, err = parseKMSMasterKey(masterKey)
if err != nil {
return err
}
}
if !config.Vault.IsEmpty() {
globalKMS, err = crypto.NewVault(config.Vault)
if err != nil {
return err
}
globalKMSKeyID = config.Vault.Key.Name
}
autoEncryption, err := ParseBoolFlag(env.Get(EnvAutoEncryption, "off"))
if err != nil {
return err
}
globalAutoEncryption = bool(autoEncryption)
if globalAutoEncryption && globalKMS == nil { // auto-encryption enabled but no KMS
return errors.New("Invalid KMS configuration: auto-encryption is enabled but no valid KMS configuration is present")
}
return nil
}
// parseKMSMasterKey parses the value of the environment variable
// `EnvKMSMasterKey` and returns a key-ID and a master-key KMS on success.
func parseKMSMasterKey(envArg string) (string, crypto.KMS, error) {
values := strings.SplitN(envArg, ":", 2)
if len(values) != 2 {
return "", nil, fmt.Errorf("Invalid KMS master key: %s does not contain a ':'", envArg)
}
var (
keyID = values[0]
hexKey = values[1]
)
if len(hexKey) != 64 { // 2 hex bytes = 1 byte
return "", nil, fmt.Errorf("Invalid KMS master key: %s not a 32 bytes long HEX value", hexKey)
}
var masterKey [32]byte
if _, err := hex.Decode(masterKey[:], []byte(hexKey)); err != nil {
return "", nil, fmt.Errorf("Invalid KMS master key: %s not a 32 bytes long HEX value", hexKey)
}
return keyID, crypto.NewKMS(masterKey), nil
}
+5
View File
@@ -682,6 +682,8 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
return oi, toObjectErr(err, bucket, object)
}
fsRemoveAll(ctx, uploadIDDir)
// It is safe to ignore any directory not empty error (in case there were multiple uploadIDs on the same object)
fsRemoveDir(ctx, fs.getMultipartSHADir(bucket, object))
fi, err := fsStatFile(ctx, pathJoin(fs.fsPath, bucket, object))
if err != nil {
return oi, toObjectErr(err, bucket, object)
@@ -769,6 +771,9 @@ func (fs *FSObjects) cleanupStaleMultipartUploads(ctx context.Context, cleanupIn
}
if now.Sub(fi.ModTime()) > expiry {
fsRemoveAll(ctx, pathJoin(fs.fsPath, minioMetaMultipartBucket, entry, uploadID))
// It is safe to ignore any directory not empty error (in case there were multiple uploadIDs on the same object)
fsRemoveDir(ctx, pathJoin(fs.fsPath, minioMetaMultipartBucket, entry))
}
}
}
+37 -49
View File
@@ -18,8 +18,8 @@ package cmd
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"os"
"os/signal"
@@ -112,34 +112,11 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
cli.ShowCommandHelpAndExit(ctx, gatewayName, 1)
}
// Get "json" flag from command line argument and
// enable json and quite modes if jason flag is turned on.
jsonFlag := ctx.IsSet("json") || ctx.GlobalIsSet("json")
if jsonFlag {
logger.EnableJSON()
}
// Get quiet flag from command line argument.
quietFlag := ctx.IsSet("quiet") || ctx.GlobalIsSet("quiet")
if quietFlag {
logger.EnableQuiet()
}
// Fetch address option
gatewayAddr := ctx.GlobalString("address")
if gatewayAddr == ":"+globalMinioPort {
gatewayAddr = ctx.String("address")
}
// Handle common command args.
handleCommonCmdArgs(ctx)
// Get port to listen on from gateway address
var pErr error
_, globalMinioPort, pErr = net.SplitHostPort(gatewayAddr)
if pErr != nil {
logger.FatalIf(pErr, "Unable to start gateway")
}
globalMinioHost, globalMinioPort = mustSplitHostPort(globalCLIContext.Addr)
// On macOS, if a process already listens on LOCALIPADDR:PORT, net.Listen() falls back
// to IPv6 address ie minio will start listening on IPv6 address whereas another
@@ -177,11 +154,12 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
if globalEtcdClient != nil {
// Enable STS router if etcd is enabled.
registerSTSRouter(router)
// Enable admin router if etcd is enabled.
registerAdminRouter(router)
}
// Enable IAM admin APIs if etcd is enabled, if not just enable basic
// operations such as profiling, server info etc.
registerAdminRouter(router, globalEtcdClient != nil)
// Add healthcheck router
registerHealthCheckRouter(router)
@@ -210,7 +188,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
getCert = globalTLSCerts.GetCertificate
}
globalHTTPServer = xhttp.NewServer([]string{gatewayAddr}, criticalErrorHandler{registerHandlers(router, globalHandlers...)}, getCert)
globalHTTPServer = xhttp.NewServer([]string{globalCLIContext.Addr}, criticalErrorHandler{registerHandlers(router, globalHandlers...)}, getCert)
globalHTTPServer.UpdateBytesReadFunc = globalConnStats.incInputBytes
globalHTTPServer.UpdateBytesWrittenFunc = globalConnStats.incOutputBytes
go func() {
@@ -219,6 +197,22 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
signal.Notify(globalOSSignalCh, os.Interrupt, syscall.SIGTERM)
// !!! Do not move this block !!!
// For all gateways, the config needs to be loaded from env
// prior to initializing the gateway layer
{
// Initialize server config.
srvCfg := newServerConfig()
// Override any values from ENVs.
srvCfg.loadFromEnvs()
// hold the mutex lock before a new config is assigned.
globalServerConfigMu.Lock()
globalServerConfig = srvCfg
globalServerConfigMu.Unlock()
}
newObject, err := gw.NewGatewayLayer(globalServerConfig.GetCredential())
if err != nil {
// Stop watching for any certificate changes.
@@ -228,27 +222,13 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
logger.FatalIf(err, "Unable to initialize gateway backend")
}
// Create a new config system.
globalConfigSys = NewConfigSys()
if globalEtcdClient != nil && gatewayName == "nas" {
// Initialize server config.
// Create a new config system.
globalConfigSys = NewConfigSys()
// Load globalServerConfig from etcd
_ = globalConfigSys.Init(newObject)
} else {
// Initialize server config.
srvCfg := newServerConfig()
// Override any values from ENVs.
srvCfg.loadFromEnvs()
// Load values to cached global values.
srvCfg.loadToCachedConfigs()
// hold the mutex lock before a new config is assigned.
globalServerConfigMu.Lock()
globalServerConfig = srvCfg
globalServerConfigMu.Unlock()
}
// Load logger subsystem
loadLoggers()
@@ -284,13 +264,18 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
if globalEtcdClient != nil && newObject.IsNotificationSupported() {
_ = globalNotificationSys.Init(newObject)
}
if globalAutoEncryption && !newObject.IsEncryptionSupported() {
logger.Fatal(errors.New("Invalid KMS configuration"), "auto-encryption is enabled but gateway does not support encryption")
}
// Once endpoints are finalized, initialize the new object api.
globalObjLayerMutex.Lock()
globalObjectAPI = newObject
globalObjLayerMutex.Unlock()
// Prints the formatted startup message once object layer is initialized.
if !quietFlag {
if !globalCLIContext.Quiet {
mode := globalMinioModeGatewayPrefix + gatewayName
// Check update mode.
checkUpdate(mode)
@@ -301,8 +286,11 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
}
// Print gateway startup message.
printGatewayStartupMessage(getAPIEndpoints(gatewayAddr), gatewayName)
printGatewayStartupMessage(getAPIEndpoints(), gatewayName)
}
// Set uptime time after object layer has initialized.
globalBootTime = UTCNow()
handleSignals()
}
+3 -2
View File
@@ -55,9 +55,10 @@ func printGatewayCommonMsg(apiEndpoints []string) {
cred := globalServerConfig.GetCredential()
apiEndpointStr := strings.Join(apiEndpoints, " ")
// Colorize the message and print.
logger.StartupMessage(colorBlue("\nEndpoint: ") + colorBold(fmt.Sprintf(getFormatStr(len(apiEndpointStr), 1), apiEndpointStr)))
if isTerminal() {
logger.StartupMessage(colorBlue("Endpoint: ") + colorBold(fmt.Sprintf(getFormatStr(len(apiEndpointStr), 1), apiEndpointStr)))
if isTerminal() && !globalCLIContext.Anonymous {
logger.StartupMessage(colorBlue("AccessKey: ") + colorBold(fmt.Sprintf("%s ", cred.AccessKey)))
logger.StartupMessage(colorBlue("SecretKey: ") + colorBold(fmt.Sprintf("%s ", cred.SecretKey)))
}
+1 -1
View File
@@ -117,7 +117,7 @@ func (g *NAS) Production() bool {
}
// IsListenBucketSupported returns whether listen bucket notification is applicable for this gateway.
func (g *NAS) IsListenBucketSupported() bool {
func (n *nasObjects) IsListenBucketSupported() bool {
return false
}
+47 -6
View File
@@ -19,7 +19,6 @@ package cmd
import (
"bufio"
"context"
"fmt"
"net"
"net/http"
"strings"
@@ -627,11 +626,52 @@ type bucketForwardingHandler struct {
func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if globalDNSConfig == nil || globalDomainName == "" ||
guessIsBrowserReq(r) || guessIsHealthCheckReq(r) ||
guessIsMetricsReq(r) || guessIsRPCReq(r) || isAdminReq(r) {
guessIsHealthCheckReq(r) || guessIsMetricsReq(r) ||
guessIsRPCReq(r) || isAdminReq(r) {
f.handler.ServeHTTP(w, r)
return
}
// For browser requests, when federation is setup we need to
// specifically handle download and upload for browser requests.
if guessIsBrowserReq(r) && globalDNSConfig != nil && globalDomainName != "" {
var bucket, _ string
switch r.Method {
case http.MethodPut:
if getRequestAuthType(r) == authTypeJWT {
bucket, _ = urlPath2BucketObjectName(strings.TrimPrefix(r.URL.Path, minioReservedBucketPath+"/upload"))
}
case http.MethodGet:
if t := r.URL.Query().Get("token"); t != "" {
bucket, _ = urlPath2BucketObjectName(strings.TrimPrefix(r.URL.Path, minioReservedBucketPath+"/download"))
} else if getRequestAuthType(r) != authTypeJWT && !strings.HasPrefix(r.URL.Path, minioReservedBucketPath) {
bucket, _ = urlPath2BucketObjectName(r.URL.Path)
}
}
if bucket != "" {
sr, err := globalDNSConfig.Get(bucket)
if err != nil {
if err == dns.ErrNoEntriesFound {
writeErrorResponse(w, ErrNoSuchBucket, r.URL, guessIsBrowserReq(r))
} else {
writeErrorResponse(w, toAPIErrorCode(context.Background(), err), r.URL, guessIsBrowserReq(r))
}
return
}
if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
r.URL.Scheme = "http"
if globalIsSSL {
r.URL.Scheme = "https"
}
r.URL.Host = getHostFromSrv(sr)
f.fwd.ServeHTTP(w, r)
return
}
}
f.handler.ServeHTTP(w, r)
return
}
bucket, object := urlPath2BucketObjectName(r.URL.Path)
// ListBucket requests should be handled at current endpoint as
// all buckets data can be fetched from here.
@@ -663,12 +703,11 @@ func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
return
}
if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
host, port := getRandomHostPort(sr)
r.URL.Scheme = "http"
if globalIsSSL {
r.URL.Scheme = "https"
}
r.URL.Host = fmt.Sprintf("%s:%d", host, port)
r.URL.Host = getHostFromSrv(sr)
f.fwd.ServeHTTP(w, r)
return
}
@@ -743,7 +782,9 @@ func (s customHeaderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Set custom headers such as x-amz-request-id and x-minio-deployment-id
// for each request.
w.Header().Set(responseRequestIDKey, mustGetRequestID(UTCNow()))
w.Header().Set(responseDeploymentIDKey, globalDeploymentID)
if globalDeploymentID != "" {
w.Header().Set(responseDeploymentIDKey, globalDeploymentID)
}
s.handler.ServeHTTP(logger.NewResponseWriter(w), r)
}
+14 -3
View File
@@ -42,6 +42,8 @@ import (
const (
globalMinioCertExpireWarnDays = time.Hour * 24 * 30 // 30 days.
globalMinioDefaultPort = "9000"
globalMinioDefaultRegion = ""
// This is a sha256 output of ``arn:aws:iam::minio:user/admin``,
// this is kept in present form to be compatible with S3 owner ID
@@ -90,6 +92,12 @@ const (
maxLocationConstraintSize = 3 * humanize.MiByte
)
var globalCLIContext = struct {
JSON, Quiet bool
Anonymous bool
Addr string
}{}
var (
// Indicates the total number of erasure coded sets configured.
globalXLSetCount int
@@ -127,7 +135,7 @@ var (
// Minio local server address (in `host:port` format)
globalMinioAddr = ""
// Minio default port, can be changed through command line.
globalMinioPort = "9000"
globalMinioPort = globalMinioDefaultPort
// Holds the host that was passed using --address
globalMinioHost = ""
@@ -230,8 +238,11 @@ var (
globalKMSKeyID string
// Allocated KMS
globalKMS crypto.KMS
// KMS config
globalKMSConfig crypto.KMSConfig
// Auto-Encryption, if enabled, turns any non-SSE-C request
// into an SSE-S3 request. If enabled a valid, non-empty KMS
// configuration must be present.
globalAutoEncryption bool
// Is compression include extensions/content-types set.
globalIsEnvCompression bool
+19
View File
@@ -134,6 +134,11 @@ func extractMetadata(ctx context.Context, r *http.Request) (metadata map[string]
return nil, err
}
// Set content-type to default value if it is not set.
if _, ok := metadata["content-type"]; !ok {
metadata["content-type"] = "application/octet-stream"
}
// Success.
return metadata, nil
}
@@ -183,6 +188,13 @@ func getReqAccessCred(r *http.Request, region string) (cred auth.Credentials) {
if cred.AccessKey == "" {
cred, _, _ = getReqAccessKeyV2(r)
}
if cred.AccessKey == "" {
claims, owner, _ := webRequestAuthenticate(r)
if owner {
return globalServerConfig.GetCredential()
}
cred, _ = globalIAMSys.GetUser(claims.Subject)
}
return cred
}
@@ -194,6 +206,7 @@ func extractReqParams(r *http.Request) map[string]string {
region := globalServerConfig.GetRegion()
cred := getReqAccessCred(r, region)
// Success.
return map[string]string{
"region": region,
@@ -351,6 +364,12 @@ func getResource(path string, host string, domain string) (string, error) {
return slashSeparator + pathJoin(bucket, path), nil
}
// If none of the http routes match respond with MethodNotAllowed, in JSON
func notFoundHandlerJSON(w http.ResponseWriter, r *http.Request) {
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
return
}
// If none of the http routes match respond with MethodNotAllowed
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
writeErrorResponse(w, ErrMethodNotAllowed, r.URL, guessIsBrowserReq(r))
+7 -3
View File
@@ -178,6 +178,9 @@ type httpListener struct {
// isRoutineNetErr returns true if error is due to a network timeout,
// connect reset or io.EOF and false otherwise
func isRoutineNetErr(err error) bool {
if err == nil {
return false
}
if nErr, ok := err.(*net.OpError); ok {
// Check if the error is a tcp connection reset
if syscallErr, ok := nErr.Err.(*os.SyscallError); ok {
@@ -188,7 +191,8 @@ func isRoutineNetErr(err error) bool {
// Check if the error is a timeout
return nErr.Timeout()
}
return err == io.EOF
// check for io.EOF and also some times io.EOF is wrapped is another error type.
return err == io.EOF || err.Error() == "EOF"
}
// start - starts separate goroutine for each TCP listener. A valid insecure/TLS HTTP new connection is passed to httpListener.acceptCh.
@@ -395,8 +399,8 @@ func newHTTPListener(serverAddrs []string,
for _, serverAddr := range serverAddrs {
var l net.Listener
if l, err = listen("tcp4", serverAddr); err != nil {
if l, err = fallbackListen("tcp4", serverAddr); err != nil {
if l, err = listen("tcp", serverAddr); err != nil {
if l, err = fallbackListen("tcp", serverAddr); err != nil {
return nil, err
}
}
+9
View File
@@ -20,6 +20,7 @@ import (
"bufio"
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
@@ -801,6 +802,10 @@ func TestIgnoreErr(t *testing.T) {
err: &net.OpError{Err: &myTimeoutErr{timeout: true}},
want: true,
},
{
err: errors.New("EOF"),
want: true,
},
{
err: &net.OpError{Err: &myTimeoutErr{timeout: false}},
want: false,
@@ -809,6 +814,10 @@ func TestIgnoreErr(t *testing.T) {
err: io.ErrUnexpectedEOF,
want: false,
},
{
err: nil,
want: false,
},
}
for i, tc := range testCases {
+1 -1
View File
@@ -36,7 +36,7 @@ const (
DefaultShutdownTimeout = 5 * time.Second
// DefaultTCPKeepAliveTimeout - default TCP keep alive timeout for accepted connection.
DefaultTCPKeepAliveTimeout = 10 * time.Second
DefaultTCPKeepAliveTimeout = 30 * time.Second
// DefaultReadTimeout - default timout to read data from accepted connection.
DefaultReadTimeout = 5 * time.Minute
+2 -2
View File
@@ -60,9 +60,9 @@ type IAMSys struct {
iamCannedPolicyMap map[string]iampolicy.Policy
}
// Load - load iam.json
// Load - loads iam subsystem
func (sys *IAMSys) Load(objAPI ObjectLayer) error {
return sys.Init(objAPI)
return sys.refresh(objAPI)
}
// Init - initializes config system from iam.json
-11
View File
@@ -18,9 +18,7 @@ package cmd
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"io/ioutil"
@@ -76,15 +74,6 @@ func (lc localAdminClient) ServerInfo() (sid ServerInfoData, e error) {
}, nil
}
// GetConfig - returns config.json of the local server.
func (lc localAdminClient) GetConfig() ([]byte, error) {
if globalServerConfig == nil {
return nil, fmt.Errorf("config not present")
}
return json.Marshal(globalServerConfig)
}
// StartProfiling - starts profiling on the local server.
func (lc localAdminClient) StartProfiling(profiler string) error {
if globalProfiler != nil {
-8
View File
@@ -24,14 +24,6 @@ func TestLocalAdminClientSignalService(t *testing.T) {
testAdminCmdRunnerSignalService(t, &localAdminClient{})
}
func TestLocalAdminClientReInitFormat(t *testing.T) {
testAdminCmdRunnerReInitFormat(t, &localAdminClient{})
}
func TestLocalAdminClientServerInfo(t *testing.T) {
testAdminCmdRunnerServerInfo(t, &localAdminClient{})
}
func TestLocalAdminClientGetConfig(t *testing.T) {
testAdminCmdRunnerGetConfig(t, &localAdminClient{})
}
+36 -1
View File
@@ -18,17 +18,28 @@ package logger
import (
"context"
"encoding/hex"
"fmt"
"go/build"
"hash"
"path/filepath"
"reflect"
"runtime"
"strings"
"time"
"github.com/minio/highwayhash"
"github.com/minio/minio-go/pkg/set"
"github.com/minio/minio/cmd/logger/message/log"
)
var (
// HighwayHash key for logging in anonymous mode
magicHighwayHash256Key = []byte("\x4b\xe7\x34\xfa\x8e\x23\x8a\xcd\x26\x3e\x83\xe6\xbb\x96\x85\x52\x04\x0f\x93\x5d\xa3\x9f\x44\x14\x97\xe0\x9d\x13\x22\xde\x36\xa0")
// HighwayHash hasher for logging in anonymous mode
loggerHighwayHasher hash.Hash
)
// Disable disables all logging, false by default. (used for "go test")
var Disable = false
@@ -94,7 +105,7 @@ func (level Level) String() string {
// quietFlag: Hide startup messages if enabled
// jsonFlag: Display in JSON format, if enabled
var (
quietFlag, jsonFlag bool
quietFlag, jsonFlag, anonFlag bool
// Custom function to format error
errorFmtFunc func(string, error, bool) string
)
@@ -110,6 +121,12 @@ func EnableJSON() {
quietFlag = true
}
// EnableAnonymous - turns anonymous flag
// to avoid printing sensitive information.
func EnableAnonymous() {
anonFlag = true
}
// IsJSON - returns true if jsonFlag is true
func IsJSON() bool {
return jsonFlag
@@ -187,6 +204,8 @@ func Init(goPath string, goRoot string) {
// paths like "{GOROOT}/src/github.com/minio/minio"
// and "{GOPATH}/src/github.com/minio/minio"
trimStrings = append(trimStrings, filepath.Join("github.com", "minio", "minio")+string(filepath.Separator))
loggerHighwayHasher, _ = highwayhash.New(magicHighwayHash256Key) // New will never return error since key is 256 bit
}
func trimTrace(f string) string {
@@ -239,6 +258,14 @@ func getTrace(traceLevel int) []string {
return trace
}
// Return the highway hash of the passed string
func hashString(input string) string {
defer loggerHighwayHasher.Reset()
loggerHighwayHasher.Write([]byte(input))
checksum := loggerHighwayHasher.Sum(nil)
return hex.EncodeToString(checksum)
}
// LogAlwaysIf prints a detailed error message during
// the execution of the server.
func LogAlwaysIf(ctx context.Context, err error) {
@@ -312,6 +339,14 @@ func logIf(ctx context.Context, err error) {
},
}
if anonFlag {
entry.API.Args.Bucket = hashString(entry.API.Args.Bucket)
entry.API.Args.Object = hashString(entry.API.Args.Object)
entry.RemoteHost = hashString(entry.RemoteHost)
entry.Message = reflect.TypeOf(err).String()
entry.Trace.Variables = make(map[string]string)
}
// Iterate over all logger targets to send the log entry
for _, t := range Targets {
t.Send(entry)
+4
View File
@@ -44,6 +44,10 @@ var globalFlags = []cli.Flag{
Name: "quiet",
Usage: "Disable startup information.",
},
cli.BoolFlag{
Name: "anonymous",
Usage: "Hide sensitive information from logging.",
},
cli.BoolFlag{
Name: "json",
Usage: "Output server logs and startup information in json format.",
+57 -20
View File
@@ -37,14 +37,21 @@ import (
// IPv4 addresses of local host.
var localIP4 = mustGetLocalIP4()
// IPv6 address of local host.
var localIP6 = mustGetLocalIP6()
// mustSplitHostPort is a wrapper to net.SplitHostPort() where error is assumed to be a fatal.
func mustSplitHostPort(hostPort string) (host, port string) {
host, port, err := net.SplitHostPort(hostPort)
// Strip off IPv6 zone information.
if i := strings.Index(host, "%"); i > -1 {
host = host[:i]
}
logger.FatalIf(err, "Unable to split host port %s", hostPort)
return host, port
}
// mustGetLocalIP4 returns IPv4 addresses of local host. It panics on error.
// mustGetLocalIP4 returns IPv4 addresses of localhost. It panics on error.
func mustGetLocalIP4() (ipList set.StringSet) {
ipList = set.NewStringSet()
addrs, err := net.InterfaceAddrs()
@@ -67,8 +74,31 @@ func mustGetLocalIP4() (ipList set.StringSet) {
return ipList
}
// getHostIP4 returns IPv4 address of given host.
func getHostIP4(host string) (ipList set.StringSet, err error) {
// mustGetLocalIP6 returns IPv6 addresses of localhost. It panics on error.
func mustGetLocalIP6() (ipList set.StringSet) {
ipList = set.NewStringSet()
addrs, err := net.InterfaceAddrs()
logger.FatalIf(err, "Unable to get IP addresses of this host")
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip.To4() == nil {
ipList.Add(ip.String())
}
}
return ipList
}
// getHostIP returns IP address of given host.
func getHostIP(host string) (ipList set.StringSet, err error) {
var ips []net.IP
if ips, err = net.LookupIP(host); err != nil {
@@ -110,9 +140,7 @@ func getHostIP4(host string) (ipList set.StringSet, err error) {
ipList = set.NewStringSet()
for _, ip := range ips {
if ip.To4() != nil {
ipList.Add(ip.String())
}
ipList.Add(ip.String())
}
return ipList, err
@@ -169,29 +197,32 @@ func sortIPs(ipList []string) []string {
return append(nonIPs, ips...)
}
func getAPIEndpoints(serverAddr string) (apiEndpoints []string) {
host, port := mustSplitHostPort(serverAddr)
func getAPIEndpoints() (apiEndpoints []string) {
var ipList []string
if host == "" {
if globalMinioHost == "" {
ipList = sortIPs(localIP4.ToSlice())
ipList = append(ipList, localIP6.ToSlice()...)
} else {
ipList = []string{host}
ipList = []string{globalMinioHost}
}
for _, ip := range ipList {
apiEndpoints = append(apiEndpoints, fmt.Sprintf("%s://%s:%s", getURLScheme(globalIsSSL), ip, port))
apiEndpoints = append(apiEndpoints, fmt.Sprintf("%s://%s", getURLScheme(globalIsSSL), net.JoinHostPort(ip, globalMinioPort)))
}
return apiEndpoints
}
// isHostIPv4 - helper for validating if the provided arg is an ip address.
func isHostIPv4(ipAddress string) bool {
// isHostIP - helper for validating if the provided arg is an ip address.
func isHostIP(ipAddress string) bool {
host, _, err := net.SplitHostPort(ipAddress)
if err != nil {
host = ipAddress
}
// Strip off IPv6 zone information.
if i := strings.Index(host, "%"); i > -1 {
host = host[:i]
}
return net.ParseIP(host) != nil
}
@@ -291,19 +322,20 @@ func extractHostPort(hostAddr string) (string, string, error) {
// correspond to one of the local IP of the
// current machine
func isLocalHost(host string) (bool, error) {
hostIPs, err := getHostIP4(host)
hostIPs, err := getHostIP(host)
if err != nil {
return false, err
}
// If intersection of two IP sets is not empty, then the host is local host.
isLocal := !localIP4.Intersection(hostIPs).IsEmpty()
return isLocal, nil
// If intersection of two IP sets is not empty, then the host is localhost.
isLocalv4 := !localIP4.Intersection(hostIPs).IsEmpty()
isLocalv6 := !localIP6.Intersection(hostIPs).IsEmpty()
return isLocalv4 || isLocalv6, nil
}
// sameLocalAddrs - returns true if two addresses, even with different
// formats, point to the same machine, e.g:
// ':9000' and 'http://localhost:9000/' will return true
// ':9000' and 'http://localhost:9000/' will return true
func sameLocalAddrs(addr1, addr2 string) (bool, error) {
// Extract host & port from given parameters
@@ -355,6 +387,11 @@ func CheckLocalServerAddr(serverAddr string) error {
return uiErrInvalidAddressFlag(err)
}
// Strip off IPv6 zone information.
if i := strings.Index(host, "%"); i > -1 {
host = host[:i]
}
// Check whether port is a valid port number.
p, err := strconv.Atoi(port)
if err != nil {
@@ -366,7 +403,7 @@ func CheckLocalServerAddr(serverAddr string) error {
// 0.0.0.0 is a wildcard address and refers to local network
// addresses. I.e, 0.0.0.0:9000 like ":9000" refers to port
// 9000 on localhost.
if host != "" && host != net.IPv4zero.String() {
if host != "" && host != net.IPv4zero.String() && host != net.IPv6zero.String() {
isLocalHost, err := isLocalHost(host)
if err != nil {
return err
+14 -8
View File
@@ -138,7 +138,7 @@ func TestGetHostIP(t *testing.T) {
}
for _, testCase := range testCases {
ipList, err := getHostIP4(testCase.host)
ipList, err := getHostIP(testCase.host)
if testCase.expectedErr == nil {
if err != nil {
t.Fatalf("error: expected = <nil>, got = %v", err)
@@ -157,17 +157,22 @@ func TestGetHostIP(t *testing.T) {
// Tests finalize api endpoints.
func TestGetAPIEndpoints(t *testing.T) {
host, port := globalMinioHost, globalMinioAddr
defer func() {
globalMinioHost, globalMinioAddr = host, port
}()
testCases := []struct {
serverAddr string
host, port string
expectedResult string
}{
{":80", "http://127.0.0.1:80"},
{"127.0.0.1:80", "http://127.0.0.1:80"},
{"localhost:80", "http://localhost:80"},
{"", "80", "http://127.0.0.1:80"},
{"127.0.0.1", "80", "http://127.0.0.1:80"},
{"localhost", "80", "http://localhost:80"},
}
for i, testCase := range testCases {
apiEndpoints := getAPIEndpoints(testCase.serverAddr)
globalMinioHost, globalMinioPort = testCase.host, testCase.port
apiEndpoints := getAPIEndpoints()
apiEndpointSet := set.CreateStringSet(apiEndpoints...)
if !apiEndpointSet.Contains(testCase.expectedResult) {
t.Fatalf("test %d: expected: Found, got: Not Found", i+1)
@@ -330,7 +335,7 @@ func TestSameLocalAddrs(t *testing.T) {
}
}
}
func TestIsHostIPv4(t *testing.T) {
func TestIsHostIP(t *testing.T) {
testCases := []struct {
args string
expectedResult bool
@@ -341,10 +346,11 @@ func TestIsHostIPv4(t *testing.T) {
{"http://192.168.1.0", false},
{"http://192.168.1.0:9000", false},
{"192.168.1.0", true},
{"[2001:3984:3989::20%eth0]:9000", true},
}
for _, testCase := range testCases {
ret := isHostIPv4(testCase.args)
ret := isHostIP(testCase.args)
if testCase.expectedResult != ret {
t.Fatalf("expected: %v , got: %v", testCase.expectedResult, ret)
}
+65 -3
View File
@@ -22,6 +22,7 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"net"
"net/url"
"path"
"strings"
@@ -89,6 +90,60 @@ func (sys *NotificationSys) DeleteBucket(ctx context.Context, bucketName string)
}()
}
// ReloadFormat - calls ReloadFormat RPC call on all peers.
func (sys *NotificationSys) ReloadFormat(dryRun bool) map[xnet.Host]error {
errors := make(map[xnet.Host]error)
var wg sync.WaitGroup
for addr, client := range sys.peerRPCClientMap {
wg.Add(1)
go func(addr xnet.Host, client *PeerRPCClient) {
defer wg.Done()
// Try to load format in three attempts, before giving up.
for i := 0; i < 3; i++ {
err := client.ReloadFormat(dryRun)
if err == nil {
break
}
errors[addr] = err
// Wait for one second and no need wait after last attempt.
if i < 2 {
time.Sleep(1 * time.Second)
}
}
}(addr, client)
}
wg.Wait()
return errors
}
// LoadUsers - calls LoadUsers RPC call on all peers.
func (sys *NotificationSys) LoadUsers() map[xnet.Host]error {
errors := make(map[xnet.Host]error)
var wg sync.WaitGroup
for addr, client := range sys.peerRPCClientMap {
wg.Add(1)
go func(addr xnet.Host, client *PeerRPCClient) {
defer wg.Done()
// Try to load users in three attempts.
for i := 0; i < 3; i++ {
err := client.LoadUsers()
if err == nil {
break
}
errors[addr] = err
// Wait for one second and no need wait after last attempt.
if i < 2 {
time.Sleep(1 * time.Second)
}
}
}(addr, client)
}
wg.Wait()
return errors
}
// LoadCredentials - calls LoadCredentials RPC call on all peers.
func (sys *NotificationSys) LoadCredentials() map[xnet.Host]error {
errors := make(map[xnet.Host]error)
@@ -97,7 +152,7 @@ func (sys *NotificationSys) LoadCredentials() map[xnet.Host]error {
wg.Add(1)
go func(addr xnet.Host, client *PeerRPCClient) {
defer wg.Done()
// Try to set credentials in three attempts.
// Try to load credentials in three attempts.
for i := 0; i < 3; i++ {
err := client.LoadCredentials()
if err == nil {
@@ -488,10 +543,10 @@ func (args eventArgs) ToEvent() event.Event {
host := globalMinioHost
if host == "" {
// FIXME: Send FQDN or hostname of this machine than sending IP address.
host = localIP4.ToSlice()[0]
host = sortIPs(localIP4.ToSlice())[0]
}
return fmt.Sprintf("%s://%s:%s", getURLScheme(globalIsSSL), host, globalMinioPort)
return fmt.Sprintf("%s://%s", getURLScheme(globalIsSSL), net.JoinHostPort(host, globalMinioPort))
}
eventTime := UTCNow()
@@ -501,6 +556,10 @@ func (args eventArgs) ToEvent() event.Event {
"x-amz-request-id": args.RespElements["requestId"],
"x-minio-origin-endpoint": getOriginEndpoint(), // Minio specific custom elements.
}
// Add deployment as part of
if globalDeploymentID != "" {
respElements["x-minio-deployment-id"] = globalDeploymentID
}
if args.RespElements["content-length"] != "" {
respElements["content-length"] = args.RespElements["content-length"]
}
@@ -537,6 +596,9 @@ func (args eventArgs) ToEvent() event.Event {
if args.EventName != event.ObjectRemovedDelete {
newEvent.S3.Object.ETag = args.Object.ETag
newEvent.S3.Object.Size = args.Object.Size
if args.Object.IsCompressed() {
newEvent.S3.Object.Size = args.Object.GetActualSize()
}
newEvent.S3.Object.ContentType = args.Object.ContentType
newEvent.S3.Object.UserMetadata = args.Object.UserDefined
}
+1 -1
View File
@@ -47,7 +47,7 @@ func toObjectErr(err error, params ...string) error {
Object: params[1],
}
}
case errIsNotRegular, errFileAccessDenied:
case errIsNotRegular:
if len(params) >= 2 {
err = ObjectExistsAsDirectory{
Bucket: params[0],
+7 -3
View File
@@ -23,6 +23,7 @@ import (
"fmt"
"io"
"math/rand"
"net"
"net/http"
"path"
"runtime"
@@ -299,11 +300,11 @@ func getHostsSlice(records []dns.SrvRecord) []string {
return hosts
}
// returns a random host (and corresponding port) from a slice of DNS records
func getRandomHostPort(records []dns.SrvRecord) (string, int) {
// returns a host (and corresponding port) from a slice of DNS records
func getHostFromSrv(records []dns.SrvRecord) string {
rand.Seed(time.Now().Unix())
srvRecord := records[rand.Intn(len(records))]
return srvRecord.Host, srvRecord.Port
return net.JoinHostPort(srvRecord.Host, fmt.Sprintf("%d", srvRecord.Port))
}
// IsCompressed returns true if the object is marked as compressed.
@@ -502,6 +503,9 @@ func NewGetObjectReader(rs *HTTPRangeSpec, oi ObjectInfo, cleanUpFns ...func())
return nil, err
}
// Decrypt the ETag before top layer consumes this value.
oi.ETag = getDecryptedETag(h, oi, copySource)
// Apply the skipLen and limit on the
// decrypted stream
decReader = io.LimitReader(ioutil.NewSkipReader(decReader, skipLen), decRangeLength)
+176 -66
View File
@@ -91,6 +91,10 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
return
}
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
writeErrorResponse(w, ErrBadRequest, r.URL, guessIsBrowserReq(r))
return
}
vars := mux.Vars(r)
bucket := vars["bucket"]
object := vars["object"]
@@ -218,11 +222,13 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
writeErrorResponse(w, ErrInvalidFileHeaderInfo, r.URL, guessIsBrowserReq(r))
return
}
if selectReq.OutputSerialization.CSV.QuoteFields != s3select.CSVQuoteFieldsAlways &&
selectReq.OutputSerialization.CSV.QuoteFields != s3select.CSVQuoteFieldsAsNeeded &&
selectReq.OutputSerialization.CSV.QuoteFields != "" {
writeErrorResponse(w, ErrInvalidQuoteFields, r.URL, guessIsBrowserReq(r))
return
if selectReq.OutputSerialization.CSV != nil {
if selectReq.OutputSerialization.CSV.QuoteFields != s3select.CSVQuoteFieldsAlways &&
selectReq.OutputSerialization.CSV.QuoteFields != s3select.CSVQuoteFieldsAsNeeded &&
selectReq.OutputSerialization.CSV.QuoteFields != "" {
writeErrorResponse(w, ErrInvalidQuoteFields, r.URL, guessIsBrowserReq(r))
return
}
}
if len(selectReq.InputSerialization.CSV.RecordDelimiter) > 2 {
writeErrorResponse(w, ErrInvalidRequestParameter, r.URL, guessIsBrowserReq(r))
@@ -316,11 +322,19 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
writeErrorResponse(w, ErrBadRequest, r.URL, guessIsBrowserReq(r))
return
}
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
writeErrorResponse(w, ErrBadRequest, r.URL, guessIsBrowserReq(r))
return
}
vars := mux.Vars(r)
bucket := vars["bucket"]
object := vars["object"]
if vid := r.URL.Query().Get("versionId"); vid != "" && vid != "null" {
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
return
}
var (
opts ObjectOptions
err error
@@ -481,14 +495,22 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
return
}
if crypto.S3.IsRequested(r.Header) || crypto.S3KMS.IsRequested(r.Header) { // If SSE-S3 or SSE-KMS present -> AWS fails with undefined error
writeErrorResponseHeadersOnly(w, ErrBadRequest)
return
}
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
writeErrorResponse(w, ErrBadRequest, r.URL, guessIsBrowserReq(r))
return
}
vars := mux.Vars(r)
bucket := vars["bucket"]
object := vars["object"]
if vid := r.URL.Query().Get("versionId"); vid != "" && vid != "null" {
writeErrorResponseHeadersOnly(w, ErrNoSuchVersion)
return
}
getObjectInfo := objectAPI.GetObjectInfo
if api.CacheAPI() != nil {
getObjectInfo = api.CacheAPI().GetObjectInfo
@@ -646,11 +668,25 @@ func getCpObjMetadataFromHeader(ctx context.Context, r *http.Request, userMeta m
// Returns a minio-go Client configured to access remote host described by destDNSRecord
// Applicable only in a federated deployment
var getRemoteInstanceClient = func(r *http.Request, host string, port int) (*miniogo.Core, error) {
// In a federated deployment, all the instances share config files and hence expected to have same
// credentials, make sure to send the same credentials for which the request came in.
var getRemoteInstanceClient = func(r *http.Request, host string) (*miniogo.Core, error) {
cred := getReqAccessCred(r, globalServerConfig.GetRegion())
return miniogo.NewCore(net.JoinHostPort(host, strconv.Itoa(port)), cred.AccessKey, cred.SecretKey, globalIsSSL)
// In a federated deployment, all the instances share config files
// and hence expected to have same credentials.
core, err := miniogo.NewCore(host, cred.AccessKey, cred.SecretKey, globalIsSSL)
if err != nil {
return nil, err
}
core.SetCustomTransport(NewCustomHTTPTransport())
return core, nil
}
// Check if the bucket is on a remote site, this code only gets executed when federation is enabled.
var isRemoteCallRequired = func(ctx context.Context, bucket string, objAPI ObjectLayer) bool {
if globalDNSConfig == nil {
return false
}
_, err := objAPI.GetBucketInfo(ctx, bucket)
return err == toObjectErr(errVolumeNotFound, bucket)
}
// CopyObjectHandler - Copy Object
@@ -676,7 +712,10 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
return
}
if !objectAPI.IsEncryptionSupported() && (hasServerSideEncryptionHeader(r.Header) || crypto.SSECopy.IsRequested(r.Header)) {
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
return
}
vars := mux.Vars(r)
dstBucket := vars["bucket"]
dstObject := vars["object"]
@@ -695,6 +734,22 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
cpSrcPath = r.Header.Get("X-Amz-Copy-Source")
}
// Check https://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectVersioning.html
// Regardless of whether you have enabled versioning, each object in your bucket
// has a version ID. If you have not enabled versioning, Amazon S3 sets the value
// of the version ID to null. If you have enabled versioning, Amazon S3 assigns a
// unique version ID value for the object.
if u, err := url.Parse(cpSrcPath); err == nil {
// Check if versionId query param was added, if yes then check if
// its non "null" value, we should error out since we do not support
// any versions other than "null".
if vid := u.Query().Get("versionId"); vid != "" && vid != "null" {
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
return
}
cpSrcPath = u.Path
}
srcBucket, srcObject := path2BucketAndObject(cpSrcPath)
// If source object is empty or bucket is empty, reply back invalid copy source.
if srcObject == "" || srcBucket == "" {
@@ -761,17 +816,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
srcInfo.metadataOnly = true
}
// Checks if a remote putobject call is needed for CopyObject operation
// 1. If source and destination bucket names are same, it means no call needed to etcd to get destination info
// 2. If destination bucket doesn't exist locally, only then a etcd call is needed
var isRemoteCallRequired = func(ctx context.Context, src, dst string, objAPI ObjectLayer) bool {
if src == dst {
return false
}
_, berr := objAPI.GetBucketInfo(ctx, dst)
return berr == toObjectErr(errVolumeNotFound, dst)
}
var reader io.Reader
var length = srcInfo.Size
@@ -786,13 +830,32 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
length = actualSize
}
// Check if the destination bucket is on a remote site, this code only gets executed
// when federation is enabled, ie when globalDNSConfig is non 'nil'.
//
// This function is similar to isRemoteCallRequired but specifically for COPY object API
// if destination and source are same we do not need to check for destnation bucket
// to exist locally.
var isRemoteCopyRequired = func(ctx context.Context, srcBucket, dstBucket string, objAPI ObjectLayer) bool {
if globalDNSConfig == nil {
return false
}
if srcBucket == dstBucket {
return false
}
_, err := objAPI.GetBucketInfo(ctx, dstBucket)
return err == toObjectErr(errVolumeNotFound, dstBucket)
}
var compressMetadata map[string]string
// No need to compress for remote etcd calls
// Pass the decompressed stream to such calls.
isCompressed := objectAPI.IsCompressionSupported() && isCompressible(r.Header, srcObject) && !isRemoteCallRequired(ctx, srcBucket, dstBucket, objectAPI)
isCompressed := objectAPI.IsCompressionSupported() && isCompressible(r.Header, srcObject) && !isRemoteCopyRequired(ctx, srcBucket, dstBucket, objectAPI)
if isCompressed {
// Storing the compression metadata.
srcInfo.UserDefined[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV1
srcInfo.UserDefined[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(actualSize, 10)
compressMetadata = make(map[string]string, 2)
// Preserving the compression metadata.
compressMetadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV1
compressMetadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(actualSize, 10)
// Remove all source encrypted related metadata to
// avoid copying them in target object.
crypto.RemoveInternalEntries(srcInfo.UserDefined)
@@ -827,6 +890,9 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
rawReader := srcInfo.Reader
pReader := NewPutObjReader(srcInfo.Reader, nil, nil)
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
}
var encMetadata = make(map[string]string)
if objectAPI.IsEncryptionSupported() && !isCompressed {
// Encryption parameters not applicable for this object.
@@ -869,8 +935,11 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
return
}
}
for k, v := range srcInfo.UserDefined {
encMetadata[k] = v
if hasPrefix(k, ReservedMetadataPrefix) {
encMetadata[k] = v
}
}
// In case of SSE-S3 oldKey and newKey aren't used - the KMS manages the keys.
@@ -935,6 +1004,11 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
return
}
// Store the preserved compression metadata.
for k, v := range compressMetadata {
srcInfo.UserDefined[k] = v
}
// We need to preserve the encryption headers set in EncryptRequest,
// so we do not want to override them, copy them instead.
for k, v := range encMetadata {
@@ -958,29 +1032,28 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
var objInfo ObjectInfo
if isRemoteCallRequired(ctx, srcBucket, dstBucket, objectAPI) {
if globalDNSConfig == nil {
writeErrorResponse(w, ErrNoSuchBucket, r.URL, guessIsBrowserReq(r))
if isRemoteCopyRequired(ctx, srcBucket, dstBucket, objectAPI) {
var dstRecords []dns.SrvRecord
dstRecords, err = globalDNSConfig.Get(dstBucket)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
var dstRecords []dns.SrvRecord
if dstRecords, err = globalDNSConfig.Get(dstBucket); err == nil {
// Send PutObject request to appropriate instance (in federated deployment)
host, port := getRandomHostPort(dstRecords)
client, rerr := getRemoteInstanceClient(r, host, port)
if rerr != nil {
writeErrorResponse(w, toAPIErrorCode(ctx, rerr), r.URL, guessIsBrowserReq(r))
return
}
remoteObjInfo, rerr := client.PutObject(dstBucket, dstObject, srcInfo.Reader,
srcInfo.Size, "", "", srcInfo.UserDefined, dstOpts.ServerSideEncryption)
if rerr != nil {
writeErrorResponse(w, toAPIErrorCode(ctx, rerr), r.URL, guessIsBrowserReq(r))
return
}
objInfo.ETag = remoteObjInfo.ETag
objInfo.ModTime = remoteObjInfo.LastModified
// Send PutObject request to appropriate instance (in federated deployment)
client, rerr := getRemoteInstanceClient(r, getHostFromSrv(dstRecords))
if rerr != nil {
writeErrorResponse(w, toAPIErrorCode(ctx, rerr), r.URL, guessIsBrowserReq(r))
return
}
remoteObjInfo, rerr := client.PutObject(dstBucket, dstObject, srcInfo.Reader,
srcInfo.Size, "", "", srcInfo.UserDefined, dstOpts.ServerSideEncryption)
if rerr != nil {
writeErrorResponse(w, toAPIErrorCode(ctx, rerr), r.URL, guessIsBrowserReq(r))
return
}
objInfo.ETag = remoteObjInfo.ETag
objInfo.ModTime = remoteObjInfo.LastModified
} else {
// Copy source object to destination, if source and destination
// object is same then only metadata is updated.
@@ -1042,7 +1115,10 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
return
}
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
return
}
vars := mux.Vars(r)
bucket := vars["bucket"]
object := vars["object"]
@@ -1209,6 +1285,9 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
return
}
}
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
}
var objectEncryptionKey []byte
if objectAPI.IsEncryptionSupported() {
if hasServerSideEncryptionHeader(r.Header) && !hasSuffix(object, slashSeparator) { // handle SSE requests
@@ -1241,15 +1320,15 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
return
}
etag := objInfo.ETag
if objInfo.IsCompressed() {
// Ignore compressed ETag.
objInfo.ETag = objInfo.ETag + "-1"
}
if hasServerSideEncryptionHeader(r.Header) {
w.Header().Set("ETag", "\""+getDecryptedETag(r.Header, objInfo, false)+"\"")
} else {
w.Header().Set("ETag", "\""+objInfo.ETag+"\"")
etag = objInfo.ETag + "-1"
} else if hasServerSideEncryptionHeader(r.Header) {
etag = getDecryptedETag(r.Header, objInfo, false)
}
w.Header().Set("ETag", "\""+etag+"\"")
if objectAPI.IsEncryptionSupported() {
if crypto.IsEncrypted(objInfo.UserDefined) {
switch {
@@ -1305,7 +1384,10 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
return
}
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
return
}
vars := mux.Vars(r)
bucket := vars["bucket"]
object := vars["object"]
@@ -1337,6 +1419,9 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
var encMetadata = map[string]string{}
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
}
if objectAPI.IsEncryptionSupported() {
if hasServerSideEncryptionHeader(r.Header) {
if err = setEncryptionMetadata(r, bucket, object, encMetadata); err != nil {
@@ -1402,6 +1487,10 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
return
}
if !objectAPI.IsEncryptionSupported() && (hasServerSideEncryptionHeader(r.Header) || crypto.SSECopy.IsRequested(r.Header)) {
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
return
}
vars := mux.Vars(r)
dstBucket := vars["bucket"]
@@ -1419,6 +1508,22 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
cpSrcPath = r.Header.Get("X-Amz-Copy-Source")
}
// Check https://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectVersioning.html
// Regardless of whether you have enabled versioning, each object in your bucket
// has a version ID. If you have not enabled versioning, Amazon S3 sets the value
// of the version ID to null. If you have enabled versioning, Amazon S3 assigns a
// unique version ID value for the object.
if u, err := url.Parse(cpSrcPath); err == nil {
// Check if versionId query param was added, if yes then check if
// its non "null" value, we should error out since we do not support
// any versions other than "null".
if vid := u.Query().Get("versionId"); vid != "" && vid != "null" {
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
return
}
cpSrcPath = u.Path
}
srcBucket, srcObject := path2BucketAndObject(cpSrcPath)
// If source object is empty or bucket is empty, reply back invalid copy source.
if srcObject == "" || srcBucket == "" {
@@ -1646,7 +1751,10 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
return
}
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
return
}
vars := mux.Vars(r)
bucket := vars["bucket"]
object := vars["object"]
@@ -1865,19 +1973,16 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
etag := partInfo.ETag
if isCompressed {
pipeWriter.Close()
// Suppress compressed ETag.
partInfo.ETag = partInfo.ETag + "-1"
}
if partInfo.ETag != "" {
if isEncrypted {
w.Header().Set("ETag", "\""+tryDecryptETag(objectEncryptionKey, partInfo.ETag, crypto.SSEC.IsRequested(r.Header))+"\"")
} else {
w.Header().Set("ETag", "\""+partInfo.ETag+"\"")
}
etag = partInfo.ETag + "-1"
} else if isEncrypted {
etag = tryDecryptETag(objectEncryptionKey, partInfo.ETag, crypto.SSEC.IsRequested(r.Header))
}
w.Header().Set("ETag", "\""+etag+"\"")
writeSuccessResponseHeadersOnly(w)
}
@@ -2207,6 +2312,11 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
return
}
if vid := r.URL.Query().Get("versionId"); vid != "" && vid != "null" {
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
return
}
// Deny if WORM is enabled
if globalWORMEnabled {
// Not required to check whether given object exists or not, because
+19 -1
View File
@@ -115,12 +115,30 @@ func (rpcClient *PeerRPCClient) SendEvent(bucketName string, targetID, remoteTar
return err
}
// ReloadFormat - calls reload format RPC.
func (rpcClient *PeerRPCClient) ReloadFormat(dryRun bool) error {
args := ReloadFormatArgs{
DryRun: dryRun,
}
reply := VoidReply{}
return rpcClient.Call(peerServiceName+".ReloadFormat", &args, &reply)
}
// LoadUsers - calls load users RPC.
func (rpcClient *PeerRPCClient) LoadUsers() error {
args := AuthArgs{}
reply := VoidReply{}
return rpcClient.Call(peerServiceName+".LoadUsers", &args, &reply)
}
// LoadCredentials - calls load credentials RPC.
func (rpcClient *PeerRPCClient) LoadCredentials() error {
args := AuthArgs{}
reply := VoidReply{}
return rpcClient.Call(peerServiceName+".SetCredentials", &args, &reply)
return rpcClient.Call(peerServiceName+".LoadCredentials", &args, &reply)
}
// NewPeerRPCClient - returns new peer RPC client.
+24
View File
@@ -192,6 +192,30 @@ func (receiver *peerRPCReceiver) SendEvent(args *SendEventArgs, reply *bool) err
return nil
}
// ReloadFormatArgs - send event RPC arguments.
type ReloadFormatArgs struct {
AuthArgs
DryRun bool
}
// ReloadFormat - handles reload format RPC call, reloads latest `format.json`
func (receiver *peerRPCReceiver) ReloadFormat(args *ReloadFormatArgs, reply *VoidReply) error {
objAPI := newObjectLayerFn()
if objAPI == nil {
return errServerNotInitialized
}
return objAPI.ReloadFormat(context.Background(), args.DryRun)
}
// LoadUsers - handles load users RPC call.
func (receiver *peerRPCReceiver) LoadUsers(args *AuthArgs, reply *VoidReply) error {
objAPI := newObjectLayerFn()
if objAPI == nil {
return errServerNotInitialized
}
return globalIAMSys.Load(objAPI)
}
// LoadCredentials - handles load credentials RPC call.
func (receiver *peerRPCReceiver) LoadCredentials(args *AuthArgs, reply *VoidReply) error {
objAPI := newObjectLayerFn()
+18 -2
View File
@@ -71,6 +71,7 @@ type posix struct {
connected bool
diskMount bool // indicates if the path is an actual mount.
driveSync bool // indicates if the backend is synchronous.
// Disk usage metrics
stopUsageCh chan struct{}
@@ -191,6 +192,15 @@ func newPosix(path string) (*posix, error) {
diskMount: mountinfo.IsLikelyMountPoint(path),
}
var pf BoolFlag
if driveSync := os.Getenv("MINIO_DRIVE_SYNC"); driveSync != "" {
pf, err = ParseBoolFlag(driveSync)
if err != nil {
return nil, err
}
p.driveSync = bool(pf)
}
if !p.diskMount {
go p.diskUsage(globalServiceDoneCh)
}
@@ -1013,8 +1023,14 @@ func (s *posix) AppendFile(volume, path string, buf []byte) (err error) {
return errFaultyDisk
}
// Create file if not found
w, err := s.openFile(volume, path, os.O_CREATE|os.O_APPEND|os.O_WRONLY)
var w *os.File
// Create file if not found, additionally also enables synchronous
// operation if asked by the user.
if s.driveSync {
w, err = s.openFile(volume, path, os.O_CREATE|os.O_SYNC|os.O_APPEND|os.O_WRONLY)
} else {
w, err = s.openFile(volume, path, os.O_CREATE|os.O_APPEND|os.O_WRONLY)
}
if err != nil {
return err
}
+2 -2
View File
@@ -107,8 +107,8 @@ func configureServerHandler(endpoints EndpointList) (http.Handler, error) {
// Add Admin RPC router
registerAdminRPCRouter(router)
// Add Admin router.
registerAdminRouter(router)
// Add Admin router, all APIs are enabled in server mode.
registerAdminRouter(router, true)
// Add healthcheck router
registerHealthCheckRouter(router)
+10 -22
View File
@@ -18,6 +18,7 @@ package cmd
import (
"context"
"errors"
"fmt"
"net/http"
"os"
@@ -40,7 +41,7 @@ func init() {
var serverFlags = []cli.Flag{
cli.StringFlag{
Name: "address",
Value: ":" + globalMinioPort,
Value: ":" + globalMinioDefaultPort,
Usage: "bind to a specific ADDRESS:PORT, ADDRESS can be an IP or hostname",
},
}
@@ -147,9 +148,7 @@ func serverHandleCmdArgs(ctx *cli.Context) {
// Handle common command args.
handleCommonCmdArgs(ctx)
// Server address.
serverAddr := ctx.String("address")
logger.FatalIf(CheckLocalServerAddr(serverAddr), "Unable to validate passed arguments")
logger.FatalIf(CheckLocalServerAddr(globalCLIContext.Addr), "Unable to validate passed arguments")
var setupType SetupType
var err error
@@ -162,9 +161,9 @@ func serverHandleCmdArgs(ctx *cli.Context) {
endpoints := strings.Fields(os.Getenv("MINIO_ENDPOINTS"))
if len(endpoints) > 0 {
globalMinioAddr, globalEndpoints, setupType, globalXLSetCount, globalXLSetDriveCount, err = createServerEndpoints(serverAddr, endpoints...)
globalMinioAddr, globalEndpoints, setupType, globalXLSetCount, globalXLSetDriveCount, err = createServerEndpoints(globalCLIContext.Addr, endpoints...)
} else {
globalMinioAddr, globalEndpoints, setupType, globalXLSetCount, globalXLSetDriveCount, err = createServerEndpoints(serverAddr, ctx.Args()...)
globalMinioAddr, globalEndpoints, setupType, globalXLSetCount, globalXLSetDriveCount, err = createServerEndpoints(globalCLIContext.Addr, ctx.Args()...)
}
logger.FatalIf(err, "Invalid command line arguments")
@@ -205,19 +204,6 @@ func serverMain(ctx *cli.Context) {
// error during initialization will be shown as a fatal message
logger.Disable = true
// Get "json" flag from command line argument and
// enable json and quite modes if jason flag is turned on.
jsonFlag := ctx.IsSet("json") || ctx.GlobalIsSet("json")
if jsonFlag {
logger.EnableJSON()
}
// Get quiet flag from command line argument.
quietFlag := ctx.IsSet("quiet") || ctx.GlobalIsSet("quiet")
if quietFlag {
logger.EnableQuiet()
}
// Handle all server command args.
serverHandleCmdArgs(ctx)
@@ -246,7 +232,7 @@ func serverMain(ctx *cli.Context) {
}
}
if !quietFlag {
if !globalCLIContext.Quiet {
// Check for new updates from dl.minio.io.
mode := globalMinioModeFS
if globalIsDistXL {
@@ -383,14 +369,16 @@ func serverMain(ctx *cli.Context) {
if err = globalNotificationSys.Init(newObject); err != nil {
logger.LogIf(context.Background(), err)
}
if globalAutoEncryption && !newObject.IsEncryptionSupported() {
logger.Fatal(errors.New("Invalid KMS configuration"), "auto-encryption is enabled but server does not support encryption")
}
globalObjLayerMutex.Lock()
globalObjectAPI = newObject
globalObjLayerMutex.Unlock()
// Prints the formatted startup message once object layer is initialized.
apiEndpoints := getAPIEndpoints(globalMinioAddr)
printStartupMessage(apiEndpoints)
printStartupMessage(getAPIEndpoints())
// Set uptime time after object layer has initialized.
globalBootTime = UTCNow()
+21 -14
View File
@@ -20,12 +20,13 @@ import (
"context"
"crypto/x509"
"fmt"
"net/url"
"net"
"runtime"
"strings"
humanize "github.com/dustin/go-humanize"
"github.com/minio/minio/cmd/logger"
xnet "github.com/minio/minio/pkg/net"
)
// Documentation links, these are part of message printing code.
@@ -76,6 +77,19 @@ func printStartupMessage(apiEndPoints []string) {
}
}
// Returns true if input is not IPv4, false if it is.
func isNotIPv4(host string) bool {
h, _, err := net.SplitHostPort(host)
if err != nil {
h = host
}
ip := net.ParseIP(h)
ok := ip.To4() != nil // This is always true of IP is IPv4
// Returns true if input is not IPv4.
return !ok
}
// strip api endpoints list with standard ports such as
// port "80" and "443" before displaying on the startup
// banner. Returns a new list of API endpoints.
@@ -83,23 +97,16 @@ func stripStandardPorts(apiEndpoints []string) (newAPIEndpoints []string) {
newAPIEndpoints = make([]string, len(apiEndpoints))
// Check all API endpoints for standard ports and strip them.
for i, apiEndpoint := range apiEndpoints {
url, err := url.Parse(apiEndpoint)
u, err := xnet.ParseURL(apiEndpoint)
if err != nil {
newAPIEndpoints[i] = apiEndpoint
continue
}
host, port := mustSplitHostPort(url.Host)
// For standard HTTP(s) ports such as "80" and "443"
// apiEndpoints should only be host without port.
switch {
case url.Scheme == "http" && port == "80":
fallthrough
case url.Scheme == "https" && port == "443":
url.Host = host
newAPIEndpoints[i] = url.String()
default:
newAPIEndpoints[i] = apiEndpoint
if globalMinioHost == "" && isNotIPv4(u.Host) {
// Skip all non-IPv4 endpoints when we bind to all interfaces.
continue
}
newAPIEndpoints[i] = u.String()
}
return newAPIEndpoints
}
@@ -116,7 +123,7 @@ func printServerCommonMsg(apiEndpoints []string) {
// Colorize the message and print.
logger.StartupMessage(colorBlue("Endpoint: ") + colorBold(fmt.Sprintf(getFormatStr(len(apiEndpointStr), 1), apiEndpointStr)))
if isTerminal() {
if isTerminal() && !globalCLIContext.Anonymous {
logger.StartupMessage(colorBlue("AccessKey: ") + colorBold(fmt.Sprintf("%s ", cred.AccessKey)))
logger.StartupMessage(colorBlue("SecretKey: ") + colorBold(fmt.Sprintf("%s ", cred.SecretKey)))
if region != "" {
+1 -1
View File
@@ -2319,7 +2319,7 @@ func getEndpointsLocalAddr(endpoints EndpointList) string {
}
}
return globalMinioHost + ":" + globalMinioPort
return net.JoinHostPort(globalMinioHost, globalMinioPort)
}
// fetches a random number between range min-max.
+8
View File
@@ -105,6 +105,12 @@ const (
httpsScheme = "https"
)
// nopCharsetConverter is a dummy charset convert which just copies input to output,
// it is used to ignore custom encoding charset in S3 XML body.
func nopCharsetConverter(label string, input io.Reader) (io.Reader, error) {
return input, nil
}
// xmlDecoder provide decoded value in xml.
func xmlDecoder(body io.Reader, v interface{}, size int64) error {
var lbody io.Reader
@@ -114,6 +120,8 @@ func xmlDecoder(body io.Reader, v interface{}, size int64) error {
lbody = body
}
d := xml.NewDecoder(lbody)
// Ignore any encoding set in the XML body
d.CharsetReader = nopCharsetConverter
return d.Decode(v)
}
+392 -159
View File
@@ -99,11 +99,14 @@ func (web *webAPIHandlers) ServerInfo(r *http.Request, args *WebGenericArgs, rep
reply.MinioGlobalInfo = getGlobalInfo()
// If ENV creds are not set and incoming user is not owner
// disable changing credentials.
// TODO: fix this in future and allow changing user credentials.
v, ok := reply.MinioGlobalInfo["isEnvCreds"].(bool)
if ok && !v {
reply.MinioGlobalInfo["isEnvCreds"] = !owner
}
// if etcd is set disallow changing credentials through UI
if globalEtcdClient != nil {
reply.MinioGlobalInfo["isEnvCreds"] = true
}
reply.MinioMemory = mem
reply.MinioPlatform = platform
@@ -148,7 +151,7 @@ func (web *webAPIHandlers) MakeBucket(r *http.Request, args *MakeBucketArgs, rep
if authErr != nil {
return toJSONError(authErr)
}
// TODO: Allow MakeBucket in future.
if !owner {
return toJSONError(errAccessDenied)
}
@@ -201,11 +204,33 @@ func (web *webAPIHandlers) DeleteBucket(r *http.Request, args *RemoveBucketArgs,
if authErr != nil {
return toJSONError(authErr)
}
// TODO: Allow DeleteBucket in future.
if !owner {
return toJSONError(errAccessDenied)
}
reply.UIVersion = browser.UIVersion
if isRemoteCallRequired(context.Background(), args.BucketName, objectAPI) {
sr, err := globalDNSConfig.Get(args.BucketName)
if err != nil {
if err == dns.ErrNoEntriesFound {
return toJSONError(BucketNotFound{
Bucket: args.BucketName,
}, args.BucketName)
}
return toJSONError(err, args.BucketName)
}
core, err := getRemoteInstanceClient(r, getHostFromSrv(sr))
if err != nil {
return toJSONError(err, args.BucketName)
}
if err = core.RemoveBucket(args.BucketName); err != nil {
return toJSONError(err, args.BucketName)
}
return nil
}
ctx := context.Background()
deleteBucket := objectAPI.DeleteBucket
@@ -229,7 +254,6 @@ func (web *webAPIHandlers) DeleteBucket(r *http.Request, args *RemoveBucketArgs,
}
}
reply.UIVersion = browser.UIVersion
return nil
}
@@ -258,7 +282,8 @@ func (web *webAPIHandlers) ListBuckets(r *http.Request, args *WebGenericArgs, re
listBuckets = web.CacheAPI().ListBuckets
}
if _, _, authErr := webRequestAuthenticate(r); authErr != nil {
claims, owner, authErr := webRequestAuthenticate(r)
if authErr != nil {
return toJSONError(authErr)
}
@@ -270,10 +295,20 @@ func (web *webAPIHandlers) ListBuckets(r *http.Request, args *WebGenericArgs, re
}
for _, dnsRecord := range dnsBuckets {
bucketName := strings.Trim(dnsRecord.Key, "/")
reply.Buckets = append(reply.Buckets, WebBucketInfo{
Name: bucketName,
CreationDate: dnsRecord.CreationDate,
})
if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.Subject,
Action: iampolicy.ListBucketAction,
BucketName: bucketName,
ConditionValues: getConditionValues(r, ""),
IsOwner: owner,
ObjectName: "",
}) {
reply.Buckets = append(reply.Buckets, WebBucketInfo{
Name: bucketName,
CreationDate: dnsRecord.CreationDate,
})
}
}
} else {
buckets, err := listBuckets(context.Background())
@@ -281,10 +316,19 @@ func (web *webAPIHandlers) ListBuckets(r *http.Request, args *WebGenericArgs, re
return toJSONError(err)
}
for _, bucket := range buckets {
reply.Buckets = append(reply.Buckets, WebBucketInfo{
Name: bucket.Name,
CreationDate: bucket.Created,
})
if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.Subject,
Action: iampolicy.ListBucketAction,
BucketName: bucket.Name,
ConditionValues: getConditionValues(r, ""),
IsOwner: owner,
ObjectName: "",
}) {
reply.Buckets = append(reply.Buckets, WebBucketInfo{
Name: bucket.Name,
CreationDate: bucket.Created,
})
}
}
}
@@ -333,16 +377,56 @@ func (web *webAPIHandlers) ListObjects(r *http.Request, args *ListObjectsArgs, r
listObjects = web.CacheAPI().ListObjects
}
if isRemoteCallRequired(context.Background(), args.BucketName, objectAPI) {
sr, err := globalDNSConfig.Get(args.BucketName)
if err != nil {
if err == dns.ErrNoEntriesFound {
return toJSONError(BucketNotFound{
Bucket: args.BucketName,
}, args.BucketName)
}
return toJSONError(err, args.BucketName)
}
core, err := getRemoteInstanceClient(r, getHostFromSrv(sr))
if err != nil {
return toJSONError(err, args.BucketName)
}
result, err := core.ListObjects(args.BucketName, args.Prefix, args.Marker, slashSeparator, 1000)
if err != nil {
return toJSONError(err, args.BucketName)
}
reply.NextMarker = result.NextMarker
reply.IsTruncated = result.IsTruncated
for _, obj := range result.Contents {
reply.Objects = append(reply.Objects, WebObjectInfo{
Key: obj.Key,
LastModified: obj.LastModified,
Size: obj.Size,
ContentType: obj.ContentType,
})
}
for _, p := range result.CommonPrefixes {
reply.Objects = append(reply.Objects, WebObjectInfo{
Key: p.Prefix,
})
}
return nil
}
claims, owner, authErr := webRequestAuthenticate(r)
if authErr != nil {
if authErr == errNoAuthToken {
// Add this for checking ListObjects conditional.
if args.Prefix != "" {
r.Header.Set("prefix", args.Prefix)
}
// Check if anonymous (non-owner) has access to download objects.
readable := globalPolicySys.IsAllowed(policy.Args{
Action: policy.GetObjectAction,
Action: policy.ListBucketAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, ""),
IsOwner: false,
ObjectName: args.Prefix + "/",
})
// Check if anonymous (non-owner) has access to upload objects.
@@ -370,18 +454,22 @@ func (web *webAPIHandlers) ListObjects(r *http.Request, args *ListObjectsArgs, r
// For authenticated users apply IAM policy.
if authErr == nil {
// Add this for checking ListObjects conditional.
if args.Prefix != "" {
r.Header.Set("prefix", args.Prefix)
}
readable := globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.Subject,
Action: iampolicy.Action(policy.GetObjectAction),
Action: iampolicy.ListBucketAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, ""),
IsOwner: owner,
ObjectName: args.Prefix + "/",
})
writable := globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.Subject,
Action: iampolicy.Action(policy.PutObjectAction),
Action: iampolicy.PutObjectAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, ""),
IsOwner: owner,
@@ -465,6 +553,40 @@ func (web *webAPIHandlers) RemoveObject(r *http.Request, args *RemoveObjectArgs,
return toJSONError(errInvalidArgument)
}
reply.UIVersion = browser.UIVersion
if isRemoteCallRequired(context.Background(), args.BucketName, objectAPI) {
sr, err := globalDNSConfig.Get(args.BucketName)
if err != nil {
if err == dns.ErrNoEntriesFound {
return toJSONError(BucketNotFound{
Bucket: args.BucketName,
}, args.BucketName)
}
return toJSONError(err, args.BucketName)
}
core, err := getRemoteInstanceClient(r, getHostFromSrv(sr))
if err != nil {
return toJSONError(err, args.BucketName)
}
objectsCh := make(chan string)
// Send object names that are needed to be removed to objectsCh
go func() {
defer close(objectsCh)
for _, objectName := range args.Objects {
objectsCh <- objectName
}
}()
for resp := range core.RemoveObjects(args.BucketName, objectsCh) {
if resp.Err != nil {
return toJSONError(resp.Err, args.BucketName, resp.ObjectName)
}
}
return nil
}
var err error
next:
for _, objectName := range args.Objects {
@@ -479,7 +601,7 @@ next:
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.Subject,
Action: iampolicy.Action(policy.DeleteObjectAction),
Action: iampolicy.DeleteObjectAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, ""),
IsOwner: owner,
@@ -488,7 +610,7 @@ next:
return toJSONError(errAccessDenied)
}
if err = deleteObject(nil, objectAPI, web.CacheAPI(), args.BucketName, objectName, r); err != nil {
if err = deleteObject(context.Background(), objectAPI, web.CacheAPI(), args.BucketName, objectName, r); err != nil {
break next
}
continue
@@ -496,7 +618,7 @@ next:
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.Subject,
Action: iampolicy.Action(policy.DeleteObjectAction),
Action: iampolicy.DeleteObjectAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, ""),
IsOwner: owner,
@@ -515,7 +637,7 @@ next:
}
marker = lo.NextMarker
for _, obj := range lo.Objects {
err = deleteObject(nil, objectAPI, web.CacheAPI(), args.BucketName, obj.Name, r)
err = deleteObject(context.Background(), objectAPI, web.CacheAPI(), args.BucketName, obj.Name, r)
if err != nil {
break next
}
@@ -531,7 +653,6 @@ next:
return toJSONError(err, args.BucketName, "")
}
reply.UIVersion = browser.UIVersion
return nil
}
@@ -605,8 +726,7 @@ func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *Se
}
// If creds are set through ENV disallow changing credentials.
// TODO: Multi-user credentials also cannot be changed from browser.
if globalIsEnvCreds || globalWORMEnabled || !owner {
if globalIsEnvCreds || globalWORMEnabled || !owner || globalEtcdClient != nil {
return toJSONError(errChangeCredNotAllowed)
}
@@ -714,11 +834,6 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
writeWebErrorResponse(w, errServerNotInitialized)
return
}
putObject := objectAPI.PutObject
if web.CacheAPI() != nil {
putObject = web.CacheAPI().PutObject
}
vars := mux.Vars(r)
bucket := vars["bucket"]
object := vars["object"]
@@ -747,7 +862,7 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
if authErr == nil {
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.Subject,
Action: iampolicy.Action(policy.PutObjectAction),
Action: iampolicy.PutObjectAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, ""),
IsOwner: owner,
@@ -757,6 +872,9 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
return
}
}
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
}
// Require Content-Length to be set in the request
size := r.ContentLength
@@ -772,9 +890,15 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
return
}
reader := r.Body
var pReader *PutObjReader
var reader io.Reader = r.Body
actualSize := size
hashReader, err := hash.NewReader(reader, size, "", "", actualSize)
if err != nil {
writeWebErrorResponse(w, err)
return
}
if objectAPI.IsCompressionSupported() && isCompressible(r.Header, object) && size > 0 {
// Storing the compression metadata.
metadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV1
@@ -800,13 +924,36 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
// Set compression metrics.
size = -1 // Since compressed size is un-predictable.
reader = pipeReader
hashReader, err = hash.NewReader(reader, size, "", "", actualSize)
if err != nil {
writeWebErrorResponse(w, err)
return
}
}
pReader = NewPutObjReader(hashReader, nil, nil)
if objectAPI.IsEncryptionSupported() {
if hasServerSideEncryptionHeader(r.Header) && !hasSuffix(object, slashSeparator) { // handle SSE requests
rawReader := hashReader
var objectEncryptionKey []byte
reader, objectEncryptionKey, err = EncryptRequest(hashReader, r, bucket, object, metadata)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
info := ObjectInfo{Size: size}
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", size) // do not try to verify encrypted content
if err != nil {
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
pReader = NewPutObjReader(rawReader, hashReader, objectEncryptionKey)
}
}
hashReader, err := hash.NewReader(reader, size, "", "", actualSize)
if err != nil {
writeWebErrorResponse(w, err)
return
}
// Ensure that metadata does not contain sensitive information
crypto.RemoveSensitiveEntries(metadata)
var opts ObjectOptions
// Deny if WORM is enabled
if globalWORMEnabled {
@@ -816,11 +963,26 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
}
}
objInfo, err := putObject(ctx, bucket, object, NewPutObjReader(hashReader, nil, nil), metadata, opts)
putObject := objectAPI.PutObject
if !hasServerSideEncryptionHeader(r.Header) && web.CacheAPI() != nil {
putObject = web.CacheAPI().PutObject
}
objInfo, err := putObject(context.Background(), bucket, object, pReader, metadata, opts)
if err != nil {
writeWebErrorResponse(w, err)
return
}
if objectAPI.IsEncryptionSupported() {
if crypto.IsEncrypted(objInfo.UserDefined) {
switch {
case crypto.S3.IsEncrypted(objInfo.UserDefined):
w.Header().Set(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
case crypto.SSEC.IsRequested(r.Header):
w.Header().Set(crypto.SSECAlgorithm, r.Header.Get(crypto.SSECAlgorithm))
w.Header().Set(crypto.SSECKeyMD5, r.Header.Get(crypto.SSECKeyMD5))
}
}
}
// Get host and port from Request.RemoteAddr.
host, port, err := net.SplitHostPort(handlers.GetSourceIP(r))
@@ -847,7 +1009,6 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
defer logger.AuditLog(w, r, "WebDownload", mustGetClaimsFromToken(r))
var wg sync.WaitGroup
objectAPI := web.ObjectAPI()
if objectAPI == nil {
writeWebErrorResponse(w, errServerNotInitialized)
@@ -883,7 +1044,7 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
if authErr == nil {
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.Subject,
Action: iampolicy.Action(policy.GetObjectAction),
Action: iampolicy.GetObjectAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, ""),
IsOwner: owner,
@@ -894,101 +1055,67 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
}
}
opts := ObjectOptions{}
getObjectInfo := objectAPI.GetObjectInfo
getObject := objectAPI.GetObject
getObjectNInfo := objectAPI.GetObjectNInfo
if web.CacheAPI() != nil {
getObjectInfo = web.CacheAPI().GetObjectInfo
getObject = web.CacheAPI().GetObject
getObjectNInfo = web.CacheAPI().GetObjectNInfo
}
objInfo, err := getObjectInfo(ctx, bucket, object, opts)
var opts ObjectOptions
gr, err := getObjectNInfo(ctx, bucket, object, nil, r.Header, readLock, opts)
if err != nil {
writeWebErrorResponse(w, err)
return
}
length := objInfo.Size
var actualSize int64
if objInfo.IsCompressed() {
// Read the decompressed size from the meta.json.
actualSize = objInfo.GetActualSize()
if actualSize < 0 {
return
}
}
defer gr.Close()
objInfo := gr.ObjInfo
if objectAPI.IsEncryptionSupported() {
if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil {
writeWebErrorResponse(w, err)
return
}
}
// Set encryption response headers
if objectAPI.IsEncryptionSupported() {
if crypto.IsEncrypted(objInfo.UserDefined) {
length, _ = objInfo.DecryptedSize()
switch {
case crypto.S3.IsEncrypted(objInfo.UserDefined):
w.Header().Set(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
case crypto.SSEC.IsEncrypted(objInfo.UserDefined):
w.Header().Set(crypto.SSECAlgorithm, r.Header.Get(crypto.SSECAlgorithm))
w.Header().Set(crypto.SSECKeyMD5, r.Header.Get(crypto.SSECKeyMD5))
}
}
}
var startOffset int64
var writer io.Writer
if objInfo.IsCompressed() {
// The decompress metrics are set.
snappyStartOffset := 0
snappyLength := actualSize
// Open a pipe for compression
// Where compressWriter is actually passed to the getObject
decompressReader, compressWriter := io.Pipe()
snappyReader := snappy.NewReader(decompressReader)
// The limit is set to the actual size.
responseWriter := ioutil.LimitedWriter(w, int64(snappyStartOffset), snappyLength)
wg.Add(1) //For closures.
go func() {
defer wg.Done()
// Finally, writes to the client.
_, perr := io.Copy(responseWriter, snappyReader)
// Close the compressWriter if the data is read already.
// Closing the pipe, releases the writer passed to the getObject.
compressWriter.CloseWithError(perr)
}()
writer = compressWriter
} else {
writer = w
}
if objectAPI.IsEncryptionSupported() && crypto.S3.IsEncrypted(objInfo.UserDefined) {
// Response writer should be limited early on for decryption upto required length,
// additionally also skipping mod(offset)64KiB boundaries.
writer = ioutil.LimitedWriter(writer, startOffset%(64*1024), length)
writer, startOffset, length, err = DecryptBlocksRequest(writer, r, bucket, object, startOffset, length, objInfo, false)
if err != nil {
writeWebErrorResponse(w, err)
return
}
w.Header().Set(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
}
httpWriter := ioutil.WriteOnClose(writer)
// Add content disposition.
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(object)))
if err = getObject(ctx, bucket, object, 0, -1, httpWriter, "", opts); err != nil {
httpWriter.Close()
if objInfo.IsCompressed() {
wg.Wait()
}
/// No need to print error, response writer already written to.
if err = setObjectHeaders(w, objInfo, nil); err != nil {
writeWebErrorResponse(w, err)
return
}
// Add content disposition.
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(objInfo.Name)))
setHeadGetRespHeaders(w, r.URL.Query())
httpWriter := ioutil.WriteOnClose(w)
// Write object content to response body
if _, err = io.Copy(httpWriter, gr); err != nil {
if !httpWriter.HasWritten() { // write error response only if no data or headers has been written to client yet
writeWebErrorResponse(w, err)
}
return
}
if err = httpWriter.Close(); err != nil {
if !httpWriter.HasWritten() { // write error response only if no data has been written to client yet
if !httpWriter.HasWritten() { // write error response only if no data or headers has been written to client yet
writeWebErrorResponse(w, err)
return
}
}
if objInfo.IsCompressed() {
// Wait for decompression go-routine to retire.
wg.Wait()
}
// Get host and port from Request.RemoteAddr.
host, port, err := net.SplitHostPort(handlers.GetSourceIP(r))
@@ -1074,7 +1201,7 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
for _, object := range args.Objects {
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.Subject,
Action: iampolicy.Action(policy.GetObjectAction),
Action: iampolicy.GetObjectAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, ""),
IsOwner: owner,
@@ -1272,18 +1399,48 @@ func (web *webAPIHandlers) GetBucketPolicy(r *http.Request, args *GetBucketPolic
return toJSONError(errAccessDenied)
}
bucketPolicy, err := objectAPI.GetBucketPolicy(context.Background(), args.BucketName)
if err != nil {
if _, ok := err.(BucketPolicyNotFound); !ok {
var policyInfo = &miniogopolicy.BucketAccessPolicy{Version: "2012-10-17"}
if isRemoteCallRequired(context.Background(), args.BucketName, objectAPI) {
sr, err := globalDNSConfig.Get(args.BucketName)
if err != nil {
if err == dns.ErrNoEntriesFound {
return toJSONError(BucketNotFound{
Bucket: args.BucketName,
}, args.BucketName)
}
return toJSONError(err, args.BucketName)
}
return err
}
client, rerr := getRemoteInstanceClient(r, getHostFromSrv(sr))
if rerr != nil {
return toJSONError(rerr, args.BucketName)
}
policyStr, err := client.GetBucketPolicy(args.BucketName)
if err != nil {
return toJSONError(rerr, args.BucketName)
}
bucketPolicy, err := policy.ParseConfig(strings.NewReader(policyStr), args.BucketName)
if err != nil {
return toJSONError(rerr, args.BucketName)
}
policyInfo, err = PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
} else {
bucketPolicy, err := objectAPI.GetBucketPolicy(context.Background(), args.BucketName)
if err != nil {
if _, ok := err.(BucketPolicyNotFound); !ok {
return toJSONError(err, args.BucketName)
}
return err
}
policyInfo, err := PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
policyInfo, err = PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
}
reply.UIVersion = browser.UIVersion
@@ -1325,17 +1482,42 @@ func (web *webAPIHandlers) ListAllBucketPolicies(r *http.Request, args *ListAllB
return toJSONError(errAccessDenied)
}
bucketPolicy, err := objectAPI.GetBucketPolicy(context.Background(), args.BucketName)
if err != nil {
if _, ok := err.(BucketPolicyNotFound); !ok {
var policyInfo = new(miniogopolicy.BucketAccessPolicy)
if isRemoteCallRequired(context.Background(), args.BucketName, objectAPI) {
sr, err := globalDNSConfig.Get(args.BucketName)
if err != nil {
if err == dns.ErrNoEntriesFound {
return toJSONError(BucketNotFound{
Bucket: args.BucketName,
}, args.BucketName)
}
return toJSONError(err, args.BucketName)
}
core, rerr := getRemoteInstanceClient(r, getHostFromSrv(sr))
if rerr != nil {
return toJSONError(rerr, args.BucketName)
}
var policyStr string
policyStr, err = core.Client.GetBucketPolicy(args.BucketName)
if err != nil {
return toJSONError(err, args.BucketName)
}
if policyStr != "" {
if err = json.Unmarshal([]byte(policyStr), policyInfo); err != nil {
return toJSONError(err, args.BucketName)
}
}
} else {
bucketPolicy, err := objectAPI.GetBucketPolicy(context.Background(), args.BucketName)
if err != nil {
if _, ok := err.(BucketPolicyNotFound); !ok {
return toJSONError(err, args.BucketName)
}
}
policyInfo, err = PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
return toJSONError(err, args.BucketName)
}
}
policyInfo, err := PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
reply.UIVersion = browser.UIVersion
@@ -1385,43 +1567,94 @@ func (web *webAPIHandlers) SetBucketPolicy(r *http.Request, args *SetBucketPolic
ctx := context.Background()
bucketPolicy, err := objectAPI.GetBucketPolicy(ctx, args.BucketName)
if err != nil {
if _, ok := err.(BucketPolicyNotFound); !ok {
if isRemoteCallRequired(context.Background(), args.BucketName, objectAPI) {
sr, err := globalDNSConfig.Get(args.BucketName)
if err != nil {
if err == dns.ErrNoEntriesFound {
return toJSONError(BucketNotFound{
Bucket: args.BucketName,
}, args.BucketName)
}
return toJSONError(err, args.BucketName)
}
}
core, rerr := getRemoteInstanceClient(r, getHostFromSrv(sr))
if rerr != nil {
return toJSONError(rerr, args.BucketName)
}
var policyStr string
// Use the abstracted API instead of core, such that
// NoSuchBucketPolicy errors are automatically handled.
policyStr, err = core.Client.GetBucketPolicy(args.BucketName)
if err != nil {
return toJSONError(err, args.BucketName)
}
var policyInfo = &miniogopolicy.BucketAccessPolicy{Version: "2012-10-17"}
if policyStr != "" {
if err = json.Unmarshal([]byte(policyStr), policyInfo); err != nil {
return toJSONError(err, args.BucketName)
}
}
policyInfo, err := PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
policyInfo.Statements = miniogopolicy.SetPolicy(policyInfo.Statements, policyType, args.BucketName, args.Prefix)
if len(policyInfo.Statements) == 0 {
if err = core.SetBucketPolicy(args.BucketName, ""); err != nil {
return toJSONError(err, args.BucketName)
}
return nil
}
policyInfo.Statements = miniogopolicy.SetPolicy(policyInfo.Statements, policyType, args.BucketName, args.Prefix)
if len(policyInfo.Statements) == 0 {
if err = objectAPI.DeleteBucketPolicy(ctx, args.BucketName); err != nil {
bucketPolicy, err := BucketAccessPolicyToPolicy(policyInfo)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
globalPolicySys.Remove(args.BucketName)
return nil
}
policyData, err := json.Marshal(bucketPolicy)
if err != nil {
return toJSONError(err, args.BucketName)
}
bucketPolicy, err = BucketAccessPolicyToPolicy(policyInfo)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
if err = core.SetBucketPolicy(args.BucketName, string(policyData)); err != nil {
return toJSONError(err, args.BucketName)
}
// Parse validate and save bucket policy.
if err := objectAPI.SetBucketPolicy(ctx, args.BucketName, bucketPolicy); err != nil {
return toJSONError(err, args.BucketName)
}
} else {
bucketPolicy, err := objectAPI.GetBucketPolicy(ctx, args.BucketName)
if err != nil {
if _, ok := err.(BucketPolicyNotFound); !ok {
return toJSONError(err, args.BucketName)
}
}
policyInfo, err := PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
globalPolicySys.Set(args.BucketName, *bucketPolicy)
globalNotificationSys.SetBucketPolicy(ctx, args.BucketName, bucketPolicy)
policyInfo.Statements = miniogopolicy.SetPolicy(policyInfo.Statements, policyType, args.BucketName, args.Prefix)
if len(policyInfo.Statements) == 0 {
if err = objectAPI.DeleteBucketPolicy(ctx, args.BucketName); err != nil {
return toJSONError(err, args.BucketName)
}
globalPolicySys.Remove(args.BucketName)
return nil
}
bucketPolicy, err = BucketAccessPolicyToPolicy(policyInfo)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
// Parse validate and save bucket policy.
if err := objectAPI.SetBucketPolicy(ctx, args.BucketName, bucketPolicy); err != nil {
return toJSONError(err, args.BucketName)
}
globalPolicySys.Set(args.BucketName, *bucketPolicy)
globalNotificationSys.SetBucketPolicy(ctx, args.BucketName, bucketPolicy)
}
return nil
}
+2 -2
View File
@@ -539,8 +539,8 @@ func testListObjectsWebHandler(obj ObjectLayer, instanceType string, t TestErrHa
Statements: []policy.Statement{policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.GetObjectAction),
policy.NewResourceSet(policy.NewResource(bucketName, "*")),
policy.NewActionSet(policy.ListBucketAction),
policy.NewResourceSet(policy.NewResource(bucketName, "")),
condition.NewFunctions(),
)},
}
+1 -1
View File
@@ -89,7 +89,7 @@ func registerWebRouter(router *mux.Router) error {
// These methods use short-expiry tokens in the URLs. These tokens may unintentionally
// be logged, so a new one must be generated for each request.
webBrowserRouter.Methods("GET").Path("/download/{bucket}/{object:.+}").Queries("token", "{token:.*}").HandlerFunc(web.Download)
webBrowserRouter.Methods("GET").Path("/download/{bucket}/{object:.+}").Queries("token", "{token:.*}").HandlerFunc(httpTraceHdrs(web.Download))
webBrowserRouter.Methods("POST").Path("/zip").Queries("token", "{token:.*}").HandlerFunc(httpTraceHdrs(web.DownloadZip))
// Add compression for assets.
+16 -5
View File
@@ -34,6 +34,14 @@ func (xl xlObjects) getUploadIDDir(bucket, object, uploadID string) string {
return pathJoin(xl.getMultipartSHADir(bucket, object), uploadID)
}
// getUploadIDLockPath returns the name of the Lock in the form of
// bucket/object/uploadID. For locking, the path bucket/object/uploadID
// is locked instead of multipart-sha256-Dir/uploadID as it is more
// readable in the list-locks output which helps in debugging.
func (xl xlObjects) getUploadIDLockPath(bucket, object, uploadID string) string {
return pathJoin(bucket, object, uploadID)
}
func (xl xlObjects) getMultipartSHADir(bucket, object string) string {
return getSHA256Hash([]byte(pathJoin(bucket, object)))
}
@@ -290,9 +298,10 @@ func (xl xlObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
var partsMetadata []xlMetaV1
var errs []error
uploadIDPath := xl.getUploadIDDir(bucket, object, uploadID)
uploadIDLockPath := xl.getUploadIDLockPath(bucket, object, uploadID)
// pre-check upload id lock.
preUploadIDLock := xl.nsMutex.NewNSLock(minioMetaMultipartBucket, uploadIDPath)
preUploadIDLock := xl.nsMutex.NewNSLock(minioMetaMultipartBucket, uploadIDLockPath)
if err := preUploadIDLock.GetRLock(globalOperationTimeout); err != nil {
return pi, err
}
@@ -398,7 +407,7 @@ func (xl xlObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
}
// post-upload check (write) lock
postUploadIDLock := xl.nsMutex.NewNSLock(minioMetaMultipartBucket, uploadIDPath)
postUploadIDLock := xl.nsMutex.NewNSLock(minioMetaMultipartBucket, uploadIDLockPath)
if err = postUploadIDLock.GetLock(globalOperationTimeout); err != nil {
return pi, err
}
@@ -491,7 +500,7 @@ func (xl xlObjects) ListObjectParts(ctx context.Context, bucket, object, uploadI
// Hold lock so that there is no competing
// abort-multipart-upload or complete-multipart-upload.
uploadIDLock := xl.nsMutex.NewNSLock(minioMetaMultipartBucket,
xl.getUploadIDDir(bucket, object, uploadID))
xl.getUploadIDLockPath(bucket, object, uploadID))
if err := uploadIDLock.GetLock(globalListingTimeout); err != nil {
return result, err
}
@@ -579,6 +588,7 @@ func (xl xlObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
defer destLock.Unlock()
uploadIDPath := xl.getUploadIDDir(bucket, object, uploadID)
uploadIDLockPath := xl.getUploadIDLockPath(bucket, object, uploadID)
// Hold lock so that
//
@@ -586,7 +596,7 @@ func (xl xlObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
//
// 2) no one does a parallel complete-multipart-upload on this
// multipart upload
uploadIDLock := xl.nsMutex.NewNSLock(minioMetaMultipartBucket, uploadIDPath)
uploadIDLock := xl.nsMutex.NewNSLock(minioMetaMultipartBucket, uploadIDLockPath)
if err := uploadIDLock.GetLock(globalOperationTimeout); err != nil {
return oi, err
}
@@ -785,9 +795,10 @@ func (xl xlObjects) AbortMultipartUpload(ctx context.Context, bucket, object, up
}
// Construct uploadIDPath.
uploadIDPath := xl.getUploadIDDir(bucket, object, uploadID)
uploadIDLockPath := xl.getUploadIDLockPath(bucket, object, uploadID)
// Hold lock so that there is no competing
// complete-multipart-upload or put-object-part.
uploadIDLock := xl.nsMutex.NewNSLock(minioMetaMultipartBucket, uploadIDPath)
uploadIDLock := xl.nsMutex.NewNSLock(minioMetaMultipartBucket, uploadIDLockPath)
if err := uploadIDLock.GetLock(globalOperationTimeout); err != nil {
return err
}
+14 -3
View File
@@ -132,9 +132,9 @@ By default, parity for objects with standard storage class is set to `N/2`, and
## Environment only settings
#### Browser
### Browser
Enable or disable access to web UI. By default it is set to `on`. You may override this field with ``MINIO_BROWSER`` environment variable.
Enable or disable access to web UI. By default it is set to `on`. You may override this field with `MINIO_BROWSER` environment variable.
Example:
@@ -145,7 +145,7 @@ minio server /data
### Domain
By default, Minio supports path-style requests that are of the format http://mydomain.com/bucket/object. MINIO_DOMAIN environment variable is used to enable virtual-host-style requests. If the request `Host` header matches with `(.+).mydomain.com` then the matched pattern `$1` is used as bucket and the path is used as object. More information on path-style and virtual-host-style [here](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAPI.html)
By default, Minio supports path-style requests that are of the format http://mydomain.com/bucket/object. `MINIO_DOMAIN` environment variable is used to enable virtual-host-style requests. If the request `Host` header matches with `(.+).mydomain.com` then the matched pattern `$1` is used as bucket and the path is used as object. More information on path-style and virtual-host-style [here](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAPI.html)
Example:
```sh
@@ -153,6 +153,17 @@ export MINIO_DOMAIN=mydomain.com
minio server /data
```
### Drive Sync
By default, Minio writes to disk in synchronous mode for all metadata operations. Set `MINIO_DRIVE_SYNC` environment variable to enable synchronous mode for all data operations as well.
Example:
```sh
export MINIO_DRIVE_SYNC=on
minio server /data
```
## Explore Further
* [Minio Quickstart Guide](https://docs.minio.io/docs/minio-quickstart-guide)
+49 -30
View File
@@ -1,20 +1,26 @@
# Minio GCS Gateway [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io)
Minio GCS Gateway adds Amazon S3 compatibility to Google Cloud Storage.
## Run Minio Gateway for GCS
### Create service account key for GCS and get the credentials file
1. Go to the [API Console Credentials page](https://console.developers.google.com/project/_/apis/credentials).
2. Select your project or create a new project. Note down your project ID.
3. On the Credentials page, select the __Create credentials__ drop-down, then select __Service account key__.
4. From the __Service account__ drop-down, select __New service account__
5. Fill up __Service account name__ and __Service account ID__
6. For the __Role__, click the select dropdown to choose __Storage__ -> __Storage Admin__ _(Full control of GCS resources)_
7. Click the __Create__ button. This will download a credentials file to your desktop. Let's call this credentials.json
Minio GCS Gateway allows you to access Google Cloud Storage (GCS) with Amazon S3-compatible APIs
Note: Alternate ways to setup *Application Default Credentials* is explained [here](https://developers.google.com/identity/protocols/application-default-credentials)
- [Run Minio Gateway for GCS](#run-minio-gateway-for-gcs)
- [Test Using Minio Browser](#test-using-minio-browser)
- [Test Using Minio Client](#test-using-minio-client)
### Using Docker
```
## <a name="run-minio-gateway-for-gcs"></a>1. Run Minio Gateway for GCS
### 1.1 Create a Service Account key for GCS and get the Credentials File
1. Navigate to the [API Console Credentials page](https://console.developers.google.com/project/_/apis/credentials).
2. Select a project or create a new project. Note the project ID.
3. Select the **Create credentials** dropdown on the **Credentials** page, and click **Service account key**.
4. Select **New service account** from the **Service account** dropdown.
5. Populate the **Service account name** and **Service account ID**.
6. Click the dropdown for the **Role** and choose **Storage** > **Storage Admin** *(Full control of GCS resources)*.
7. Click the **Create** button to download a credentials file and rename it to `credentials.json`.
**Note:** For alternate ways to set up *Application Default Credentials*, see [Setting Up Authentication for Server to Server Production Applications](https://developers.google.com/identity/protocols/application-default-credentials).
### 1.1 Run Minio GCS Gateway Using Docker
```sh
docker run -p 9000:9000 --name gcs-s3 \
-v /path/to/credentials.json:/credentials.json \
-e "GOOGLE_APPLICATION_CREDENTIALS=/credentials.json" \
@@ -23,47 +29,60 @@ docker run -p 9000:9000 --name gcs-s3 \
minio/minio gateway gcs yourprojectid
```
### Using Binary
```
### 1.2 Run Minio GCS Gateway Using the Minio Binary
```sh
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
export MINIO_ACCESS_KEY=minioaccesskey
export MINIO_SECRET_KEY=miniosecretkey
minio gateway gcs yourprojectid
```
## Test using Minio Browser
Minio Gateway comes with an embedded web based object browser. Point your web browser to http://127.0.0.1:9000 to ensure that your server has started successfully.
## <a name="test-using-minio-browser"></a>2. Test Using Minio Browser
Minio Gateway comes with an embedded web-based object browser that outputs content to http://127.0.0.1:9000. To test that Minio Gateway is running, open a web browser, navigate to http://127.0.0.1:9000, and ensure that the object browser is displayed.
![Screenshot](https://github.com/minio/minio/blob/master/docs/screenshots/minio-browser-gateway.png?raw=true)
## Test using Minio Client `mc`
`mc` provides a modern alternative to UNIX commands such as ls, cat, cp, mirror, diff etc. It supports filesystems and Amazon S3 compatible cloud storage services.
## <a name="test-using-minio-client"></a>3. Test Using Minio Client
### Configure `mc`
```
Minio Client is a command-line tool called `mc` that provides UNIX-like commands for interacting with the server (e.g. ls, cat, cp, mirror, diff, find, etc.). `mc` supports file systems and Amazon S3-compatible cloud storage services (AWS Signature v2 and v4).
### 3.1 Configure the Gateway using Minio Client
Use the following command to configure the gateway:
```sh
mc config host add mygcs http://gateway-ip:9000 minioaccesskey miniosecretkey
```
### List containers on GCS
```
### 3.2 List Containers on GCS
Use the following command to list the containers on GCS:
```sh
mc ls mygcs
```
A response similar to this one should be displayed:
```
[2017-02-22 01:50:43 PST] 0B ferenginar/
[2017-02-26 21:43:51 PST] 0B my-container/
[2017-02-26 22:10:11 PST] 0B test-container1/
```
### Known limitations
Gateway inherits the following GCS limitations:
### 3.3 Known limitations
Minio Gateway has the following limitations when used with GCS:
- Only read-only or write-only bucket policy supported at bucket level, all other variations will return API Notimplemented error.
- _List Multipart Uploads_ and _List Object parts_ always returns empty list. i.e Client will need to remember all the parts that it has uploaded and use it for _Complete Multipart Upload_
* It only supports read-only and write-only bucket policies at the bucket level; all other variations will return `API Not implemented`.
* The `List Multipart Uploads` and `List Object parts` commands always return empty lists. Therefore, the client must store all of the parts that it has uploaded and use that information when invoking the `_Complete Multipart Upload` command.
Other limitations:
- Bucket notification APIs are not supported.
* Bucket notification APIs are not supported.
## Explore Further
## <a name="explore-further"></a>4. Explore Further
- [`mc` command-line interface](https://docs.minio.io/docs/minio-client-quickstart-guide)
- [`aws` command-line interface](https://docs.minio.io/docs/aws-cli-with-minio)
- [`minio-go` Go SDK](https://docs.minio.io/docs/golang-client-quickstart-guide)
+15 -3
View File
@@ -1,35 +1,47 @@
# Minio NAS Gateway [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io)
Minio Gateway adds Amazon S3 compatibility to NAS storage. You may run multiple minio instances on the same shared NAS volume as a distributed object gateway.
## Run Minio Gateway for NAS Storage
### Using Docker
Please ensure to replace `/shared/nasvol` with actual mount path.
```
docker run -p 9000:9000 --name nas-s3 \
-e "MINIO_ACCESS_KEY=minio" \
-e "MINIO_SECRET_KEY=minio123" \
minio/minio gateway nas /shared/nasvol
-v /shared/nasvol:/container/vol \
minio/minio gateway nas /container/vol
```
### Using Binary
```
export MINIO_ACCESS_KEY=minioaccesskey
export MINIO_SECRET_KEY=miniosecretkey
export MINIO_ACCESS_KEY=minio
export MINIO_SECRET_KEY=minio123
minio gateway nas /shared/nasvol
```
## Test using Minio Browser
Minio Gateway comes with an embedded web based object browser. Point your web browser to http://127.0.0.1:9000 to ensure that your server has started successfully.
![Screenshot](https://raw.githubusercontent.com/minio/minio/master/docs/screenshots/minio-browser-gateway.png)
## Test using Minio Client `mc`
`mc` provides a modern alternative to UNIX commands such as ls, cat, cp, mirror, diff etc. It supports filesystems and Amazon S3 compatible cloud storage services.
### Configure `mc`
```
mc config host add mynas http://gateway-ip:9000 access_key secret_key
```
### List buckets on nas
```
mc ls mynas
[2017-02-22 01:50:43 PST] 0B ferenginar/
+98 -15
View File
@@ -1,26 +1,57 @@
# KMS Quickstart Guide [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io)
KMS feature allows you to use Vault to generate and manage encryption keys for use by the minio server to encrypt objects. This document explains how to configure Minio with Vault as KMS.
Minio uses a key-management-system (KMS) to support SSE-S3. If a client requests SSE-S3, or auto-encryption
is enabled, the Minio server encrypts each object with an unique object key which is protected by a master key
managed by the KMS. Usually all object keys are protected by a single master key.
Minio supports two different KMS concepts:
- External KMS:
Minio can be configured to use an external KMS i.e. [Hashicorp Vault](https://www.vaultproject.io/).
An external KMS decouples Minio as storage system from key-management. An external KMS can
be managed by a dedicated security team and allows you to grant/deny access to (certain) objects
by enabling or disabling the corresponding master keys on demand.
- Direct KMS master keys:
Minio can also be configured to directly use a master key specified by the environment variable `MINIO_SSE_MASTER_KEY`.
Direct master keys are useful if the storage backend is not on the same machine as the Minio server, e.g.,
if network drives or Minio gateway is used and an external KMS would cause too much management overhead.
Note: KMS master keys are mainly for testing purposes. It's not recommended to use them for production deployments.
Further if the Minio server machine is ever compromised, then the master key must also be treated as compromised.
**Important:**
If multiple Minio servers are configured as [gateways](https://github.com/minio/minio/blob/master/docs/gateway/README.md)
pointing to the *same* backend - for example the same NAS storage - then the KMS configuration **must** be the same for
all gateways. Otherwise one gateway may not be able to decrypt objects created by another gateway. It is the operators'
responsibility to ensure consistency.
## Get started
### 1. Prerequisites
Install Minio - [Minio Quickstart Guide](https://docs.minio.io/docs/minio-quickstart-guide).
### 2. Configure Vault
Vault as Key Management System requires following to be configured in Vault
### 2. Setup a KMS
- [transit](https://www.vaultproject.io/docs/secrets/transit/index.html) backend configured with a named encryption key-ring
- [AppRole](https://www.vaultproject.io/docs/auth/approle.html) based authentication with read/update policy for transit backend. In particular, read and update policy are required for the [Generate Data Key](https://www.vaultproject.io/api/secret/transit/index.html#generate-data-key) endpoint and [Decrypt Data](https://www.vaultproject.io/api/secret/transit/index.html#decrypt-data) endpoint.
Either use Hashicorp Vault as external KMS or specify a master key directly depending on your use case.
#### 2.1 Setup Hashicorp Vault
Here is a sample quick start for configuring vault with a transit backend and Approle with correct policy
#### 2.1 Start Vault server in dev mode
Minio requires the following Vault setup:
- The [transit backend](https://www.vaultproject.io/api/secret/transit/index.html) configured with a named encryption key-ring
- [AppRole](https://www.vaultproject.io/docs/auth/approle.html) based authentication with read/update policy for transit backend. In particular, read and update policy are required for the [Generate Data Key](https://www.vaultproject.io/api/secret/transit/index.html#generate-data-key) endpoint and [Decrypt Data](https://www.vaultproject.io/api/secret/transit/index.html#decrypt-data) endpoint.
**2.1.1 Start Vault server in dev mode**
In dev mode, Vault server runs in-memory and starts unsealed. Note that running Vault in dev mode is insecure and any data stored in the Vault is lost upon restart.
```
vault server -dev
```
#### 2.2 Set up vault transit backend and create an app role
**2.1.2 Set up vault transit backend and create an app role**
```
cat > vaultpolicy.hcl <<EOF
path "transit/datakey/plaintext/my-minio-key" {
@@ -50,33 +81,85 @@ vault write -f auth/approle/role/my-role/secret-id
The AppRole ID, AppRole Secret Id, Vault endpoint and Vault key name can now be used to start minio server with Vault as KMS.
Note: If [Vault Namespaces](https://learn.hashicorp.com/vault/operations/namespaces) are in use, VAULT_NAMESPACE variable needs to be set before setting approle and transit secrets engine.
**2.1.3 Vault Environment variables**
### 3. Environment variables
You'll need the Vault endpoint, AppRole ID, AppRole SecretID and encryption key-ring name defined in step 2.2
You'll need the Vault endpoint, AppRole ID, AppRole SecretID and encryption key-ring name defined in step 2.1.2
```sh
export MINIO_SSE_VAULT_APPROLE_ID=9b56cc08-8258-45d5-24a3-679876769126
export MINIO_SSE_VAULT_APPROLE_SECRET=4e30c52f-13e4-a6f5-0763-d50e8cb4321f
export MINIO_SSE_VAULT_ENDPOINT=https://vault-endpoint-ip:8200
export MINIO_SSE_VAULT_KEY_NAME=my-minio-key
export MINIO_SSE_VAULT_AUTH_TYPE=approle
minio server ~/export
```
Optionally set `MINIO_SSE_VAULT_CAPATH` as the path to a directory of PEM-encoded CA cert files to verify the Vault server SSL certificate.
Optionally, set `MINIO_SSE_VAULT_CAPATH` to a directory of PEM-encoded CA cert files to use mTLS for client-server authentication.
```
export MINIO_SSE_VAULT_CAPATH=/home/user/custom-pems
export MINIO_SSE_VAULT_CAPATH=/home/user/custom-certs
```
Optionally set `MINIO_SSE_VAULT_NAMESPACE` if AppRole and Transit Secrets engine have been scoped to Vault Namespace
An additional option is to set `MINIO_SSE_VAULT_NAMESPACE` if AppRole and Transit Secrets engine have been scoped to Vault Namespace
```
export MINIO_SSE_VAULT_NAMESPACE=ns1
```
### 4. Test your setup
Note: If [Vault Namespaces](https://learn.hashicorp.com/vault/operations/namespaces) are in use, MINIO_SSE_VAULT_NAMESPACE variable needs to be set before setting approle and transit secrets engine.
#### 2.2 Specify a master key
A KMS master key consists of a master-key ID (CMK) and the 256 bit master key encoded as HEX value separated by a `:`.
A KMS master key can be specified directly using:
```sh
export MINIO_SSE_MASTER_KEY=my-minio-key:6368616e676520746869732070617373776f726420746f206120736563726574
```
Please use your own master key. A random master key can be generated using e.g. this command on Linux/Mac/BSD* systems:
```sh
head -c 32 /dev/urandom | xxd -c 32 -ps
```
### 3. Test your setup
To test this setup, start minio server with environment variables set in Step 3, and server is ready to handle SSE-S3 requests.
### Auto-Encryption
Minio can also enable auto-encryption **if** a valid KMS configuration is specified and the storage backend supports
encrypted objects. Auto-Encryption, if enabled, ensures that all uploaded objects are encrypted using the specified
KMS configuration.
Auto-Encryption is useful especially if the Minio operator wants to ensure that objects are **never** stored in
plaintext - for example if sensitive data is stored on public cloud storage.
To enable auto-encryption set the environment variable to `on`:
```sh
export MINIO_SSE_AUTO_ENCRYPTION=on
```
To verify auto-encryption, use the `mc` command:
```sh
mc cp test.file myminio/crypt/
test.file: 5 B / 5 B ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 100.00% 337 B/s 0s
mc stat myminio/crypt/test.file
Name : test.file
...
Encrypted :
X-Amz-Server-Side-Encryption: AES256
```
Note: Auto-Encryption only affects non-SSE-C requests since objects uploaded using SSE-C are already encrypted
and S3 only allows either SSE-S3 or SSE-C but not both for the same object.
# Explore Further
- [Use `mc` with Minio Server](https://docs.minio.io/docs/minio-client-quickstart-guide)
@@ -5,7 +5,7 @@ version: '2'
# 9001 through 9004.
services:
minio1:
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
volumes:
- data1:/data
ports:
@@ -15,7 +15,7 @@ services:
MINIO_SECRET_KEY: minio123
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
minio2:
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
volumes:
- data2:/data
ports:
@@ -25,7 +25,7 @@ services:
MINIO_SECRET_KEY: minio123
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
minio3:
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
volumes:
- data3:/data
ports:
@@ -35,7 +35,7 @@ services:
MINIO_SECRET_KEY: minio123
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
minio4:
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
volumes:
- data4:/data
ports:
@@ -2,7 +2,7 @@ version: '3.1'
services:
minio1:
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
hostname: minio1
volumes:
- minio1-data:/export
@@ -24,7 +24,7 @@ services:
- access_key
minio2:
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
hostname: minio2
volumes:
- minio2-data:/export
@@ -46,7 +46,7 @@ services:
- access_key
minio3:
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
hostname: minio3
volumes:
- minio3-data:/export
@@ -68,7 +68,7 @@ services:
- access_key
minio4:
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
hostname: minio4
volumes:
- minio4-data:/export
@@ -2,7 +2,7 @@ version: '3'
services:
minio1:
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
hostname: minio1
volumes:
- minio1-data:/export
@@ -24,7 +24,7 @@ services:
command: server http://minio1/export http://minio2/export http://minio3/export http://minio4/export
minio2:
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
hostname: minio2
volumes:
- minio2-data:/export
@@ -46,7 +46,7 @@ services:
command: server http://minio1/export http://minio2/export http://minio3/export http://minio4/export
minio3:
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
hostname: minio3
volumes:
- minio3-data:/export
@@ -68,7 +68,7 @@ services:
command: server http://minio1/export http://minio2/export http://minio3/export http://minio4/export
minio4:
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
hostname: minio4
volumes:
- minio4-data:/export
@@ -30,7 +30,7 @@ spec:
value: "minio"
- name: MINIO_SECRET_KEY
value: "minio123"
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
# Unfortunately you must manually define each server. Perhaps autodiscovery via DNS can be implemented in the future.
args:
- server
@@ -21,7 +21,7 @@ spec:
value: "minio"
- name: MINIO_SECRET_KEY
value: "minio123"
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
args:
- server
- http://minio-0.minio.default.svc.cluster.local/data
@@ -21,7 +21,7 @@ spec:
containers:
- name: minio
# Pulls the default Minio image from Docker Hub
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
args:
- gateway
- gcs
@@ -29,7 +29,7 @@ spec:
- name: data
mountPath: "/data"
# Pulls the lastest Minio image from Docker Hub
image: minio/minio:RELEASE.2018-11-30T03-56-59Z
image: minio/minio:RELEASE.2018-12-13T02-04-19Z
args:
- server
- /data
+30 -9
View File
@@ -85,16 +85,28 @@ func (c *coreDNS) list(key string) ([]SrvRecord, error) {
}
srvRecord.Key = strings.TrimPrefix(string(n.Key), key)
srvRecord.Key = strings.TrimSuffix(srvRecord.Key, srvRecord.Host)
srvRecords = append(srvRecords, srvRecord)
// SRV records are stored in the following form
// /skydns/net/miniocloud/bucket1, so this function serves multiple
// purposes basically when we do a Get(bucketName) this function
// should return a single DNS record for any input 'bucketName'.
//
// In all other situations when we want to list all DNS records,
// which is handled in the else clause.
if key != msg.Path(fmt.Sprintf(".%s.", c.domainName), defaultPrefixPath) {
if srvRecord.Key == "/" {
srvRecords = append(srvRecords, srvRecord)
}
} else {
srvRecords = append(srvRecords, srvRecord)
}
}
if srvRecords != nil {
sort.Slice(srvRecords, func(i int, j int) bool {
return srvRecords[i].Key < srvRecords[j].Key
})
} else {
if len(srvRecords) == 0 {
return nil, ErrNoEntriesFound
}
sort.Slice(srvRecords, func(i int, j int) bool {
return srvRecords[i].Key < srvRecords[j].Key
})
return srvRecords, nil
}
@@ -123,9 +135,18 @@ func (c *coreDNS) Put(bucket string) error {
// Removes DNS entries added in Put().
func (c *coreDNS) Delete(bucket string) error {
key := msg.Path(fmt.Sprintf("%s.%s.", bucket, c.domainName), defaultPrefixPath)
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
_, err := c.etcdClient.Delete(ctx, key)
defer cancel()
srvRecords, err := c.list(key)
if err != nil {
return err
}
for _, record := range srvRecords {
dctx, dcancel := context.WithTimeout(context.Background(), defaultContextTimeout)
if _, err = c.etcdClient.Delete(dctx, key+"/"+record.Host); err != nil {
dcancel()
return err
}
dcancel()
}
return err
}
+24 -5
View File
@@ -26,7 +26,7 @@ import (
var (
// Regex to extract ellipses syntax inputs.
regexpEllipses = regexp.MustCompile(`(.*)({[0-9]*\.\.\.[0-9]*})(.*)`)
regexpEllipses = regexp.MustCompile(`(.*)({[0-9a-z]*\.\.\.[0-9a-z]*})(.*)`)
// Ellipses constants
openBraces = "{"
@@ -53,13 +53,24 @@ func parseEllipsesRange(pattern string) (seq []string, err error) {
return nil, errors.New("Invalid argument")
}
var hexadecimal bool
var start, end uint64
if start, err = strconv.ParseUint(ellipsesRange[0], 10, 64); err != nil {
return nil, err
// Look for hexadecimal conversions if any.
start, err = strconv.ParseUint(ellipsesRange[0], 16, 64)
if err != nil {
return nil, err
}
hexadecimal = true
}
if end, err = strconv.ParseUint(ellipsesRange[1], 10, 64); err != nil {
return nil, err
// Look for hexadecimal conversions if any.
end, err = strconv.ParseUint(ellipsesRange[1], 16, 64)
if err != nil {
return nil, err
}
hexadecimal = true
}
if start > end {
@@ -68,9 +79,17 @@ func parseEllipsesRange(pattern string) (seq []string, err error) {
for i := start; i <= end; i++ {
if strings.HasPrefix(ellipsesRange[0], "0") && len(ellipsesRange[0]) > 1 || strings.HasPrefix(ellipsesRange[1], "0") {
seq = append(seq, fmt.Sprintf(fmt.Sprintf("%%0%dd", len(ellipsesRange[1])), i))
if hexadecimal {
seq = append(seq, fmt.Sprintf(fmt.Sprintf("%%0%dx", len(ellipsesRange[1])), i))
} else {
seq = append(seq, fmt.Sprintf(fmt.Sprintf("%%0%dd", len(ellipsesRange[1])), i))
}
} else {
seq = append(seq, fmt.Sprintf("%d", i))
if hexadecimal {
seq = append(seq, fmt.Sprintf("%x", i))
} else {
seq = append(seq, fmt.Sprintf("%d", i))
}
}
}
+11
View File
@@ -96,6 +96,7 @@ func TestHasEllipses(t *testing.T) {
true,
},
{
[]string{
"mydisk-{1...4}{1..2.}",
},
@@ -201,6 +202,11 @@ func TestFindEllipsesPatterns(t *testing.T) {
false,
0,
},
{
"{f...z}",
false,
0,
},
// Test for valid input.
{
"{1...64}",
@@ -222,6 +228,11 @@ func TestFindEllipsesPatterns(t *testing.T) {
true,
36,
},
{
"{1...a}",
true,
10,
},
}
for i, testCase := range testCases {
+5 -1
View File
@@ -44,7 +44,7 @@ func (host Host) String() string {
return host.Name
}
return host.Name + ":" + host.Port.String()
return net.JoinHostPort(host.Name, host.Port.String())
}
// Equal - checks whether given host is equal or not.
@@ -129,6 +129,10 @@ func ParseHost(s string) (*Host, error) {
isPortSet = true
}
if i := strings.Index(host, "%"); i > -1 {
host = host[:i]
}
if !isValidHost(host) {
return nil, errors.New("invalid hostname")
}
+29 -5
View File
@@ -57,9 +57,12 @@ type Options struct {
// SQL expression meant to be evaluated.
Expression string
// What the outputted CSV will be delimited by .
// Output CSV will be delimited by.
OutputFieldDelimiter string
// Output CSV record will be delimited by.
OutputRecordDelimiter string
// Size of incoming object
StreamSize int64
@@ -68,6 +71,9 @@ type Options struct {
// Progress enabled, enable/disable progress messages.
Progress bool
// Output format type, supported values are CSV and JSON
OutputType format.Type
}
// cinput represents a record producing input from a formatted object.
@@ -147,6 +153,9 @@ func (reader *cinput) readHeader() error {
reader.firstRow = nil
} else {
reader.firstRow, readErr = reader.reader.Read()
if readErr != nil {
return format.ErrCSVParsingError
}
reader.header = make([]string, len(reader.firstRow))
for i := range reader.firstRow {
reader.header[i] = "_" + strconv.Itoa(i)
@@ -173,8 +182,13 @@ func (reader *cinput) Read() ([]byte, error) {
if dec != nil {
var data []byte
var err error
for i, value := range dec {
data, err = sjson.SetBytes(data, reader.header[i], value)
// Navigate column values in reverse order to preserve
// the input order for AWS S3 compatibility, because
// sjson adds json key/value pairs in first in last out
// fashion. This should be fixed in sjson ideally. Following
// work around is needed to circumvent this issue for now.
for i := len(dec) - 1; i >= 0; i-- {
data, err = sjson.SetBytes(data, reader.header[i], dec[i])
if err != nil {
return nil, err
}
@@ -184,11 +198,16 @@ func (reader *cinput) Read() ([]byte, error) {
return nil, nil
}
// OutputFieldDelimiter - returns the delimiter specified in input request
// OutputFieldDelimiter - returns the requested output field delimiter.
func (reader *cinput) OutputFieldDelimiter() string {
return reader.options.OutputFieldDelimiter
}
// OutputRecordDelimiter - returns the requested output record delimiter.
func (reader *cinput) OutputRecordDelimiter() string {
return reader.options.OutputRecordDelimiter
}
// HasHeader - returns true or false depending upon the header.
func (reader *cinput) HasHeader() bool {
return reader.options.HasHeader
@@ -285,11 +304,16 @@ func (reader *cinput) CreateProgressXML() (string, error) {
return xml.Header + string(out), nil
}
// Type - return the data format type {
// Type - return the data format type
func (reader *cinput) Type() format.Type {
return format.CSV
}
// OutputType - return the data format type
func (reader *cinput) OutputType() format.Type {
return reader.options.OutputType
}
// ColNameErrs is a function which makes sure that the headers are requested are
// present in the file otherwise it throws an error.
func (reader *cinput) ColNameErrs(columnNames []string) error {
+38 -10
View File
@@ -22,6 +22,7 @@ import (
"io"
"github.com/minio/minio/pkg/s3select/format"
"github.com/tidwall/gjson"
)
// Options options are passed to the underlying encoding/json reader.
@@ -40,24 +41,32 @@ type Options struct {
// SQL expression meant to be evaluated.
Expression string
// What the outputted will be delimited by .
// Input record delimiter.
RecordDelimiter string
// Output CSV will be delimited by.
OutputFieldDelimiter string
// Output record delimiter.
OutputRecordDelimiter string
// Size of incoming object
StreamSize int64
// True if Type is DOCUMENTS
Type bool
// True if DocumentType is DOCUMENTS
DocumentType bool
// Progress enabled, enable/disable progress messages.
Progress bool
// Output format type, supported values are CSV and JSON
OutputType format.Type
}
// jinput represents a record producing input from a formatted file or pipe.
type jinput struct {
options *Options
reader *bufio.Reader
firstRow []string
header []string
minOutputLength int
stats struct {
@@ -79,7 +88,6 @@ func New(opts *Options) (format.Select, error) {
reader.stats.BytesScanned = opts.StreamSize
reader.stats.BytesProcessed = 0
reader.stats.BytesReturned = 0
return reader, nil
}
@@ -95,7 +103,7 @@ func (reader *jinput) UpdateBytesProcessed(size int64) {
// Read the file and returns
func (reader *jinput) Read() ([]byte, error) {
data, err := reader.reader.ReadBytes('\n')
data, _, err := reader.reader.ReadLine()
if err != nil {
if err == io.EOF || err == io.ErrClosedPipe {
err = nil
@@ -103,17 +111,32 @@ func (reader *jinput) Read() ([]byte, error) {
err = format.ErrJSONParsingError
}
}
if err == nil {
var header []string
gjson.ParseBytes(data).ForEach(func(key, value gjson.Result) bool {
header = append(header, key.String())
return true
})
reader.header = header
}
return data, err
}
// OutputFieldDelimiter - returns the delimiter specified in input request
// OutputFieldDelimiter - returns the delimiter specified in input request,
// for JSON output this value is empty, but does have a value when
// output type is CSV.
func (reader *jinput) OutputFieldDelimiter() string {
return ","
return reader.options.OutputFieldDelimiter
}
// OutputRecordDelimiter - returns the delimiter specified in input request, after each JSON record.
func (reader *jinput) OutputRecordDelimiter() string {
return reader.options.OutputRecordDelimiter
}
// HasHeader - returns true or false depending upon the header.
func (reader *jinput) HasHeader() bool {
return false
return true
}
// Expression - return the Select Expression for
@@ -128,7 +151,7 @@ func (reader *jinput) UpdateBytesReturned(size int64) {
// Header returns a nil in case of
func (reader *jinput) Header() []string {
return nil
return reader.header
}
// CreateStatXML is the function which does the marshaling from the stat
@@ -171,6 +194,11 @@ func (reader *jinput) Type() format.Type {
return format.JSON
}
// OutputType - return the data format type {
func (reader *jinput) OutputType() format.Type {
return reader.options.OutputType
}
// ColNameErrs - this is a dummy function for JSON input type.
func (reader *jinput) ColNameErrs(columnNames []string) error {
return nil
+2
View File
@@ -22,10 +22,12 @@ import "encoding/xml"
// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectSELECTContent.html
type Select interface {
Type() Type
OutputType() Type
Read() ([]byte, error)
Header() []string
HasHeader() bool
OutputFieldDelimiter() string
OutputRecordDelimiter() string
UpdateBytesProcessed(int64)
Expression() string
UpdateBytesReturned(int64)
+47 -27
View File
@@ -65,40 +65,60 @@ func New(reader io.Reader, size int64, req ObjectSelectRequest) (s3s format.Sele
// Initializating options for CSV
if req.InputSerialization.CSV != nil {
if req.OutputSerialization.CSV.FieldDelimiter == "" {
req.OutputSerialization.CSV.FieldDelimiter = ","
}
if req.InputSerialization.CSV.FileHeaderInfo == "" {
req.InputSerialization.CSV.FileHeaderInfo = CSVFileHeaderInfoNone
}
if req.InputSerialization.CSV.RecordDelimiter == "" {
req.InputSerialization.CSV.RecordDelimiter = "\n"
}
s3s, err = csv.New(&csv.Options{
HasHeader: req.InputSerialization.CSV.FileHeaderInfo == CSVFileHeaderInfoUse,
RecordDelimiter: req.InputSerialization.CSV.RecordDelimiter,
FieldDelimiter: req.InputSerialization.CSV.FieldDelimiter,
Comments: req.InputSerialization.CSV.Comments,
Name: "S3Object", // Default table name for all objects
ReadFrom: reader,
Compressed: string(req.InputSerialization.CompressionType),
Expression: cleanExpr(req.Expression),
OutputFieldDelimiter: req.OutputSerialization.CSV.FieldDelimiter,
StreamSize: size,
HeaderOpt: req.InputSerialization.CSV.FileHeaderInfo == CSVFileHeaderInfoUse,
Progress: req.RequestProgress.Enabled,
})
options := &csv.Options{
Name: "S3Object", // Default table name for all objects
HasHeader: req.InputSerialization.CSV.FileHeaderInfo == CSVFileHeaderInfoUse,
RecordDelimiter: req.InputSerialization.CSV.RecordDelimiter,
FieldDelimiter: req.InputSerialization.CSV.FieldDelimiter,
Comments: req.InputSerialization.CSV.Comments,
ReadFrom: reader,
Compressed: string(req.InputSerialization.CompressionType),
Expression: cleanExpr(req.Expression),
StreamSize: size,
HeaderOpt: req.InputSerialization.CSV.FileHeaderInfo == CSVFileHeaderInfoUse,
Progress: req.RequestProgress.Enabled,
}
if req.OutputSerialization.CSV != nil {
if req.OutputSerialization.CSV.FieldDelimiter == "" {
req.OutputSerialization.CSV.FieldDelimiter = ","
}
options.OutputFieldDelimiter = req.OutputSerialization.CSV.FieldDelimiter
options.OutputRecordDelimiter = req.OutputSerialization.CSV.RecordDelimiter
options.OutputType = format.CSV
}
if req.OutputSerialization.JSON != nil {
options.OutputRecordDelimiter = req.OutputSerialization.JSON.RecordDelimiter
options.OutputType = format.JSON
}
// Initialize CSV input type
s3s, err = csv.New(options)
} else if req.InputSerialization.JSON != nil {
// Initializating options for JSON
s3s, err = json.New(&json.Options{
Name: "S3Object", // Default table name for all objects
ReadFrom: reader,
Compressed: string(req.InputSerialization.CompressionType),
Expression: cleanExpr(req.Expression),
StreamSize: size,
Type: req.InputSerialization.JSON.Type == JSONTypeDocument,
Progress: req.RequestProgress.Enabled,
})
options := &json.Options{
Name: "S3Object", // Default table name for all objects
ReadFrom: reader,
Compressed: string(req.InputSerialization.CompressionType),
Expression: cleanExpr(req.Expression),
StreamSize: size,
DocumentType: req.InputSerialization.JSON.Type == JSONTypeDocument,
Progress: req.RequestProgress.Enabled,
}
if req.OutputSerialization.JSON != nil {
options.OutputRecordDelimiter = req.OutputSerialization.JSON.RecordDelimiter
options.OutputType = format.JSON
}
if req.OutputSerialization.CSV != nil {
options.OutputFieldDelimiter = req.OutputSerialization.CSV.FieldDelimiter
options.OutputRecordDelimiter = req.OutputSerialization.CSV.RecordDelimiter
options.OutputType = format.CSV
}
// Initialize JSON input type
s3s, err = json.New(options)
}
return s3s, err
}
+27 -9
View File
@@ -201,14 +201,19 @@ func processSelectReq(reqColNames []string, alias string, wc sqlparser.Expr, lre
lrecords = math.MaxInt64
}
columnsKv, err := columnsIndex(reqColNames, f)
if err != nil {
rowCh <- Row{
err: err,
var results []string
var columnsKv []columnKv
if f.Type() == format.CSV {
var err error
columnsKv, err = columnsIndex(reqColNames, f)
if err != nil {
rowCh <- Row{
err: err,
}
return
}
return
results = make([]string, len(columnsKv))
}
var results = make([]string, len(columnsKv))
for {
record, err := f.Read()
@@ -228,6 +233,19 @@ func processSelectReq(reqColNames []string, alias string, wc sqlparser.Expr, lre
return
}
// For JSON multi-line input type columns needs
// to be handled for each record.
if f.Type() == format.JSON {
columnsKv, err = columnsIndex(reqColNames, f)
if err != nil {
rowCh <- Row{
err: err,
}
return
}
results = make([]string, len(columnsKv))
}
f.UpdateBytesProcessed(int64(len(record)))
// Return in case the number of record reaches the LIMIT
@@ -250,17 +268,17 @@ func processSelectReq(reqColNames []string, alias string, wc sqlparser.Expr, lre
if condition {
// if its an asterix we just print everything in the row
if reqColNames[0] == "*" && fnNames[0] == "" {
switch f.Type() {
switch f.OutputType() {
case format.CSV:
for i, kv := range columnsKv {
results[i] = gjson.GetBytes(record, kv.Key).String()
}
rowCh <- Row{
record: strings.Join(results, f.OutputFieldDelimiter()) + "\n",
record: strings.Join(results, f.OutputFieldDelimiter()) + f.OutputRecordDelimiter(),
}
case format.JSON:
rowCh <- Row{
record: string(record) + "\n",
record: string(record) + f.OutputRecordDelimiter(),
}
}
} else if alias != "" {