Compare commits

..

18 Commits

Author SHA1 Message Date
Aditya Manthramurthy be6ccd129d fix: typo in FIPS sha256 (#15024) 2022-06-01 19:11:04 -07:00
Klaus Post f7cecf0945 Make isIndexedMetaV2 return errors (#15012)
Indexed streams would be decoded by the legacy loader if there 
was an error loading it. Return an error when the stream is indexed 
and it cannot be loaded.

Fixes "unknown minor metadata version" on corrupted xl.meta files and 
returns an actual error.
2022-05-31 19:06:57 -07:00
Harshavardhana 7b2198f7e5 handle IPv6 sourceIPs properly (#15005) 2022-05-31 06:04:12 -07:00
Harshavardhana 52221db7ef fix: for unexpected errors in reading versioning config panic (#14994)
We need to make sure if we cannot read bucket metadata
for some reason, and bucket metadata is not missing and
returning corrupted information we should panic such
handlers to disallow I/O to protect the overall state
on the system.

In-case of such corruption we have a mechanism now
to force recreate the metadata on the bucket, using
`x-minio-force-create` header with `PUT /bucket` API
call.

Additionally fix the versioning config updated state
to be set properly for the site replication healing
to trigger correctly.
2022-05-31 02:57:57 -07:00
Harshavardhana befbf48563 fix: s3-check-md5 to not panic for incomplete md5 2022-05-30 20:58:42 -07:00
Anis Elleuch 56a61bab56 test: Add GetObjectNInfo test with some outdated disks (#15004)
Add a test reading an object which has some old data in some outdated
disks, in a versioned and non-versioned bucket.
2022-05-30 17:52:59 -07:00
Harshavardhana d480022711 fix: invalidate outdated disks appropriately during readAllXL (#15002)
readAllXL would return inlined data for outdated disks
causing "read" to return incorrect content to the client,

this PR fixes this behavior by making sure we skip such
outdated disks appropriately based on the latest ModTime
on the disk.
2022-05-30 12:43:54 -07:00
Harshavardhana f1abb92f0c feat: Single drive XL implementation (#14970)
Main motivation is move towards a common backend format
for all different types of modes in MinIO, allowing for
a simpler code and predictable behavior across all features.

This PR also brings features such as versioning, replication,
transitioning to single drive setups.
2022-05-30 10:58:37 -07:00
Harshavardhana 5792be71fa fix: add timeouts to avoid goroutine leaks in net/http (#14995)
Following code can reproduce an unending go-routine buildup,
while keeping connections established due to lack of client
not closing the connections.

https://gist.github.com/harshavardhana/2d00e6f909054d2d2524c71485ad02e1

Without this PR all MinIO deployments can be put into
denial of service attacks, causing entire service to be
unavailable.

We bring in two timeouts at this stage to control such
go-routine build ups, new change

- IdleTimeout (to kill off idle connections)
- ReadHeaderTimeout (to kill off connections that are too slow)

This new change also brings two hidden options to make any
additional relevant changes if desired in some setups.
2022-05-30 06:24:51 -07:00
Harshavardhana c2630bb3a3 add total usage pie chart based on total/free bytes 2022-05-28 09:53:53 -07:00
Poorna 5e3010d455 Tighten enforcement of object retention (#14993)
Ref issue#14991 - in the rare case that object in bucket under
retention has null version, make sure to enforce retention
rules.
2022-05-28 02:21:19 -07:00
Anis Elleuch ccbf65c8e8 site-repl: Fix deadlock after an IAM loading error (#14990)
Fix forgotten IAM cache lock releases when reading some data from
disk/etcd

Co-authored-by: Anis Elleuch <anis@min.io>
2022-05-27 10:26:38 -07:00
Harshavardhana 9d07cde385 use crypto/sha256 only for FIPS 140-2 compliance (#14983)
It would seem like the PR #11623 had chewed more
than it wanted to, non-fips build shouldn't really
be forced to use slower crypto/sha256 even for
presumed "non-performance" codepaths. In MinIO
there are really no "non-performance" codepaths.
This assumption seems to have had an adverse
effect in certain areas of CPU usage.

This PR ensures that we stick to sha256-simd
on all non-FIPS builds, our most common build
to ensure we get the best out of the CPU at
any given point in time.
2022-05-27 06:00:19 -07:00
Aditya Manthramurthy 464b9d7c80 Add support for Identity Management Plugin (#14913)
- Adds an STS API `AssumeRoleWithCustomToken` that can be used to 
  authenticate via the Id. Mgmt. Plugin.
- Adds a sample identity manager plugin implementation
- Add doc for plugin and STS API
- Add an example program using go SDK for AssumeRoleWithCustomToken
2022-05-26 17:58:09 -07:00
Poorna 5c81d0d89a site replication: heal missing/invalid replication config (#14979)
Validate remote target ARNs and heal any stale rules in
the replication config
2022-05-26 17:57:23 -07:00
Praveen raj Mani 62cd643868 Add --insecure flag to skip TLS verification in s3-md5-check tool (#14980) 2022-05-26 06:02:05 -07:00
Klaus Post c0bf02b8b2 Ignore disks with 0 total space (#14981)
Ignore disks with 0 total

Mainly defensive to ensure no `/0` in percent calculation.
2022-05-26 06:01:50 -07:00
Minio Trusted 1b7dd70f72 Update yaml files to latest version RELEASE.2022-05-26T05-48-41Z 2022-05-26 06:27:03 +00:00
109 changed files with 5763 additions and 536 deletions
+4 -4
View File
@@ -53,7 +53,7 @@ test-iam: build ## verify IAM (external IDP, etcd backends)
@echo "Running tests for IAM (external IDP, etcd backends)"
@CGO_ENABLED=0 go test -tags kqueue -v -run TestIAM* ./cmd
@echo "Running tests for IAM (external IDP, etcd backends) with -race"
@CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAM* ./cmd
@GORACE=history_size=7 CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAM* ./cmd
test-replication: install ## verify multi site replication
@echo "Running tests for replicating three sites"
@@ -73,18 +73,18 @@ test-site-replication-minio: install ## verify automatic site replication
verify: ## verify minio various setups
@echo "Verifying build with race"
@CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@(env bash $(PWD)/buildscripts/verify-build.sh)
verify-healing: ## verify healing and replacing disks with minio binary
@echo "Verify healing build with race"
@CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@(env bash $(PWD)/buildscripts/verify-healing.sh)
@(env bash $(PWD)/buildscripts/unaligned-healing.sh)
verify-healing-inconsistent-versions: ## verify resolving inconsistent versions
@echo "Verify resolving inconsistent versions build with race"
@CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
@(env bash $(PWD)/buildscripts/resolve-right-versions.sh)
build: checks ## builds minio to $(PWD)
-6
View File
@@ -196,12 +196,6 @@ iptables -A INPUT -p tcp --dport 9000:9010 -j ACCEPT
service iptables restart
```
## Pre-existing data
When deployed on a single drive, MinIO server lets clients access any pre-existing data in the data directory. For example, if MinIO is started with the command `minio server /mnt/data`, any pre-existing data in the `/mnt/data` directory would be accessible to the clients.
The above statement is also valid for all gateway backends.
## Test MinIO Connectivity
### Test using MinIO Console
+1
View File
@@ -2,6 +2,7 @@
set -e
export GORACE="history_size=7"
## TODO remove `dsync` from race detector once this is merged and released https://go-review.googlesource.com/c/go/+/333529/
for d in $(go list ./... | grep -v dsync); do
CGO_ENABLED=1 go test -v -race --timeout 100m "$d"
+4 -3
View File
@@ -145,7 +145,7 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
bucket := pathClean(vars["bucket"])
update := r.Form.Get("update") == "true"
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
@@ -274,7 +274,8 @@ func (a adminAPIHandlers) ListRemoteTargetsHandler(w http.ResponseWriter, r *htt
vars := mux.Vars(r)
bucket := pathClean(vars["bucket"])
arnType := vars["type"]
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
@@ -314,7 +315,7 @@ func (a adminAPIHandlers) RemoveRemoteTargetHandler(w http.ResponseWriter, r *ht
bucket := pathClean(vars["bucket"])
arn := vars["arn"]
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
+3
View File
@@ -33,6 +33,7 @@ import (
"github.com/minio/minio/internal/config/etcd"
xldap "github.com/minio/minio/internal/config/identity/ldap"
"github.com/minio/minio/internal/config/identity/openid"
idplugin "github.com/minio/minio/internal/config/identity/plugin"
polplugin "github.com/minio/minio/internal/config/policy/plugin"
"github.com/minio/minio/internal/config/storageclass"
"github.com/minio/minio/internal/logger"
@@ -444,6 +445,8 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
off = !xldap.Enabled(kv)
case config.IdentityTLSSubSys:
off = !globalSTSTLSConfig.Enabled
case config.IdentityPluginSubSys:
off = !idplugin.Enabled(kv)
}
if off {
s.WriteString(config.KvComment)
+4 -4
View File
@@ -47,10 +47,10 @@ func TestIAMInternalIDPConcurrencyServerSuite(t *testing.T) {
}
baseTestCases := []TestSuiteCommon{
// Init and run test on FS backend with signature v4.
{serverType: "FS", signer: signerV4},
// Init and run test on FS backend, with tls enabled.
{serverType: "FS", signer: signerV4, secure: true},
// Init and run test on ErasureSD backend with signature v4.
{serverType: "ErasureSD", signer: signerV4},
// Init and run test on ErasureSD backend, with tls enabled.
{serverType: "ErasureSD", signer: signerV4, secure: true},
// Init and run test on Erasure backend.
{serverType: "Erasure", signer: signerV4},
// Init and run test on ErasureSet backend.
+4 -4
View File
@@ -102,10 +102,10 @@ func (s *TestSuiteIAM) iamSetup(c *check) {
// common to tests.
var iamTestSuites = func() []*TestSuiteIAM {
baseTestCases := []TestSuiteCommon{
// Init and run test on FS backend with signature v4.
{serverType: "FS", signer: signerV4},
// Init and run test on FS backend, with tls enabled.
{serverType: "FS", signer: signerV4, secure: true},
// Init and run test on ErasureSD backend with signature v4.
{serverType: "ErasureSD", signer: signerV4},
// Init and run test on ErasureSD backend, with tls enabled.
{serverType: "ErasureSD", signer: signerV4, secure: true},
// Init and run test on Erasure backend.
{serverType: "Erasure", signer: signerV4},
// Init and run test on ErasureSet backend.
+4 -5
View File
@@ -805,8 +805,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Check if this setup has an erasure coded backend.
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrHealNotImplemented), r.URL)
return
}
@@ -998,7 +997,7 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *
}
// Check if this setup has an erasure coded backend.
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrHealNotImplemented), r.URL)
return
}
@@ -1078,7 +1077,7 @@ func (a adminAPIHandlers) ObjectSpeedtestHandler(w http.ResponseWriter, r *http.
return
}
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
@@ -1228,7 +1227,7 @@ func (a adminAPIHandlers) DriveSpeedtestHandler(w http.ResponseWriter, r *http.R
return
}
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
+38 -40
View File
@@ -170,50 +170,48 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
// Set Group Status
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetGroupStatus))).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
if globalIsDistErasure || globalIsErasure {
// GetBucketQuotaConfig
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
gz(httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
// PutBucketQuotaConfig
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-quota").HandlerFunc(
gz(httpTraceHdrs(adminAPI.PutBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
// GetBucketQuotaConfig
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
gz(httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
// PutBucketQuotaConfig
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-quota").HandlerFunc(
gz(httpTraceHdrs(adminAPI.PutBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
// Bucket replication operations
// GetBucketTargetHandler
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-remote-targets").HandlerFunc(
gz(httpTraceHdrs(adminAPI.ListRemoteTargetsHandler))).Queries("bucket", "{bucket:.*}", "type", "{type:.*}")
// SetRemoteTargetHandler
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-remote-target").HandlerFunc(
gz(httpTraceHdrs(adminAPI.SetRemoteTargetHandler))).Queries("bucket", "{bucket:.*}")
// RemoveRemoteTargetHandler
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-remote-target").HandlerFunc(
gz(httpTraceHdrs(adminAPI.RemoveRemoteTargetHandler))).Queries("bucket", "{bucket:.*}", "arn", "{arn:.*}")
// Bucket replication operations
// GetBucketTargetHandler
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-remote-targets").HandlerFunc(
gz(httpTraceHdrs(adminAPI.ListRemoteTargetsHandler))).Queries("bucket", "{bucket:.*}", "type", "{type:.*}")
// SetRemoteTargetHandler
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-remote-target").HandlerFunc(
gz(httpTraceHdrs(adminAPI.SetRemoteTargetHandler))).Queries("bucket", "{bucket:.*}")
// RemoveRemoteTargetHandler
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-remote-target").HandlerFunc(
gz(httpTraceHdrs(adminAPI.RemoveRemoteTargetHandler))).Queries("bucket", "{bucket:.*}", "arn", "{arn:.*}")
// Remote Tier management operations
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.AddTierHandler)))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.EditTierHandler)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListTierHandler)))
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.RemoveTierHandler)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.VerifyTierHandler)))
// Tier stats
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier-stats").HandlerFunc(gz(httpTraceHdrs(adminAPI.TierStatsHandler)))
// Remote Tier management operations
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.AddTierHandler)))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.EditTierHandler)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListTierHandler)))
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.RemoveTierHandler)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.VerifyTierHandler)))
// Tier stats
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier-stats").HandlerFunc(gz(httpTraceHdrs(adminAPI.TierStatsHandler)))
// Cluster Replication APIs
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/add").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationAdd)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/remove").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationRemove)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/info").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationInfo)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/metainfo").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationMetaInfo)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationStatus)))
// Cluster Replication APIs
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/add").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationAdd)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/remove").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationRemove)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/info").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationInfo)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/metainfo").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationMetaInfo)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationStatus)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/join").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerJoin)))
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/site-replication/peer/bucket-ops").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerBucketOps))).Queries("bucket", "{bucket:.*}").Queries("operation", "{operation:.*}")
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/iam-item").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerReplicateIAMItem)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/bucket-meta").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerReplicateBucketItem)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/peer/idp-settings").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerGetIDPSettings)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationEdit)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerEdit)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/remove").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerRemove)))
}
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/join").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerJoin)))
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/site-replication/peer/bucket-ops").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerBucketOps))).Queries("bucket", "{bucket:.*}").Queries("operation", "{operation:.*}")
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/iam-item").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerReplicateIAMItem)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/bucket-meta").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerReplicateBucketItem)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/peer/idp-settings").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerGetIDPSettings)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationEdit)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerEdit)))
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/remove").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerRemove)))
if globalIsDistErasure {
// Top locks
+1 -1
View File
@@ -19,7 +19,6 @@ package cmd
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
@@ -27,6 +26,7 @@ import (
"io"
"github.com/minio/highwayhash"
"github.com/minio/minio/internal/hash/sha256"
"golang.org/x/crypto/blake2b"
xioutil "github.com/minio/minio/internal/ioutil"
+5 -5
View File
@@ -527,7 +527,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
VersionSuspended: vc.Suspended(),
}
if replicateDeletes || object.VersionID != "" && hasLockEnabled || !globalTierConfigMgr.Empty() {
if replicateDeletes || hasLockEnabled || !globalTierConfigMgr.Empty() {
if !globalTierConfigMgr.Empty() && object.VersionID == "" && opts.VersionSuspended {
opts.VersionID = nullVersionID
}
@@ -556,7 +556,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
object.ReplicateDecisionStr = dsc.String()
}
}
if object.VersionID != "" && hasLockEnabled {
if hasLockEnabled {
if apiErrCode := enforceRetentionBypassForDelete(ctx, r, bucket, object, goi, gerr); apiErrCode != ErrNone {
apiErr := errorCodes.ToAPIErr(apiErrCode)
deleteResults[index].errInfo = DeleteError{
@@ -1364,7 +1364,7 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return
}
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
@@ -1611,7 +1611,7 @@ func (api objectAPIHandlers) PutBucketReplicationConfigHandler(w http.ResponseWr
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return
}
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
@@ -1636,7 +1636,7 @@ func (api objectAPIHandlers) PutBucketReplicationConfigHandler(w http.ResponseWr
writeErrorResponse(ctx, w, apiErr, r.URL)
return
}
sameTarget, apiErr := validateReplicationDestination(ctx, bucket, replicationConfig)
sameTarget, apiErr := validateReplicationDestination(ctx, bucket, replicationConfig, true)
if apiErr != noError {
writeErrorResponse(ctx, w, apiErr, r.URL)
return
+1 -1
View File
@@ -203,7 +203,7 @@ func TestObjectIsRemote(t *testing.T) {
if got := fi.IsRemote(); got != tc.remote {
t.Fatalf("Test %d.a: expected %v got %v", i+1, tc.remote, got)
}
oi := fi.ToObjectInfo("bucket", "object")
oi := fi.ToObjectInfo("bucket", "object", false)
if got := oi.IsRemote(); got != tc.remote {
t.Fatalf("Test %d.b: expected %v got %v", i+1, tc.remote, got)
}
+18 -33
View File
@@ -127,20 +127,12 @@ func (sys *BucketMetadataSys) Update(ctx context.Context, bucket string, configF
meta.QuotaConfigJSON = configData
meta.QuotaConfigUpdatedAt = UTCNow()
case objectLockConfig:
if !globalIsErasure && !globalIsDistErasure {
return NotImplemented{}
}
meta.ObjectLockConfigXML = configData
meta.ObjectLockConfigUpdatedAt = UTCNow()
case bucketVersioningConfig:
if !globalIsErasure && !globalIsDistErasure {
return NotImplemented{}
}
meta.VersioningConfigXML = configData
meta.VersioningConfigUpdatedAt = UTCNow()
case bucketReplicationConfig:
if !globalIsErasure && !globalIsDistErasure {
return NotImplemented{}
}
meta.ReplicationConfigXML = configData
meta.ReplicationConfigUpdatedAt = UTCNow()
case bucketTargetsFile:
@@ -193,12 +185,15 @@ func (sys *BucketMetadataSys) Get(bucket string) (BucketMetadata, error) {
// GetVersioningConfig returns configured versioning config
// The returned object may not be modified.
func (sys *BucketMetadataSys) GetVersioningConfig(bucket string) (*versioning.Versioning, error) {
func (sys *BucketMetadataSys) GetVersioningConfig(bucket string) (*versioning.Versioning, time.Time, error) {
meta, err := sys.GetConfig(GlobalContext, bucket)
if err != nil {
return &versioning.Versioning{}, err
if errors.Is(err, errConfigNotFound) {
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, meta.Created, nil
}
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, time.Time{}, err
}
return meta.versioningConfig, nil
return meta.versioningConfig, meta.VersioningConfigUpdatedAt, nil
}
// GetTaggingConfig returns configured tagging config
@@ -315,26 +310,27 @@ func (sys *BucketMetadataSys) CreatedAt(bucket string) (time.Time, error) {
// GetPolicyConfig returns configured bucket policy
// The returned object may not be modified.
func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, error) {
func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, time.Time, error) {
if globalIsGateway {
objAPI := newObjectLayerFn()
if objAPI == nil {
return nil, errServerNotInitialized
return nil, time.Time{}, errServerNotInitialized
}
return objAPI.GetBucketPolicy(GlobalContext, bucket)
p, err := objAPI.GetBucketPolicy(GlobalContext, bucket)
return p, UTCNow(), err
}
meta, err := sys.GetConfig(GlobalContext, bucket)
if err != nil {
if errors.Is(err, errConfigNotFound) {
return nil, BucketPolicyNotFound{Bucket: bucket}
return nil, time.Time{}, BucketPolicyNotFound{Bucket: bucket}
}
return nil, err
return nil, time.Time{}, err
}
if meta.policyConfig == nil {
return nil, BucketPolicyNotFound{Bucket: bucket}
return nil, time.Time{}, BucketPolicyNotFound{Bucket: bucket}
}
return meta.policyConfig, nil
return meta.policyConfig, meta.PolicyConfigUpdatedAt, nil
}
// GetQuotaConfig returns configured bucket quota
@@ -369,6 +365,9 @@ func (sys *BucketMetadataSys) GetReplicationConfig(ctx context.Context, bucket s
func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.BucketTargets, error) {
meta, err := sys.GetConfig(GlobalContext, bucket)
if err != nil {
if errors.Is(err, errConfigNotFound) {
return nil, BucketRemoteTargetNotFound{Bucket: bucket}
}
return nil, err
}
if meta.bucketTargetConfig == nil {
@@ -377,20 +376,6 @@ func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.Buc
return meta.bucketTargetConfig, nil
}
// GetBucketTarget returns the target for the bucket and arn.
func (sys *BucketMetadataSys) GetBucketTarget(bucket string, arn string) (madmin.BucketTarget, error) {
targets, err := sys.GetBucketTargetsConfig(bucket)
if err != nil {
return madmin.BucketTarget{}, err
}
for _, t := range targets.Targets {
if t.Arn == arn {
return t, nil
}
}
return madmin.BucketTarget{}, errConfigNotFound
}
// GetConfig returns a specific configuration from the bucket metadata.
// The returned object may not be modified.
func (sys *BucketMetadataSys) GetConfig(ctx context.Context, bucket string) (BucketMetadata, error) {
+6
View File
@@ -87,6 +87,7 @@ type BucketMetadata struct {
TaggingConfigUpdatedAt time.Time
QuotaConfigUpdatedAt time.Time
ReplicationConfigUpdatedAt time.Time
VersioningConfigUpdatedAt time.Time
// Unexported fields. Must be updated atomically.
policyConfig *policy.Policy
@@ -364,6 +365,7 @@ func (b *BucketMetadata) defaultTimestamps() {
if b.PolicyConfigUpdatedAt.IsZero() {
b.PolicyConfigUpdatedAt = b.Created
}
if b.EncryptionConfigUpdatedAt.IsZero() {
b.EncryptionConfigUpdatedAt = b.Created
}
@@ -383,6 +385,10 @@ func (b *BucketMetadata) defaultTimestamps() {
if b.ReplicationConfigUpdatedAt.IsZero() {
b.ReplicationConfigUpdatedAt = b.Created
}
if b.VersioningConfigUpdatedAt.IsZero() {
b.VersioningConfigUpdatedAt = b.Created
}
}
// Save config to supplied ObjectLayer api.
+30 -5
View File
@@ -144,6 +144,12 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) {
err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
return
}
case "VersioningConfigUpdatedAt":
z.VersioningConfigUpdatedAt, err = dc.ReadTime()
if err != nil {
err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
return
}
default:
err = dc.Skip()
if err != nil {
@@ -157,9 +163,9 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) {
// EncodeMsg implements msgp.Encodable
func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
// map header, size 20
// map header, size 21
// write "Name"
err = en.Append(0xde, 0x0, 0x14, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
err = en.Append(0xde, 0x0, 0x15, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
if err != nil {
return
}
@@ -358,15 +364,25 @@ func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
return
}
// write "VersioningConfigUpdatedAt"
err = en.Append(0xb9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
if err != nil {
return
}
err = en.WriteTime(z.VersioningConfigUpdatedAt)
if err != nil {
err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
return
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
// map header, size 20
// map header, size 21
// string "Name"
o = append(o, 0xde, 0x0, 0x14, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
o = append(o, 0xde, 0x0, 0x15, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
o = msgp.AppendString(o, z.Name)
// string "Created"
o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
@@ -425,6 +441,9 @@ func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
// string "ReplicationConfigUpdatedAt"
o = append(o, 0xba, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
o = msgp.AppendTime(o, z.ReplicationConfigUpdatedAt)
// string "VersioningConfigUpdatedAt"
o = append(o, 0xb9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
o = msgp.AppendTime(o, z.VersioningConfigUpdatedAt)
return
}
@@ -566,6 +585,12 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) {
err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
return
}
case "VersioningConfigUpdatedAt":
z.VersioningConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
if err != nil {
err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
return
}
default:
bts, err = msgp.Skip(bts)
if err != nil {
@@ -580,6 +605,6 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) {
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *BucketMetadata) Msgsize() (s int) {
s = 3 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + 28 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigMetaJSON) + 22 + msgp.TimeSize + 26 + msgp.TimeSize + 26 + msgp.TimeSize + 23 + msgp.TimeSize + 21 + msgp.TimeSize + 27 + msgp.TimeSize
s = 3 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + 28 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigMetaJSON) + 22 + msgp.TimeSize + 26 + msgp.TimeSize + 26 + msgp.TimeSize + 23 + msgp.TimeSize + 21 + msgp.TimeSize + 27 + msgp.TimeSize + 26 + msgp.TimeSize
return
}
+2 -1
View File
@@ -38,7 +38,8 @@ type PolicySys struct{}
// Get returns stored bucket policy
func (sys *PolicySys) Get(bucket string) (*policy.Policy, error) {
return globalBucketMetadataSys.GetPolicyConfig(bucket)
policy, _, err := globalBucketMetadataSys.GetPolicyConfig(bucket)
return policy, err
}
// IsAllowed - checks given policy args is allowed to continue the Rest API.
+31 -19
View File
@@ -87,7 +87,7 @@ func getReplicationConfig(ctx context.Context, bucketName string) (rc *replicati
// validateReplicationDestination returns error if replication destination bucket missing or not configured
// It also returns true if replication destination is same as this server.
func validateReplicationDestination(ctx context.Context, bucket string, rCfg *replication.Config) (bool, APIError) {
func validateReplicationDestination(ctx context.Context, bucket string, rCfg *replication.Config, checkRemote bool) (bool, APIError) {
var arns []string
if rCfg.RoleArn != "" {
arns = append(arns, rCfg.RoleArn)
@@ -96,26 +96,29 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
arns = append(arns, rule.Destination.String())
}
}
var sameTarget bool
for _, arnStr := range arns {
arn, err := madmin.ParseARN(arnStr)
if err != nil {
return false, errorCodes.ToAPIErrWithErr(ErrBucketRemoteArnInvalid, err)
return sameTarget, errorCodes.ToAPIErrWithErr(ErrBucketRemoteArnInvalid, err)
}
if arn.Type != madmin.ReplicationService {
return false, toAPIError(ctx, BucketRemoteArnTypeInvalid{Bucket: bucket})
return sameTarget, toAPIError(ctx, BucketRemoteArnTypeInvalid{Bucket: bucket})
}
clnt := globalBucketTargetSys.GetRemoteTargetClient(ctx, arnStr)
if clnt == nil {
return false, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
return sameTarget, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
}
if found, err := clnt.BucketExists(ctx, arn.Bucket); !found {
return false, errorCodes.ToAPIErrWithErr(ErrRemoteDestinationNotFoundError, err)
}
if ret, err := globalBucketObjectLockSys.Get(bucket); err == nil {
if ret.LockEnabled {
lock, _, _, _, err := clnt.GetObjectLockConfig(ctx, arn.Bucket)
if err != nil || lock != "Enabled" {
return false, errorCodes.ToAPIErrWithErr(ErrReplicationDestinationMissingLock, err)
if checkRemote { // validate remote bucket
if found, err := clnt.BucketExists(ctx, arn.Bucket); !found {
return sameTarget, errorCodes.ToAPIErrWithErr(ErrRemoteDestinationNotFoundError, err)
}
if ret, err := globalBucketObjectLockSys.Get(bucket); err == nil {
if ret.LockEnabled {
lock, _, _, _, err := clnt.GetObjectLockConfig(ctx, arn.Bucket)
if err != nil || lock != "Enabled" {
return sameTarget, errorCodes.ToAPIErrWithErr(ErrReplicationDestinationMissingLock, err)
}
}
}
}
@@ -123,12 +126,19 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
c, ok := globalBucketTargetSys.arnRemotesMap[arnStr]
if ok {
if c.EndpointURL().String() == clnt.EndpointURL().String() {
sameTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort)
return sameTarget, toAPIError(ctx, nil)
selfTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort)
if !sameTarget {
sameTarget = selfTarget
}
continue
}
}
}
return false, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
if len(arns) == 0 {
return false, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
}
return sameTarget, toAPIError(ctx, nil)
}
type mustReplicateOptions struct {
@@ -180,11 +190,17 @@ func mustReplicate(ctx context.Context, bucket, object string, mopts mustReplica
return
}
// object layer not initialized we return with no decision.
if newObjectLayerFn() == nil {
return
}
// Disable server-side replication on object prefixes which are excluded
// from versioning via the MinIO bucket versioning extension.
if globalBucketVersioningSys.PrefixSuspended(bucket, object) {
return
}
replStatus := mopts.ReplicationStatus()
if replStatus == replication.Replica && !mopts.isMetadataReplication() {
return
@@ -2177,10 +2193,6 @@ func (p *ReplicationPool) initResync(ctx context.Context, buckets []BucketInfo,
if objAPI == nil {
return errServerNotInitialized
}
// replication applies only to erasure coded setups
if !globalIsErasure {
return nil
}
// Load bucket metadata sys in background
go p.loadResync(ctx, buckets, objAPI)
return nil
-6
View File
@@ -120,9 +120,6 @@ func (sys *BucketTargetSys) SetTarget(ctx context.Context, bucket string, tgt *m
return BucketRemoteConnectionErr{Bucket: tgt.TargetBucket, Err: err}
}
if tgt.Type == madmin.ReplicationService {
if !globalIsErasure {
return NotImplemented{Message: "Replication is not implemented in " + getMinioMode()}
}
if !globalBucketVersioningSys.Enabled(bucket) {
return BucketReplicationSourceNotVersioned{Bucket: bucket}
}
@@ -184,9 +181,6 @@ func (sys *BucketTargetSys) RemoveTarget(ctx context.Context, bucket, arnStr str
if globalIsGateway {
return nil
}
if !globalIsErasure {
return NotImplemented{Message: "Replication is not implemented in " + getMinioMode()}
}
if arnStr == "" {
return BucketRemoteArnInvalid{Bucket: bucket}
+24 -14
View File
@@ -17,16 +17,21 @@
package cmd
import "github.com/minio/minio/internal/bucket/versioning"
import (
"strings"
"github.com/minio/minio/internal/bucket/versioning"
"github.com/minio/minio/internal/logger"
)
// BucketVersioningSys - policy subsystem.
type BucketVersioningSys struct{}
// Enabled enabled versioning?
func (sys *BucketVersioningSys) Enabled(bucket string) bool {
vc, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
vc, err := sys.Get(bucket)
if err != nil {
return false
logger.CriticalIf(GlobalContext, err)
}
return vc.Enabled()
}
@@ -35,18 +40,18 @@ func (sys *BucketVersioningSys) Enabled(bucket string) bool {
// the given prefix doesn't match any excluded prefixes pattern. This is
// part of a MinIO versioning configuration extension.
func (sys *BucketVersioningSys) PrefixEnabled(bucket, prefix string) bool {
vc, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
vc, err := sys.Get(bucket)
if err != nil {
return false
logger.CriticalIf(GlobalContext, err)
}
return vc.PrefixEnabled(prefix)
}
// Suspended suspended versioning?
func (sys *BucketVersioningSys) Suspended(bucket string) bool {
vc, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
vc, err := sys.Get(bucket)
if err != nil {
return false
logger.CriticalIf(GlobalContext, err)
}
return vc.Suspended()
}
@@ -54,9 +59,9 @@ func (sys *BucketVersioningSys) Suspended(bucket string) bool {
// PrefixSuspended returns true if the given prefix matches an excluded prefix
// pattern. This is part of a MinIO versioning configuration extension.
func (sys *BucketVersioningSys) PrefixSuspended(bucket, prefix string) bool {
vc, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
vc, err := sys.Get(bucket)
if err != nil {
return false
logger.CriticalIf(GlobalContext, err)
}
return vc.PrefixSuspended(prefix)
@@ -64,12 +69,17 @@ func (sys *BucketVersioningSys) PrefixSuspended(bucket, prefix string) bool {
// Get returns stored bucket policy
func (sys *BucketVersioningSys) Get(bucket string) (*versioning.Versioning, error) {
return globalBucketMetadataSys.GetVersioningConfig(bucket)
}
if globalIsGateway {
// Gateway does not implement versioning.
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, nil
}
// Reset BucketVersioningSys to initial state.
func (sys *BucketVersioningSys) Reset() {
// There is currently no internal state.
if bucket == minioMetaBucket || strings.HasPrefix(bucket, minioMetaBucket) {
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, nil
}
vcfg, _, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
return vcfg, err
}
// NewBucketVersioningSys - creates new versioning system.
+25 -6
View File
@@ -34,6 +34,7 @@ import (
"github.com/minio/minio/internal/config/heal"
xldap "github.com/minio/minio/internal/config/identity/ldap"
"github.com/minio/minio/internal/config/identity/openid"
idplugin "github.com/minio/minio/internal/config/identity/plugin"
xtls "github.com/minio/minio/internal/config/identity/tls"
"github.com/minio/minio/internal/config/notify"
"github.com/minio/minio/internal/config/policy/opa"
@@ -56,6 +57,7 @@ func initHelp() {
config.IdentityLDAPSubSys: xldap.DefaultKVS,
config.IdentityOpenIDSubSys: openid.DefaultKVS,
config.IdentityTLSSubSys: xtls.DefaultKVS,
config.IdentityPluginSubSys: idplugin.DefaultKVS,
config.PolicyOPASubSys: opa.DefaultKVS,
config.PolicyPluginSubSys: polplugin.DefaultKVS,
config.SiteSubSys: config.DefaultSiteKVS,
@@ -108,6 +110,10 @@ func initHelp() {
Key: config.IdentityTLSSubSys,
Description: "enable X.509 TLS certificate SSO support",
},
config.HelpKV{
Key: config.IdentityPluginSubSys,
Description: "enable Identity Plugin via external hook",
},
config.HelpKV{
Key: config.PolicyPluginSubSys,
Description: "enable Access Management Plugin for policy enforcement",
@@ -220,6 +226,7 @@ func initHelp() {
config.IdentityOpenIDSubSys: openid.Help,
config.IdentityLDAPSubSys: xldap.Help,
config.IdentityTLSSubSys: xtls.Help,
config.IdentityPluginSubSys: idplugin.Help,
config.PolicyOPASubSys: opa.Help,
config.PolicyPluginSubSys: polplugin.Help,
config.LoggerWebhookSubSys: logger.Help,
@@ -342,6 +349,11 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
if _, err := xtls.Lookup(s[config.IdentityTLSSubSys][config.Default]); err != nil {
return err
}
case config.IdentityPluginSubSys:
if _, err := idplugin.LookupConfig(s[config.IdentityPluginSubSys][config.Default],
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region); err != nil {
return err
}
case config.SubnetSubSys:
if _, err := subnet.LookupConfig(s[config.SubnetSubSys][config.Default]); err != nil {
return err
@@ -541,6 +553,19 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
}
globalLDAPConfig, err = xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
globalRootCAs)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to parse LDAP configuration: %w", err))
}
authNPluginCfg, err := idplugin.LookupConfig(s[config.IdentityPluginSubSys][config.Default],
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize AuthNPlugin: %w", err))
}
globalAuthNPlugin = idplugin.New(authNPluginCfg)
authZPluginCfg, err := polplugin.LookupConfig(s[config.PolicyPluginSubSys][config.Default],
NewGatewayHTTPTransport(), xhttp.DrainBody)
if err != nil {
@@ -561,12 +586,6 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
setGlobalAuthZPlugin(polplugin.New(authZPluginCfg))
globalLDAPConfig, err = xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
globalRootCAs)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to parse LDAP configuration: %w", err))
}
globalSubnetConfig, err = subnet.LookupConfig(s[config.SubnetSubSys][config.Default])
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to parse subnet configuration: %w", err))
+5 -1
View File
@@ -1057,9 +1057,13 @@ func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ Ob
fivs = fivs[:lim+1]
rcfg, _ := globalBucketObjectLockSys.Get(i.bucket)
vcfg, _ := globalBucketVersioningSys.Get(i.bucket)
versioned := vcfg != nil && vcfg.Versioned(i.objectPath())
toDel := make([]ObjectToDelete, 0, len(overflowVersions))
for _, fi := range overflowVersions {
obj := fi.ToObjectInfo(i.bucket, i.objectPath())
obj := fi.ToObjectInfo(i.bucket, i.objectPath(), versioned)
// skip versions with object locking enabled
if rcfg.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
if i.debug {
+1 -1
View File
@@ -582,7 +582,7 @@ func CreateEndpoints(serverAddr string, foundLocal bool, args ...[]string) (Endp
return endpoints, setupType, config.ErrInvalidFSEndpoint(nil).Msg("use path style endpoint for FS setup")
}
endpoints = append(endpoints, endpoint)
setupType = FSSetupType
setupType = ErasureSDSetupType
// Check for cross device mounts if any.
if err = checkCrossDeviceMounts(endpoints); err != nil {
+3 -3
View File
@@ -231,10 +231,10 @@ func TestCreateEndpoints(t *testing.T) {
}{
{"localhost", [][]string{}, "", Endpoints{}, -1, fmt.Errorf("address localhost: missing port in address")},
// FS Setup
// Erasure Single Drive
{"localhost:9000", [][]string{{"http://localhost/d1"}}, "", Endpoints{}, -1, fmt.Errorf("use path style endpoint for FS setup")},
{":443", [][]string{{"/d1"}}, ":443", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, FSSetupType, nil},
{"localhost:10000", [][]string{{"/d1"}}, "localhost:10000", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, FSSetupType, nil},
{":443", [][]string{{"/d1"}}, ":443", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, ErasureSDSetupType, nil},
{"localhost:10000", [][]string{{"/d1"}}, "localhost:10000", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, ErasureSDSetupType, nil},
{"localhost:9000", [][]string{{"https://127.0.0.1:9000/d1", "https://localhost:9001/d1", "https://example.com/d1", "https://example.com/d2"}}, "", Endpoints{}, -1, fmt.Errorf("path '/d1' can not be served by different port on same address")},
// Erasure Setup with PathEndpointType
+1 -1
View File
@@ -41,7 +41,7 @@ type Erasure struct {
// NewErasure creates a new ErasureStorage.
func NewErasure(ctx context.Context, dataBlocks, parityBlocks int, blockSize int64) (e Erasure, err error) {
// Check the parameters for sanity now.
if dataBlocks <= 0 || parityBlocks <= 0 {
if dataBlocks <= 0 || parityBlocks < 0 {
return e, reedsolomon.ErrInvShardNum
}
+9 -5
View File
@@ -19,7 +19,6 @@ package cmd
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
@@ -28,6 +27,7 @@ import (
"time"
"github.com/minio/minio/internal/bucket/replication"
"github.com/minio/minio/internal/hash/sha256"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/sync/errgroup"
@@ -99,15 +99,15 @@ func (fi FileInfo) IsValid() bool {
fi.Erasure.Index <= dataBlocks+parityBlocks &&
len(fi.Erasure.Distribution) == (dataBlocks+parityBlocks))
return ((dataBlocks >= parityBlocks) &&
(dataBlocks != 0) && (parityBlocks != 0) &&
(dataBlocks > 0) && (parityBlocks >= 0) &&
correctIndexes)
}
// ToObjectInfo - Converts metadata to object info.
func (fi FileInfo) ToObjectInfo(bucket, object string) ObjectInfo {
func (fi FileInfo) ToObjectInfo(bucket, object string, versioned bool) ObjectInfo {
object = decodeDirObject(object)
versionID := fi.VersionID
if (globalBucketVersioningSys.PrefixEnabled(bucket, object) || globalBucketVersioningSys.PrefixSuspended(bucket, object)) && versionID == "" {
if versioned && versionID == "" {
versionID = nullVersionID
}
@@ -284,7 +284,7 @@ func (fi FileInfo) ObjectToPartOffset(ctx context.Context, offset int64) (partIn
func findFileInfoInQuorum(ctx context.Context, metaArr []FileInfo, modTime time.Time, quorum int) (FileInfo, error) {
// with less quorum return error.
if quorum < 2 {
if quorum < 1 {
return FileInfo{}, errErasureReadQuorum
}
metaHashes := make([]string, len(metaArr))
@@ -398,6 +398,10 @@ func writeUniqueFileInfo(ctx context.Context, disks []StorageAPI, bucket, prefix
// readQuorum is the min required disks to read data.
// writeQuorum is the min required disks to write data.
func objectQuorumFromMeta(ctx context.Context, partsMetaData []FileInfo, errs []error, defaultParityCount int) (objectReadQuorum, objectWriteQuorum int, err error) {
if defaultParityCount == 0 {
return 1, 1, nil
}
// get the latest updated Metadata and a count of all the latest updated FileInfo(s)
latestFileInfo, err := getLatestFileInfo(ctx, partsMetaData, errs)
if err != nil {
+1 -1
View File
@@ -1027,7 +1027,7 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
fi.IsLatest = true
// Success, return object info.
return fi.ToObjectInfo(bucket, object), nil
return fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
}
// AbortMultipartUpload - aborts an ongoing multipart operation
+47 -18
View File
@@ -117,7 +117,7 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
if srcOpts.VersionID == "" {
return oi, toObjectErr(errFileNotFound, srcBucket, srcObject)
}
return fi.ToObjectInfo(srcBucket, srcObject), toObjectErr(errMethodNotAllowed, srcBucket, srcObject)
return fi.ToObjectInfo(srcBucket, srcObject, srcOpts.Versioned || srcOpts.VersionSuspended), toObjectErr(errMethodNotAllowed, srcBucket, srcObject)
}
filterOnlineDisksInplace(fi, metaArr, onlineDisks)
@@ -178,7 +178,7 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
return oi, toObjectErr(err, srcBucket, srcObject)
}
return fi.ToObjectInfo(srcBucket, srcObject), nil
return fi.ToObjectInfo(srcBucket, srcObject, srcOpts.Versioned || srcOpts.VersionSuspended), nil
}
// GetObjectNInfo - returns object info and an object
@@ -245,7 +245,7 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri
}
}
objInfo := fi.ToObjectInfo(bucket, object)
objInfo := fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
if objInfo.DeleteMarker {
if opts.VersionID == "" {
return &GetObjectReader{
@@ -509,23 +509,45 @@ func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, r
}
readQuorum := (len(disks) + 1) / 2
merged := mergeXLV2Versions(readQuorum, false, 1, metadataShallowVersions...)
meta := &xlMetaV2{versions: mergeXLV2Versions(readQuorum, false, 1, metadataShallowVersions...)}
lfi, err := meta.ToFileInfo(bucket, object, "")
if err != nil {
for i := range errs {
if errs[i] == nil {
errs[i] = err
}
}
return metaFileInfos, errs
}
if !lfi.IsValid() {
for i := range errs {
if errs[i] == nil {
errs[i] = errCorruptedFormat
}
}
return metaFileInfos, errs
}
for index := range metadataArray {
if metadataArray[index] == nil {
continue
}
metadataArray[index].versions = merged
// make sure to preserve this for diskmtime based healing bugfix.
diskMTime := metaFileInfos[index].DiskMTime
metaFileInfos[index], errs[index] = metadataArray[index].ToFileInfo(bucket, object, "")
if errs[index] == nil {
if errs[index] != nil {
continue
}
if metaFileInfos[index].IsValid() && metaFileInfos[index].ModTime.Equal(lfi.ModTime) {
versionID := metaFileInfos[index].VersionID
if versionID == "" {
versionID = nullVersionID
}
metaFileInfos[index].Data = metadataArray[index].data.find(versionID)
if readData {
metaFileInfos[index].Data = metadataArray[index].data.find(versionID)
}
metaFileInfos[index].DiskMTime = diskMTime
}
}
@@ -593,6 +615,8 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
if metaArr[i].IsValid() && metaArr[i].ModTime.Equal(fi.ModTime) {
continue
}
metaArr[i] = FileInfo{}
onlineDisks[i] = nil
missingBlocks++
}
@@ -614,7 +638,7 @@ func (er erasureObjects) getObjectInfo(ctx context.Context, bucket, object strin
if err != nil {
return objInfo, toObjectErr(err, bucket, object)
}
objInfo = fi.ToObjectInfo(bucket, object)
objInfo = fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
if fi.Deleted {
if opts.VersionID == "" || opts.DeleteMarker {
return objInfo, toObjectErr(errFileNotFound, bucket, object)
@@ -638,7 +662,7 @@ func (er erasureObjects) getObjectInfoAndQuorum(ctx context.Context, bucket, obj
wquorum++
}
objInfo = fi.ToObjectInfo(bucket, object)
objInfo = fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
if !fi.VersionPurgeStatus().Empty() && opts.VersionID != "" {
// Make sure to return object info to provide extra information.
return objInfo, wquorum, toObjectErr(errMethodNotAllowed, bucket, object)
@@ -830,7 +854,7 @@ func (er erasureObjects) putMetacacheObject(ctx context.Context, key string, r *
return ObjectInfo{}, toObjectErr(err, minioMetaBucket, key)
}
return fi.ToObjectInfo(minioMetaBucket, key), nil
return fi.ToObjectInfo(minioMetaBucket, key, opts.Versioned || opts.VersionSuspended), nil
}
// PutObject - creates an object upon reading from the input stream
@@ -1135,7 +1159,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
// we are adding a new version to this object under the namespace lock, so this is the latest version.
fi.IsLatest = true
return fi.ToObjectInfo(bucket, object), nil
return fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
}
func (er erasureObjects) deleteObjectVersion(ctx context.Context, bucket, object string, writeQuorum int, fi FileInfo, forceDelMarker bool) error {
@@ -1327,12 +1351,17 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
func (er erasureObjects) deletePrefix(ctx context.Context, bucket, prefix string) error {
disks := er.getDisks()
g := errgroup.WithNErrs(len(disks))
dirPrefix := encodeDirObject(prefix)
for index := range disks {
index := index
g.Go(func() error {
if disks[index] == nil {
return nil
}
// Deletes
// - The prefix and its children
// - The prefix__XLDIR__
defer disks[index].Delete(ctx, bucket, dirPrefix, true)
return disks[index].Delete(ctx, bucket, prefix, true)
}, index)
}
@@ -1504,7 +1533,7 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
if err = er.deleteObjectVersion(ctx, bucket, object, writeQuorum, fi, opts.DeleteMarker); err != nil {
return objInfo, toObjectErr(err, bucket, object)
}
return fi.ToObjectInfo(bucket, object), nil
return fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
}
}
@@ -1609,7 +1638,7 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
opts.VersionID = fi.VersionID
}
objInfo := fi.ToObjectInfo(bucket, object)
objInfo := fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
if opts.EvalMetadataFn != nil {
if err := opts.EvalMetadataFn(objInfo); err != nil {
return ObjectInfo{}, err
@@ -1625,7 +1654,7 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
return ObjectInfo{}, toObjectErr(err, bucket, object)
}
return fi.ToObjectInfo(bucket, object), nil
return fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
}
// PutObjectTags - replace or add tags to an existing object
@@ -1689,7 +1718,7 @@ func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object strin
return ObjectInfo{}, toObjectErr(err, bucket, object)
}
return fi.ToObjectInfo(bucket, object), nil
return fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
}
// updateObjectMeta will update the metadata of a file.
@@ -1827,7 +1856,7 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
break
}
objInfo := fi.ToObjectInfo(bucket, object)
objInfo := fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
sendEvent(eventArgs{
EventName: eventName,
BucketName: bucket,
@@ -1882,7 +1911,7 @@ func (er erasureObjects) restoreTransitionedObject(ctx context.Context, bucket s
return setRestoreHeaderFn(oi, toObjectErr(err, bucket, object))
}
oi = actualfi.ToObjectInfo(bucket, object)
oi = actualfi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
ropts := putRestoreOpts(bucket, object, opts.Transition.RestoreRequest, oi)
if len(oi.Parts) == 1 {
var rs *HTTPRangeSpec
+94
View File
@@ -1111,3 +1111,97 @@ func TestGetObjectInlineNotInline(t *testing.T) {
t.Fatalf("Expected data to have md5sum = `%s`, found `%s`", expectedHash, foundHash)
}
}
// Test reading an object with some outdated data in some disks
func TestGetObjectWithOutdatedDisks(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Create an instance of xl backend.
obj, fsDirs, err := prepareErasure(ctx, 6)
if err != nil {
t.Fatal(err)
}
// Cleanup backend directories.
defer obj.Shutdown(context.Background())
defer removeRoots(fsDirs)
z := obj.(*erasureServerPools)
sets := z.serverPools[0]
xl := sets.sets[0]
origErasureDisks := xl.getDisks()
testCases := []struct {
bucket string
versioned bool
object string
content []byte
}{
{"bucket1", false, "object1", []byte("aaaaaaaaaaaaaaaa")},
{"bucket2", false, "object2", bytes.Repeat([]byte{'a'}, smallFileThreshold*2)},
{"bucket3", true, "version1", []byte("aaaaaaaaaaaaaaaa")},
{"bucket4", true, "version2", bytes.Repeat([]byte{'a'}, smallFileThreshold*2)},
}
for i, testCase := range testCases {
// Step 1: create a bucket
err = z.MakeBucketWithLocation(ctx, testCase.bucket, BucketOptions{VersioningEnabled: testCase.versioned})
if err != nil {
t.Fatalf("Test %d: Failed to create a bucket: %v", i+1, err)
}
// Step 2: Upload an object with a random content
initialData := bytes.Repeat([]byte{'b'}, len(testCase.content))
sets.erasureDisksMu.Lock()
xl.getDisks = func() []StorageAPI { return origErasureDisks }
sets.erasureDisksMu.Unlock()
_, err = z.PutObject(ctx, testCase.bucket, testCase.object, mustGetPutObjReader(t, bytes.NewReader(initialData), int64(len(initialData)), "", ""),
ObjectOptions{Versioned: testCase.versioned})
if err != nil {
t.Fatalf("Test %d: Failed to upload a random object: %v", i+1, err)
}
// Step 3: Upload the object with some disks offline
sets.erasureDisksMu.Lock()
xl.getDisks = func() []StorageAPI {
disks := make([]StorageAPI, len(origErasureDisks))
copy(disks, origErasureDisks)
disks[0] = nil
disks[1] = nil
return disks
}
sets.erasureDisksMu.Unlock()
_, err = z.PutObject(ctx, testCase.bucket, testCase.object, mustGetPutObjReader(t, bytes.NewReader(testCase.content), int64(len(testCase.content)), "", ""),
ObjectOptions{Versioned: testCase.versioned})
if err != nil {
t.Fatalf("Test %d: Failed to upload the final object: %v", i+1, err)
}
// Step 4: Try to read the object back and check its md5sum
sets.erasureDisksMu.Lock()
xl.getDisks = func() []StorageAPI { return origErasureDisks }
sets.erasureDisksMu.Unlock()
gr, err := z.GetObjectNInfo(ctx, testCase.bucket, testCase.object, nil, nil, readLock, ObjectOptions{})
if err != nil {
t.Fatalf("Expected GetObject to succeed, but failed with %v", err)
}
h := md5.New()
h.Write(testCase.content)
expectedHash := h.Sum(nil)
h.Reset()
_, err = io.Copy(h, gr)
if err != nil {
t.Fatalf("Test %d: Failed to calculate md5sum of the object: %v", i+1, err)
}
gr.Close()
foundHash := h.Sum(nil)
if !bytes.Equal(foundHash, expectedHash) {
t.Fatalf("Test %d: Expected data to have md5sum = `%x`, found `%x`", i+1, expectedHash, foundHash)
}
}
}
+24 -6
View File
@@ -61,6 +61,22 @@ func (z *erasureServerPools) SinglePool() bool {
// Initialize new pool of erasure sets.
func newErasureServerPools(ctx context.Context, endpointServerPools EndpointServerPools) (ObjectLayer, error) {
if endpointServerPools.NEndpoints() == 1 {
ep := endpointServerPools[0]
storageDisks, format, err := waitForFormatErasure(true, ep.Endpoints, 1, ep.SetCount, ep.DrivesPerSet, "", "")
if err != nil {
return nil, err
}
objLayer, err := newErasureSingle(ctx, storageDisks[0], format)
if err != nil {
return nil, err
}
globalLocalDrives = storageDisks
return objLayer, nil
}
var (
deploymentID string
distributionAlgo string
@@ -320,7 +336,7 @@ func (z *erasureServerPools) getServerPoolsAvailableSpace(ctx context.Context, b
nSets[index] = pool.setCount
g.Go(func() error {
// Get the set where it would be placed.
storageInfos[index] = getDiskInfos(ctx, pool.getHashedSet(object).getDisks())
storageInfos[index] = getDiskInfos(ctx, pool.getHashedSet(object).getDisks()...)
return nil
}, index)
}
@@ -336,7 +352,7 @@ func (z *erasureServerPools) getServerPoolsAvailableSpace(ctx context.Context, b
}
var maxUsedPct int
for _, disk := range zinfo {
if disk == nil {
if disk == nil || disk.Total == 0 {
continue
}
available += disk.Total - disk.Used
@@ -933,7 +949,7 @@ func (z *erasureServerPools) PutObject(ctx context.Context, bucket string, objec
object = encodeDirObject(object)
if z.SinglePool() {
if !isMinioMetaBucketName(bucket) && !hasSpaceFor(getDiskInfos(ctx, z.serverPools[0].getHashedSet(object).getDisks()), data.Size()) {
if !isMinioMetaBucketName(bucket) && !hasSpaceFor(getDiskInfos(ctx, z.serverPools[0].getHashedSet(object).getDisks()...), data.Size()) {
return ObjectInfo{}, toObjectErr(errDiskFull)
}
return z.serverPools[0].PutObject(ctx, bucket, object, data, opts)
@@ -1325,7 +1341,7 @@ func (z *erasureServerPools) NewMultipartUpload(ctx context.Context, bucket, obj
}
if z.SinglePool() {
if !isMinioMetaBucketName(bucket) && !hasSpaceFor(getDiskInfos(ctx, z.serverPools[0].getHashedSet(object).getDisks()), -1) {
if !isMinioMetaBucketName(bucket) && !hasSpaceFor(getDiskInfos(ctx, z.serverPools[0].getHashedSet(object).getDisks()...), -1) {
return "", toObjectErr(errDiskFull)
}
return z.serverPools[0].NewMultipartUpload(ctx, bucket, object, opts)
@@ -1740,6 +1756,8 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
defer cancel()
defer close(results)
versioned := opts.Versioned || opts.VersionSuspended
for _, erasureSet := range z.serverPools {
var wg sync.WaitGroup
for _, set := range erasureSet.sets {
@@ -1767,12 +1785,12 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
if opts.WalkAscending {
for i := len(fivs.Versions) - 1; i >= 0; i-- {
version := fivs.Versions[i]
results <- version.ToObjectInfo(bucket, version.Name)
results <- version.ToObjectInfo(bucket, version.Name, versioned)
}
return
}
for _, version := range fivs.Versions {
results <- version.ToObjectInfo(bucket, version.Name)
results <- version.ToObjectInfo(bucket, version.Name, versioned)
}
}
+1 -1
View File
@@ -1252,7 +1252,7 @@ func (s *erasureSets) HealFormat(ctx context.Context, dryRun bool) (res madmin.H
defer func(storageDisks []StorageAPI) {
if err != nil {
closeStorageDisks(storageDisks)
closeStorageDisks(storageDisks...)
}
}(storageDisks)
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -79,7 +79,7 @@ func (er erasureObjects) NewNSLock(bucket string, objects ...string) RWLocker {
// Shutdown function for object storage interface.
func (er erasureObjects) Shutdown(ctx context.Context) error {
// Add any object layer shutdown activities here.
closeStorageDisks(er.getDisks())
closeStorageDisks(er.getDisks()...)
return nil
}
+11 -5
View File
@@ -40,6 +40,9 @@ const (
// Represents Erasure backend.
formatBackendErasure = "xl"
// Represents Erasure backend - single drive
formatBackendErasureSingle = "xl-single"
// formatErasureV1.Erasure.Version - version '1'.
formatErasureVersionV1 = "1"
@@ -146,6 +149,9 @@ func newFormatErasureV3(numSets int, setLen int) *formatErasureV3 {
format := &formatErasureV3{}
format.Version = formatMetaVersionV1
format.Format = formatBackendErasure
if setLen == 1 {
format.Format = formatBackendErasureSingle
}
format.ID = mustGetUUID()
format.Erasure.Version = formatErasureVersionV3
format.Erasure.DistributionAlgo = formatErasureVersionV3DistributionAlgoV3
@@ -170,8 +176,8 @@ func formatGetBackendErasureVersion(b []byte) (string, error) {
if meta.Version != formatMetaVersionV1 {
return "", fmt.Errorf(`format.Version expected: %s, got: %s`, formatMetaVersionV1, meta.Version)
}
if meta.Format != formatBackendErasure {
return "", fmt.Errorf(`found backend type %s, expected %s`, meta.Format, formatBackendErasure)
if meta.Format != formatBackendErasure && meta.Format != formatBackendErasureSingle {
return "", fmt.Errorf(`found backend type %s, expected %s or %s`, meta.Format, formatBackendErasure, formatBackendErasureSingle)
}
// Erasure backend found, proceed to detect version.
format := &formatErasureVersionDetect{}
@@ -291,7 +297,7 @@ func formatErasureMigrateV2ToV3(data []byte, export, version string) ([]byte, er
func countErrs(errs []error, err error) int {
i := 0
for _, err1 := range errs {
if err1 == err {
if err1 == err || errors.Is(err1, err) {
i++
}
}
@@ -410,7 +416,7 @@ func checkFormatErasureValue(formatErasure *formatErasureV3, disk StorageAPI) er
if formatErasure.Version != formatMetaVersionV1 {
return fmt.Errorf("Unsupported version of backend format [%s] found on %s", formatErasure.Version, disk)
}
if formatErasure.Format != formatBackendErasure {
if formatErasure.Format != formatBackendErasure && formatErasure.Format != formatBackendErasureSingle {
return fmt.Errorf("Unsupported backend format [%s] found on %s", formatErasure.Format, disk)
}
if formatErasure.Erasure.Version != formatErasureVersionV3 {
@@ -643,7 +649,7 @@ func saveFormatErasureAll(ctx context.Context, storageDisks []StorageAPI, format
}
// relinquishes the underlying connection for all storage disks.
func closeStorageDisks(storageDisks []StorageAPI) {
func closeStorageDisks(storageDisks ...StorageAPI) {
var wg sync.WaitGroup
for _, disk := range storageDisks {
if disk == nil {
+7
View File
@@ -240,6 +240,9 @@ func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, er
if err != nil {
return nil, err
}
if formatBackend == formatBackendErasureSingle {
return nil, errFreshDisk
}
if formatBackend != formatBackendFS {
return nil, fmt.Errorf(`%s file: expected format-type: %s, found: %s`, formatConfigFile, formatBackendFS, formatBackend)
}
@@ -319,6 +322,10 @@ func formatFSFixDeploymentID(ctx context.Context, fsFormatPath string) error {
rlk.Close()
return err
}
if formatBackend == formatBackendErasureSingle {
rlk.Close()
return errFreshDisk
}
if formatBackend != formatBackendFS {
rlk.Close()
return fmt.Errorf(`%s file: expected format-type: %s, found: %s`, formatConfigFile, formatBackendFS, formatBackend)
+3
View File
@@ -45,6 +45,8 @@ func TestFSV1MetadataObjInfo(t *testing.T) {
// TestReadFSMetadata - readFSMetadata testing with a healthy and faulty disk
func TestReadFSMetadata(t *testing.T) {
t.Skip()
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
defer os.RemoveAll(disk)
@@ -80,6 +82,7 @@ func TestReadFSMetadata(t *testing.T) {
// TestWriteFSMetadata - tests of writeFSMetadata with healthy disk.
func TestWriteFSMetadata(t *testing.T) {
t.Skip()
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
defer os.RemoveAll(disk)
+2
View File
@@ -32,6 +32,7 @@ import (
// Tests cleanup multipart uploads for filesystem backend.
func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
t.Skip()
// Prepare for tests
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
defer os.RemoveAll(disk)
@@ -88,6 +89,7 @@ func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
// TestNewMultipartUploadFaultyDisk - test NewMultipartUpload with faulty disks
func TestNewMultipartUploadFaultyDisk(t *testing.T) {
t.Skip()
// Prepare for tests
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
defer os.RemoveAll(disk)
+5
View File
@@ -142,6 +142,11 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
return nil, config.ErrUnableToWriteInBackend(err).Hint(hint)
}
fsFormatPath := pathJoin(fsPath, minioMetaBucket, formatConfigFile)
if _, err = fsStat(ctx, fsFormatPath); err != nil && os.IsNotExist(err) {
return nil, errFreshDisk
}
// Assign a new UUID for FS minio mode. Each server instance
// gets its own UUID for temporary file transaction.
fsUUID := mustGetUUID()
+7
View File
@@ -51,6 +51,8 @@ func TestNewFS(t *testing.T) {
// TestFSShutdown - initialize a new FS object layer then calls
// Shutdown to check returned results
func TestFSShutdown(t *testing.T) {
t.Skip()
bucketName := "testbucket"
objectName := "object"
// Create and return an fsObject with its path in the disk
@@ -83,6 +85,8 @@ func TestFSShutdown(t *testing.T) {
// TestFSGetBucketInfo - test GetBucketInfo with healty and faulty disks
func TestFSGetBucketInfo(t *testing.T) {
t.Skip()
// Prepare for testing
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
defer os.RemoveAll(disk)
@@ -165,6 +169,7 @@ func TestFSPutObject(t *testing.T) {
// TestFSDeleteObject - test fs.DeleteObject() with healthy and corrupted disks
func TestFSDeleteObject(t *testing.T) {
t.Skip()
// Prepare for tests
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
defer os.RemoveAll(disk)
@@ -209,6 +214,7 @@ func TestFSDeleteObject(t *testing.T) {
// TestFSDeleteBucket - tests for fs DeleteBucket
func TestFSDeleteBucket(t *testing.T) {
t.Skip()
// Prepare for testing
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
defer os.RemoveAll(disk)
@@ -249,6 +255,7 @@ func TestFSDeleteBucket(t *testing.T) {
// TestFSListBuckets - tests for fs ListBuckets
func TestFSListBuckets(t *testing.T) {
t.Skip()
// Prepare for tests
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
defer os.RemoveAll(disk)
+5
View File
@@ -19,6 +19,7 @@ package cmd
import (
"context"
"errors"
"fmt"
"io/ioutil"
"log"
@@ -32,6 +33,7 @@ import (
"github.com/minio/cli"
"github.com/minio/madmin-go"
"github.com/minio/minio/internal/color"
"github.com/minio/minio/internal/config"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
"github.com/minio/pkg/certs"
@@ -292,6 +294,9 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
SecretKey: globalActiveCred.SecretKey,
})
if err != nil {
if errors.Is(err, errFreshDisk) {
err = config.ErrInvalidFSValue(err)
}
logger.FatalIf(err, "Unable to initialize gateway backend")
}
newObject = NewGatewayLayerWithLocker(newObject)
+2
View File
@@ -340,7 +340,9 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
// healObject heals given object path in deep to fix bitrot.
func healObject(bucket, object, versionID string, scan madmin.HealScanMode) {
// Get background heal sequence to send elements to heal
globalHealStateLK.Lock()
bgSeq, ok := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
globalHealStateLK.Unlock()
if ok {
bgSeq.queueHealTask(healSource{
bucket: bucket,
+8
View File
@@ -41,6 +41,7 @@ import (
"github.com/minio/minio/internal/config/dns"
xldap "github.com/minio/minio/internal/config/identity/ldap"
"github.com/minio/minio/internal/config/identity/openid"
idplugin "github.com/minio/minio/internal/config/identity/plugin"
xtls "github.com/minio/minio/internal/config/identity/tls"
polplugin "github.com/minio/minio/internal/config/policy/plugin"
"github.com/minio/minio/internal/config/storageclass"
@@ -73,6 +74,7 @@ const (
globalWindowsOSName = "windows"
globalMacOSName = "darwin"
globalMinioModeFS = "mode-server-fs"
globalMinioModeErasureSD = "mode-server-xl-single"
globalMinioModeErasure = "mode-server-xl"
globalMinioModeDistErasure = "mode-server-distributed-xl"
globalMinioModeGatewayPrefix = "mode-gateway-"
@@ -140,6 +142,9 @@ var (
// Indicates if the running minio server is an erasure-code backend.
globalIsErasure = false
// Indicates if the running minio server is in single drive XL mode.
globalIsErasureSD = false
// Indicates if the running minio is in gateway mode.
globalIsGateway = false
@@ -200,10 +205,13 @@ var (
globalAPIConfig = apiConfig{listQuorum: "strict"}
globalStorageClass storageclass.Config
globalLDAPConfig xldap.Config
globalOpenIDConfig openid.Config
globalSTSTLSConfig xtls.Config
globalAuthNPlugin *idplugin.AuthNPlugin
// CA root certificates, a nil value means system certs pool will be used
globalRootCAs *x509.CertPool
+1 -4
View File
@@ -116,10 +116,7 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int) {
// + 2 * 10MiB (default erasure block size v1) + 2 * 1MiB (default erasure block size v2)
blockSize := xioutil.BlockSizeLarge + xioutil.BlockSizeSmall
apiRequestsMaxPerNode = int(maxMem / uint64(maxSetDrives*blockSize+int(blockSizeV1*2+blockSizeV2*2)))
if globalIsErasure {
logger.Info("Automatically configured API requests per node based on available memory on the system: %d", apiRequestsMaxPerNode)
}
logger.Info("Automatically configured API requests per node based on available memory on the system: %d", apiRequestsMaxPerNode)
} else {
apiRequestsMaxPerNode = cfg.RequestsMax
if len(globalEndpoints.Hostnames()) > 0 {
+2 -1
View File
@@ -19,8 +19,9 @@ package cmd
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"github.com/minio/minio/internal/hash/sha256"
)
// getSHA256Hash returns SHA-256 hash in hex encoding of given data.
+47 -21
View File
@@ -314,18 +314,20 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
break
}
refreshInterval := sys.iamRefreshInterval
// Set up polling for expired accounts and credentials purging.
switch {
case sys.openIDConfig.ProviderEnabled():
go func() {
timer := time.NewTimer(sys.iamRefreshInterval)
timer := time.NewTimer(refreshInterval)
defer timer.Stop()
for {
select {
case <-timer.C:
sys.purgeExpiredCredentialsForExternalSSO(ctx)
timer.Reset(sys.iamRefreshInterval)
timer.Reset(refreshInterval)
case <-ctx.Done():
return
}
@@ -333,7 +335,7 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
}()
case sys.ldapConfig.Enabled:
go func() {
timer := time.NewTimer(sys.iamRefreshInterval)
timer := time.NewTimer(refreshInterval)
defer timer.Stop()
for {
@@ -342,7 +344,7 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
sys.purgeExpiredCredentialsForLDAP(ctx)
sys.updateGroupMembershipsForLDAP(ctx)
timer.Reset(sys.iamRefreshInterval)
timer.Reset(refreshInterval)
case <-ctx.Done():
return
}
@@ -353,20 +355,18 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
// Start watching changes to storage.
go sys.watch(ctx)
// Load RoleARN
if rolePolicyMap := sys.openIDConfig.GetRoleInfo(); rolePolicyMap != nil {
// Validate that policies associated with roles are defined.
for _, rolePolicies := range rolePolicyMap {
ps := newMappedPolicy(rolePolicies).toSlice()
numPolicies := len(ps)
validPolicies, _ := sys.store.FilterPolicies(rolePolicies, "")
numValidPolicies := len(strings.Split(validPolicies, ","))
if numPolicies != numValidPolicies {
logger.LogIf(ctx, fmt.Errorf("Some specified role policies (in %s) were not defined - role policies will not be enabled.", rolePolicies))
return
}
}
sys.rolesMap = rolePolicyMap
// Load RoleARNs
sys.rolesMap = make(map[arn.ARN]string)
// From OpenID
if riMap := globalOpenIDConfig.GetRoleInfo(); riMap != nil {
sys.validateAndAddRolePolicyMappings(ctx, riMap)
}
// From AuthN plugin if enabled.
if globalAuthNPlugin != nil {
riMap := globalAuthNPlugin.GetRoleInfo()
sys.validateAndAddRolePolicyMappings(ctx, riMap)
}
sys.printIAMRoles()
@@ -375,6 +375,32 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
logger.Info("Finished loading IAM sub-system (took %.1fs of %.1fs to load data).", now.Sub(iamLoadStart).Seconds(), now.Sub(iamInitStart).Seconds())
}
func (sys *IAMSys) validateAndAddRolePolicyMappings(ctx context.Context, m map[arn.ARN]string) {
// Validate that policies associated with roles are defined. If
// authZ plugin is set, role policies are just claims sent to
// the plugin and they need not exist.
//
// If some mapped policies do not exist, we print some error
// messages but continue any way - they can be fixed in the
// running server by creating the policies after start up.
for arn, rolePolicies := range m {
specifiedPoliciesSet := newMappedPolicy(rolePolicies).policySet()
validPolicies, _ := sys.store.FilterPolicies(rolePolicies, "")
knownPoliciesSet := newMappedPolicy(validPolicies).policySet()
unknownPoliciesSet := specifiedPoliciesSet.Difference(knownPoliciesSet)
if len(unknownPoliciesSet) > 0 {
if globalAuthZPlugin == nil {
// Print a warning that some policies mapped to a role are not defined.
errMsg := fmt.Errorf(
"The policies \"%s\" mapped to role ARN %s are not defined - this role may not work as expected.",
unknownPoliciesSet.ToSlice(), arn.String())
logger.LogIf(ctx, errMsg)
}
}
sys.rolesMap[arn] = rolePolicies
}
}
// Prints IAM role ARNs.
func (sys *IAMSys) printIAMRoles() {
if len(sys.rolesMap) == 0 {
@@ -693,7 +719,7 @@ func (sys *IAMSys) SetTempUser(ctx context.Context, accessKey string, cred auth.
return errServerNotInitialized
}
if globalAuthZPlugin != nil {
if newGlobalAuthZPluginFn() != nil {
// If OPA is set, we do not need to set a policy mapping.
policyName = ""
}
@@ -1666,8 +1692,8 @@ func (sys *IAMSys) GetCombinedPolicy(policies ...string) iampolicy.Policy {
// IsAllowed - checks given policy args is allowed to continue the Rest API.
func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool {
// If opa is configured, use OPA always.
if globalAuthZPlugin != nil {
ok, err := globalAuthZPlugin.IsAllowed(args)
if authz := newGlobalAuthZPluginFn(); authz != nil {
ok, err := authz.IsAllowed(args)
if err != nil {
logger.LogIf(GlobalContext, err)
}
+1 -16
View File
@@ -209,22 +209,7 @@ func (l *lockRESTServer) ForceUnlockHandler(w http.ResponseWriter, r *http.Reque
// lockMaintenance loops over all locks and discards locks
// that have not been refreshed for some time.
func lockMaintenance(ctx context.Context) {
// Wait until the object API is ready
// no need to start the lock maintenance
// if ObjectAPI is not initialized.
var objAPI ObjectLayer
for {
objAPI = newObjectLayerFn()
if objAPI == nil {
time.Sleep(time.Second)
continue
}
break
}
if _, ok := objAPI.(*erasureServerPools); !ok {
if !globalIsDistErasure {
return
}
+14 -6
View File
@@ -51,10 +51,12 @@ func newBucketMetacache(bucket string, cleanup bool) *bucketMetacache {
if cleanup {
// Recursively delete all caches.
objAPI := newObjectLayerFn()
ez, ok := objAPI.(*erasureServerPools)
if ok {
ctx := context.Background()
ez.renameAll(ctx, minioMetaBucket, metacachePrefixForID(bucket, slashSeparator))
if objAPI != nil {
ez, ok := objAPI.(renameAllStorager)
if ok {
ctx := context.Background()
ez.renameAll(ctx, minioMetaBucket, metacachePrefixForID(bucket, slashSeparator))
}
}
}
return &bucketMetacache{
@@ -207,9 +209,15 @@ func (b *bucketMetacache) cloneCaches() (map[string]metacache, map[string][]stri
// Deletes are performed concurrently.
func (b *bucketMetacache) deleteAll() {
ctx := context.Background()
ez, ok := newObjectLayerFn().(*erasureServerPools)
objAPI := newObjectLayerFn()
if objAPI == nil {
return
}
ez, ok := objAPI.(renameAllStorager)
if !ok {
logger.LogIf(ctx, errors.New("bucketMetacache: expected objAPI to be *erasurePools"))
logger.LogIf(ctx, errors.New("bucketMetacache: expected objAPI to be 'renameAllStorager'"))
return
}
+12 -3
View File
@@ -168,8 +168,10 @@ func (e *metaCacheEntry) isLatestDeletemarker() bool {
if !isXL2V1Format(e.metadata) {
return false
}
if meta, _ := isIndexedMetaV2(e.metadata); meta != nil {
if meta, _, err := isIndexedMetaV2(e.metadata); meta != nil {
return meta.IsLatestDeleteMarker()
} else if err != nil {
return true
}
// Fall back...
xlMeta, err := e.xlmeta()
@@ -438,6 +440,9 @@ func (m metaCacheEntriesSorted) shallowClone() metaCacheEntriesSorted {
func (m *metaCacheEntriesSorted) fileInfoVersions(bucket, prefix, delimiter, afterV string) (versions []ObjectInfo) {
versions = make([]ObjectInfo, 0, m.len())
prevPrefix := ""
vcfg, _ := globalBucketVersioningSys.Get(bucket)
versioned := vcfg != nil && vcfg.Versioned(prefix)
for _, entry := range m.o {
if entry.isObject() {
if delimiter != "" {
@@ -473,7 +478,7 @@ func (m *metaCacheEntriesSorted) fileInfoVersions(bucket, prefix, delimiter, aft
}
for _, version := range fiVersions {
versions = append(versions, version.ToObjectInfo(bucket, entry.name))
versions = append(versions, version.ToObjectInfo(bucket, entry.name, versioned))
}
continue
@@ -509,6 +514,10 @@ func (m *metaCacheEntriesSorted) fileInfoVersions(bucket, prefix, delimiter, aft
func (m *metaCacheEntriesSorted) fileInfos(bucket, prefix, delimiter string) (objects []ObjectInfo) {
objects = make([]ObjectInfo, 0, m.len())
prevPrefix := ""
vcfg, _ := globalBucketVersioningSys.Get(bucket)
versioned := vcfg != nil && vcfg.Versioned(prefix)
for _, entry := range m.o {
if entry.isObject() {
if delimiter != "" {
@@ -531,7 +540,7 @@ func (m *metaCacheEntriesSorted) fileInfos(bucket, prefix, delimiter string) (ob
fi, err := entry.fileInfo(bucket)
if err == nil {
objects = append(objects, fi.ToObjectInfo(bucket, entry.name))
objects = append(objects, fi.ToObjectInfo(bucket, entry.name, versioned))
}
continue
}
+1 -1
View File
@@ -56,7 +56,7 @@ func (m *metacacheManager) initManager() {
objAPI = newObjectLayerFn()
}
if !globalIsErasure {
if globalIsGateway {
return
}
+353 -1
View File
@@ -259,6 +259,286 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
return entries, nil
}
// listPath will return the requested entries.
// If no more entries are in the listing io.EOF is returned,
// otherwise nil or an unexpected error is returned.
// The listPathOptions given will be checked and modified internally.
// Required important fields are Bucket, Prefix, Separator.
// Other important fields are Limit, Marker.
// List ID always derived from the Marker.
func (es *erasureSingle) listPath(ctx context.Context, o *listPathOptions) (entries metaCacheEntriesSorted, err error) {
if err := checkListObjsArgs(ctx, o.Bucket, o.Prefix, o.Marker, es); err != nil {
return entries, err
}
// Marker is set validate pre-condition.
if o.Marker != "" && o.Prefix != "" {
// Marker not common with prefix is not implemented. Send an empty response
if !HasPrefix(o.Marker, o.Prefix) {
return entries, io.EOF
}
}
// With max keys of zero we have reached eof, return right here.
if o.Limit == 0 {
return entries, io.EOF
}
// For delimiter and prefix as '/' we do not list anything at all
// along // with the prefix. On a flat namespace with 'prefix'
// as '/' we don't have any entries, since all the keys are
// of form 'keyName/...'
if strings.HasPrefix(o.Prefix, SlashSeparator) {
return entries, io.EOF
}
// If delimiter is slashSeparator we must return directories of
// the non-recursive scan unless explicitly requested.
o.IncludeDirectories = o.Separator == slashSeparator
if (o.Separator == slashSeparator || o.Separator == "") && !o.Recursive {
o.Recursive = o.Separator != slashSeparator
o.Separator = slashSeparator
} else {
// Default is recursive, if delimiter is set then list non recursive.
o.Recursive = true
}
// Decode and get the optional list id from the marker.
o.parseMarker()
o.BaseDir = baseDirFromPrefix(o.Prefix)
o.Transient = o.Transient || isReservedOrInvalidBucket(o.Bucket, false)
o.SetFilter()
if o.Transient {
o.Create = false
}
// We have 2 cases:
// 1) Cold listing, just list.
// 2) Returning, but with no id. Start async listing.
// 3) Returning, with ID, stream from list.
//
// If we don't have a list id we must ask the server if it has a cache or create a new.
if o.ID != "" && !o.Transient {
// Create or ping with handout...
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
var c *metacache
if rpc == nil {
resp := localMetacacheMgr.getBucket(ctx, o.Bucket).findCache(*o)
c = &resp
} else {
c, err = rpc.GetMetacacheListing(ctx, *o)
}
if err != nil {
if errors.Is(err, context.Canceled) {
// Context is canceled, return at once.
// request canceled, no entries to return
return entries, io.EOF
}
if !errors.Is(err, context.DeadlineExceeded) {
o.debugln("listPath: got error", err)
}
o.Transient = true
o.Create = false
o.ID = mustGetUUID()
} else {
if c.fileNotFound {
// No cache found, no entries found.
return entries, io.EOF
}
if c.status == scanStateError || c.status == scanStateNone {
o.ID = ""
o.Create = false
o.debugln("scan status", c.status, " - waiting a roundtrip to create")
} else {
// Continue listing
o.ID = c.id
go func(meta metacache) {
// Continuously update while we wait.
t := time.NewTicker(metacacheMaxClientWait / 10)
defer t.Stop()
select {
case <-ctx.Done():
// Request is done, stop updating.
return
case <-t.C:
meta.lastHandout = time.Now()
if rpc == nil {
meta, _ = localMetacacheMgr.updateCacheEntry(meta)
}
meta, _ = rpc.UpdateMetacacheListing(ctx, meta)
}
}(*c)
}
}
// We have an existing list ID, continue streaming.
if o.Create {
o.debugln("Creating", o)
entries, err = es.listAndSave(ctx, o)
if err == nil || err == io.EOF {
return entries, err
}
entries.truncate(0)
} else {
o.debugln("Resuming", o)
entries, err = es.streamMetadataParts(ctx, *o)
entries.reuse = true // We read from stream and are not sharing results.
if err == nil {
return entries, nil
}
}
if IsErr(err, []error{
nil,
context.Canceled,
context.DeadlineExceeded,
// io.EOF is expected and should be returned but no need to log it.
io.EOF,
}...) {
// Expected good errors we don't need to return error.
return entries, err
}
entries.truncate(0)
go func() {
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
if rpc != nil {
ctx, cancel := context.WithTimeout(GlobalContext, 5*time.Second)
defer cancel()
c, err := rpc.GetMetacacheListing(ctx, *o)
if err == nil {
c.error = "no longer used"
c.status = scanStateError
rpc.UpdateMetacacheListing(ctx, *c)
}
}
}()
o.ID = ""
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Resuming listing from drives failed %w, proceeding to do raw listing", err))
}
}
// Do listing in-place.
// Create output for our results.
// Create filter for results.
o.debugln("Raw List", o)
filterCh := make(chan metaCacheEntry, o.Limit)
listCtx, cancelList := context.WithCancel(ctx)
filteredResults := o.gatherResults(listCtx, filterCh)
var wg sync.WaitGroup
wg.Add(1)
var listErr error
go func(o listPathOptions) {
defer wg.Done()
o.Limit = 0
listErr = es.listMerged(listCtx, o, filterCh)
o.debugln("listMerged returned with", listErr)
}(*o)
entries, err = filteredResults()
cancelList()
wg.Wait()
if listErr != nil && !errors.Is(listErr, context.Canceled) {
return entries, listErr
}
entries.reuse = true
truncated := entries.len() > o.Limit || err == nil
entries.truncate(o.Limit)
if !o.Transient && truncated {
if o.ID == "" {
entries.listID = mustGetUUID()
} else {
entries.listID = o.ID
}
}
if !truncated {
return entries, io.EOF
}
return entries, nil
}
// listMerged will list across all sets and return a merged results stream.
// The result channel is closed when no more results are expected.
func (es *erasureSingle) listMerged(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) error {
var mu sync.Mutex
var wg sync.WaitGroup
var listErr error
var inputs []chan metaCacheEntry
innerResults := make(chan metaCacheEntry, 100)
inputs = append(inputs, innerResults)
mu.Lock()
listCtx, cancelList := context.WithCancel(ctx)
defer cancelList()
wg.Add(1)
go func() {
defer wg.Done()
err := es.listPathInner(listCtx, o, innerResults)
mu.Lock()
defer mu.Unlock()
listErr = err
}()
mu.Unlock()
// Do lifecycle filtering.
if o.Lifecycle != nil {
filterIn := make(chan metaCacheEntry, 10)
go filterLifeCycle(ctx, o.Bucket, *o.Lifecycle, o.Retention, filterIn, results)
// Replace results.
results = filterIn
}
// Gather results to a single channel.
err := mergeEntryChannels(ctx, inputs, results, func(existing, other *metaCacheEntry) (replace bool) {
// Pick object over directory
if existing.isDir() && !other.isDir() {
return true
}
if !existing.isDir() && other.isDir() {
return false
}
eMeta, err := existing.xlmeta()
if err != nil {
return true
}
oMeta, err := other.xlmeta()
if err != nil {
return false
}
// Replace if modtime is newer
if !oMeta.latestModtime().Equal(oMeta.latestModtime()) {
return oMeta.latestModtime().After(eMeta.latestModtime())
}
// Use NumVersions as a final tiebreaker.
return len(oMeta.versions) > len(eMeta.versions)
})
cancelList()
wg.Wait()
if err != nil {
return err
}
if listErr != nil {
if contextCanceled(ctx) {
return nil
}
if listErr.Error() == io.EOF.Error() {
return nil
}
logger.LogIf(ctx, listErr)
return listErr
}
if contextCanceled(ctx) {
return ctx.Err()
}
return nil
}
// listMerged will list across all sets and return a merged results stream.
// The result channel is closed when no more results are expected.
func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) error {
@@ -362,6 +642,8 @@ func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions,
// function closes 'out' and exits.
func filterLifeCycle(ctx context.Context, bucket string, lc lifecycle.Lifecycle, lr lock.Retention, in <-chan metaCacheEntry, out chan<- metaCacheEntry) {
defer close(out)
vcfg, _ := globalBucketVersioningSys.Get(bucket)
for {
var obj metaCacheEntry
var ok bool
@@ -378,7 +660,10 @@ func filterLifeCycle(ctx context.Context, bucket string, lc lifecycle.Lifecycle,
if err != nil {
continue
}
objInfo := fi.ToObjectInfo(bucket, obj.name)
versioned := vcfg != nil && vcfg.Versioned(obj.name)
objInfo := fi.ToObjectInfo(bucket, obj.name, versioned)
action := evalActionFromLifecycle(ctx, lc, lr, objInfo, false)
switch action {
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
@@ -395,6 +680,73 @@ func filterLifeCycle(ctx context.Context, bucket string, lc lifecycle.Lifecycle,
}
}
func (es *erasureSingle) listAndSave(ctx context.Context, o *listPathOptions) (entries metaCacheEntriesSorted, err error) {
// Use ID as the object name...
o.pool = 0
o.set = 0
saver := es
// Disconnect from call above, but cancel on exit.
listCtx, cancel := context.WithCancel(GlobalContext)
saveCh := make(chan metaCacheEntry, metacacheBlockSize)
inCh := make(chan metaCacheEntry, metacacheBlockSize)
outCh := make(chan metaCacheEntry, o.Limit)
filteredResults := o.gatherResults(ctx, outCh)
mc := o.newMetacache()
meta := metaCacheRPC{meta: &mc, cancel: cancel, rpc: globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix)), o: *o}
// Save listing...
go func() {
if err := saver.saveMetaCacheStream(listCtx, &meta, saveCh); err != nil {
meta.setErr(err.Error())
}
cancel()
}()
// Do listing...
go func(o listPathOptions) {
err := es.listMerged(listCtx, o, inCh)
if err != nil {
meta.setErr(err.Error())
}
o.debugln("listAndSave: listing", o.ID, "finished with ", err)
}(*o)
// Keep track of when we return since we no longer have to send entries to output.
var funcReturned bool
var funcReturnedMu sync.Mutex
defer func() {
funcReturnedMu.Lock()
funcReturned = true
funcReturnedMu.Unlock()
}()
// Write listing to results and saver.
go func() {
var returned bool
for entry := range inCh {
if !returned {
funcReturnedMu.Lock()
returned = funcReturned
funcReturnedMu.Unlock()
outCh <- entry
if returned {
close(outCh)
}
}
entry.reusable = returned
saveCh <- entry
}
if !returned {
close(outCh)
}
close(saveCh)
}()
return filteredResults()
}
func (z *erasureServerPools) listAndSave(ctx context.Context, o *listPathOptions) (entries metaCacheEntriesSorted, err error) {
// Use ID as the object name...
o.pool = z.getAvailablePoolIdx(ctx, minioMetaBucket, o.ID, 10<<20)
+338
View File
@@ -543,6 +543,170 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
}
}
func (es *erasureSingle) streamMetadataParts(ctx context.Context, o listPathOptions) (entries metaCacheEntriesSorted, err error) {
retries := 0
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
for {
if contextCanceled(ctx) {
return entries, ctx.Err()
}
// If many failures, check the cache state.
if retries > 10 {
err := o.checkMetacacheState(ctx, rpc)
if err != nil {
return entries, fmt.Errorf("remote listing canceled: %w", err)
}
retries = 1
}
const retryDelay = 250 * time.Millisecond
// All operations are performed without locks, so we must be careful and allow for failures.
// Read metadata associated with the object from a disk.
if retries > 0 {
_, err := es.disk.ReadVersion(ctx, minioMetaBucket,
o.objectPath(0), "", false)
if err != nil {
time.Sleep(retryDelay)
retries++
continue
}
}
// Load first part metadata...
// Read metadata associated with the object from all disks.
fi, metaArr, onlineDisks, err := es.getObjectFileInfo(ctx, minioMetaBucket, o.objectPath(0), ObjectOptions{}, true)
if err != nil {
switch toObjectErr(err, minioMetaBucket, o.objectPath(0)).(type) {
case ObjectNotFound:
retries++
time.Sleep(retryDelay)
continue
case InsufficientReadQuorum:
retries++
time.Sleep(retryDelay)
continue
default:
return entries, fmt.Errorf("reading first part metadata: %w", err)
}
}
partN, err := o.findFirstPart(fi)
switch {
case err == nil:
case errors.Is(err, io.ErrUnexpectedEOF):
if retries == 10 {
err := o.checkMetacacheState(ctx, rpc)
if err != nil {
return entries, fmt.Errorf("remote listing canceled: %w", err)
}
retries = -1
}
retries++
time.Sleep(retryDelay)
continue
case errors.Is(err, io.EOF):
return entries, io.EOF
}
// We got a stream to start at.
loadedPart := 0
for {
if contextCanceled(ctx) {
return entries, ctx.Err()
}
if partN != loadedPart {
if retries > 10 {
err := o.checkMetacacheState(ctx, rpc)
if err != nil {
return entries, fmt.Errorf("waiting for next part %d: %w", partN, err)
}
retries = 1
}
if retries > 0 {
// Load from one disk only
_, err := es.disk.ReadVersion(ctx, minioMetaBucket,
o.objectPath(partN), "", false)
if err != nil {
time.Sleep(retryDelay)
retries++
continue
}
}
// Load partN metadata...
fi, metaArr, onlineDisks, err = es.getObjectFileInfo(ctx, minioMetaBucket, o.objectPath(partN), ObjectOptions{}, true)
if err != nil {
time.Sleep(retryDelay)
retries++
continue
}
loadedPart = partN
bi, err := getMetacacheBlockInfo(fi, partN)
logger.LogIf(ctx, err)
if err == nil {
if bi.pastPrefix(o.Prefix) {
return entries, io.EOF
}
}
}
pr, pw := io.Pipe()
go func() {
werr := es.getObjectWithFileInfo(ctx, minioMetaBucket, o.objectPath(partN), 0,
fi.Size, pw, fi, metaArr, onlineDisks)
pw.CloseWithError(werr)
}()
tmp := newMetacacheReader(pr)
e, err := tmp.filter(o)
pr.CloseWithError(err)
entries.o = append(entries.o, e.o...)
if o.Limit > 0 && entries.len() > o.Limit {
entries.truncate(o.Limit)
return entries, nil
}
if err == nil {
// We stopped within the listing, we are done for now...
return entries, nil
}
if err != nil && err.Error() != io.EOF.Error() {
switch toObjectErr(err, minioMetaBucket, o.objectPath(partN)).(type) {
case ObjectNotFound:
retries++
time.Sleep(retryDelay)
continue
case InsufficientReadQuorum:
retries++
time.Sleep(retryDelay)
continue
default:
logger.LogIf(ctx, err)
return entries, err
}
}
// We finished at the end of the block.
// And should not expect any more results.
bi, err := getMetacacheBlockInfo(fi, partN)
logger.LogIf(ctx, err)
if err != nil || bi.EOS {
// We are done and there are no more parts.
return entries, io.EOF
}
if bi.endedPrefix(o.Prefix) {
// Nothing more for prefix.
return entries, io.EOF
}
partN++
retries = 0
}
}
}
// getListQuorum interprets list quorum values and returns appropriate
// acceptable quorum expected for list operations
func getListQuorum(quorum string, driveCount int) int {
@@ -562,6 +726,53 @@ func getListQuorum(quorum string, driveCount int) int {
return 3
}
// Will return io.EOF if continuing would not yield more results.
func (es *erasureSingle) listPathInner(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) (err error) {
defer close(results)
o.debugf(color.Green("listPath:")+" with options: %#v", o)
// How to resolve results.
resolver := metadataResolutionParams{
dirQuorum: 1,
objQuorum: 1,
bucket: o.Bucket,
}
// Maximum versions requested for "latest" object
// resolution on versioned buckets, this is to be only
// used when o.Versioned is false
if !o.Versioned {
resolver.requestedVersions = 1
}
ctxDone := ctx.Done()
return listPathRaw(ctx, listPathRawOptions{
disks: []StorageAPI{es.disk},
bucket: o.Bucket,
path: o.BaseDir,
recursive: o.Recursive,
filterPrefix: o.FilterPrefix,
minDisks: 1,
forwardTo: o.Marker,
agreed: func(entry metaCacheEntry) {
select {
case <-ctxDone:
case results <- entry:
}
},
partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
// Results Disagree :-(
entry, ok := entries.resolve(&resolver)
if ok {
select {
case <-ctxDone:
case results <- *entry:
}
}
},
})
}
// Will return io.EOF if continuing would not yield more results.
func (er *erasureObjects) listPath(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) (err error) {
defer close(results)
@@ -654,6 +865,133 @@ func (m *metaCacheRPC) setErr(err string) {
*m.meta = meta
}
func (es *erasureSingle) saveMetaCacheStream(ctx context.Context, mc *metaCacheRPC, entries <-chan metaCacheEntry) (err error) {
o := mc.o
o.debugf(color.Green("saveMetaCacheStream:")+" with options: %#v", o)
metaMu := &mc.mu
rpc := mc.rpc
cancel := mc.cancel
defer func() {
o.debugln(color.Green("saveMetaCacheStream:")+"err:", err)
if err != nil && !errors.Is(err, io.EOF) {
go mc.setErr(err.Error())
cancel()
}
}()
defer cancel()
// Save continuous updates
go func() {
var err error
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
var exit bool
for !exit {
select {
case <-ticker.C:
case <-ctx.Done():
exit = true
}
metaMu.Lock()
meta := *mc.meta
meta, err = o.updateMetacacheListing(meta, rpc)
if err == nil && time.Since(meta.lastHandout) > metacacheMaxClientWait {
cancel()
exit = true
meta.status = scanStateError
meta.error = fmt.Sprintf("listing canceled since time since last handout was %v ago", time.Since(meta.lastHandout).Round(time.Second))
o.debugln(color.Green("saveMetaCacheStream: ") + meta.error)
meta, err = o.updateMetacacheListing(meta, rpc)
}
if err == nil {
*mc.meta = meta
if meta.status == scanStateError {
cancel()
exit = true
}
}
metaMu.Unlock()
}
}()
const retryDelay = 200 * time.Millisecond
const maxTries = 5
// Keep destination...
// Write results to disk.
bw := newMetacacheBlockWriter(entries, func(b *metacacheBlock) error {
// if the block is 0 bytes and its a first block skip it.
// skip only this for Transient caches.
if len(b.data) == 0 && b.n == 0 && o.Transient {
return nil
}
o.debugln(color.Green("saveMetaCacheStream:")+" saving block", b.n, "to", o.objectPath(b.n))
r, err := hash.NewReader(bytes.NewReader(b.data), int64(len(b.data)), "", "", int64(len(b.data)))
logger.LogIf(ctx, err)
custom := b.headerKV()
_, err = es.putMetacacheObject(ctx, o.objectPath(b.n), NewPutObjReader(r), ObjectOptions{
UserDefined: custom,
})
if err != nil {
mc.setErr(err.Error())
cancel()
return err
}
if b.n == 0 {
return nil
}
// Update block 0 metadata.
var retries int
for {
meta := b.headerKV()
fi := FileInfo{
Metadata: make(map[string]string, len(meta)),
}
for k, v := range meta {
fi.Metadata[k] = v
}
err := es.updateObjectMeta(ctx, minioMetaBucket, o.objectPath(0), fi, es.disk)
if err == nil {
break
}
switch err.(type) {
case ObjectNotFound:
return err
case StorageErr:
return err
case InsufficientReadQuorum:
default:
logger.LogIf(ctx, err)
}
if retries >= maxTries {
return err
}
retries++
time.Sleep(retryDelay)
}
return nil
})
// Blocks while consuming entries or an error occurs.
err = bw.Close()
if err != nil {
mc.setErr(err.Error())
}
metaMu.Lock()
defer metaMu.Unlock()
if mc.meta.error != "" {
return err
}
// Save success
mc.meta.status = scanStateSuccess
meta, err := o.updateMetacacheListing(*mc.meta, rpc)
if err == nil {
*mc.meta = meta
}
return nil
}
func (er *erasureObjects) saveMetaCacheStream(ctx context.Context, mc *metaCacheRPC, entries <-chan metaCacheEntry) (err error) {
o := mc.o
o.debugf(color.Green("saveMetaCacheStream:")+" with options: %#v", o)
+2 -2
View File
@@ -155,9 +155,9 @@ func (m *metacache) delete(ctx context.Context) {
logger.LogIf(ctx, errors.New("metacache.delete: no object layer"))
return
}
ez, ok := objAPI.(*erasureServerPools)
ez, ok := objAPI.(renameAllStorager)
if !ok {
logger.LogIf(ctx, errors.New("metacache.delete: expected objAPI to be *erasureServerPools"))
logger.LogIf(ctx, errors.New("metacache.delete: expected objAPI to be 'renameAllStorager'"))
return
}
ez.renameAll(ctx, minioMetaBucket, metacachePrefixForID(m.bucket, m.id))
+2 -2
View File
@@ -1365,7 +1365,7 @@ func getMinioHealingMetrics() *MetricsGroup {
mg := &MetricsGroup{}
mg.RegisterRead(func(_ context.Context) (metrics []Metric) {
metrics = make([]Metric, 0, 5)
if !globalIsErasure {
if globalIsGateway {
return
}
bgSeq, exists := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
@@ -1817,7 +1817,7 @@ func getClusterStorageMetrics() *MetricsGroup {
mg.RegisterRead(func(ctx context.Context) (metrics []Metric) {
objLayer := newObjectLayerFn()
// Service not initialized yet
if objLayer == nil || !globalIsErasure {
if objLayer == nil || globalIsGateway {
return
}
+1 -1
View File
@@ -132,7 +132,7 @@ func nodeHealthMetricsPrometheus(ch chan<- prometheus.Metric) {
// collects healing specific metrics for MinIO instance in Prometheus specific format
// and sends to given channel
func healingMetricsPrometheus(ch chan<- prometheus.Metric) {
if !globalIsErasure {
if globalIsGateway {
return
}
bgSeq, exists := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
+2 -18
View File
@@ -35,9 +35,6 @@ func TestListObjectsVersionedFolders(t *testing.T) {
}
func testListObjectsVersionedFolders(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
if instanceType == FSTestStr {
return
}
t, _ := t1.(*testing.T)
testBuckets := []string{
// This bucket is used for testing ListObject operations.
@@ -317,9 +314,6 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
}
func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, versioned bool) {
if instanceType == FSTestStr && versioned {
return
}
t, _ := t1.(*testing.T)
testBuckets := []string{
// This bucket is used for testing ListObject operations.
@@ -1020,10 +1014,6 @@ func TestDeleteObjectVersionMarker(t *testing.T) {
}
func testDeleteObjectVersion(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
if instanceType == FSTestStr {
return
}
t, _ := t1.(*testing.T)
testBuckets := []string{
@@ -1101,10 +1091,6 @@ func TestListObjectVersions(t *testing.T) {
// Unit test for ListObjectVersions
func testListObjectVersions(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
if instanceType == FSTestStr {
return
}
t, _ := t1.(*testing.T)
testBuckets := []string{
// This bucket is used for testing ListObject operations.
@@ -1886,16 +1872,14 @@ func testListObjectsContinuation(obj ObjectLayer, instanceType string, t1 TestEr
// Initialize FS backend for the benchmark.
func initFSObjectsB(disk string, t *testing.B) (obj ObjectLayer) {
var err error
obj, err = NewFSObjectLayer(disk)
obj, _, err := initObjectLayer(context.Background(), mustGetPoolEndpoints(disk))
if err != nil {
t.Fatal("Unexpected err: ", err)
t.Fatal(err)
}
newTestConfig(globalMinioDefaultRegion, obj)
initAllSubsystems()
return obj
}
+26 -30
View File
@@ -1620,39 +1620,35 @@ func testListObjectParts(obj ObjectLayer, instanceType string, t TestErrHandler)
t.Errorf("Test %d: %s: Expected Bucket to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.Bucket, actualResult.Bucket)
}
// ListObjectParts returns empty response always in FS mode
if instanceType != FSTestStr {
// Asserting IsTruncated.
if actualResult.IsTruncated != testCase.expectedResult.IsTruncated {
t.Errorf("Test %d: %s: Expected IsTruncated to be \"%v\", but found it to \"%v\"", i+1, instanceType, expectedResult.IsTruncated, actualResult.IsTruncated)
continue
// Asserting IsTruncated.
if actualResult.IsTruncated != testCase.expectedResult.IsTruncated {
t.Errorf("Test %d: %s: Expected IsTruncated to be \"%v\", but found it to \"%v\"", i+1, instanceType, expectedResult.IsTruncated, actualResult.IsTruncated)
continue
}
// Asserting NextPartNumberMarker.
if actualResult.NextPartNumberMarker != expectedResult.NextPartNumberMarker {
t.Errorf("Test %d: %s: Expected NextPartNumberMarker to be \"%d\", but instead found it to be \"%d\"", i+1, instanceType, expectedResult.NextPartNumberMarker, actualResult.NextPartNumberMarker)
continue
}
// Asserting the number of Parts.
if len(expectedResult.Parts) != len(actualResult.Parts) {
t.Errorf("Test %d: %s: Expected the result to contain info of %d Parts, but found %d instead", i+1, instanceType, len(expectedResult.Parts), len(actualResult.Parts))
continue
}
// Iterating over the partInfos and asserting the fields.
for j, actualMetaData := range actualResult.Parts {
// Asserting the PartNumber in the PartInfo.
if actualMetaData.PartNumber != expectedResult.Parts[j].PartNumber {
t.Errorf("Test %d: %s: Part %d: Expected PartNumber to be \"%d\", but instead found \"%d\"", i+1, instanceType, j+1, expectedResult.Parts[j].PartNumber, actualMetaData.PartNumber)
}
// Asserting NextPartNumberMarker.
if actualResult.NextPartNumberMarker != expectedResult.NextPartNumberMarker {
t.Errorf("Test %d: %s: Expected NextPartNumberMarker to be \"%d\", but instead found it to be \"%d\"", i+1, instanceType, expectedResult.NextPartNumberMarker, actualResult.NextPartNumberMarker)
continue
// Asserting the Size in the PartInfo.
if actualMetaData.Size != expectedResult.Parts[j].Size {
t.Errorf("Test %d: %s: Part %d: Expected Part Size to be \"%d\", but instead found \"%d\"", i+1, instanceType, j+1, expectedResult.Parts[j].Size, actualMetaData.Size)
}
// Asserting the number of Parts.
if len(expectedResult.Parts) != len(actualResult.Parts) {
t.Errorf("Test %d: %s: Expected the result to contain info of %d Parts, but found %d instead", i+1, instanceType, len(expectedResult.Parts), len(actualResult.Parts))
continue
// Asserting the ETag in the PartInfo.
if actualMetaData.ETag != expectedResult.Parts[j].ETag {
t.Errorf("Test %d: %s: Part %d: Expected Etag to be \"%s\", but instead found \"%s\"", i+1, instanceType, j+1, expectedResult.Parts[j].ETag, actualMetaData.ETag)
}
// Iterating over the partInfos and asserting the fields.
for j, actualMetaData := range actualResult.Parts {
// Asserting the PartNumber in the PartInfo.
if actualMetaData.PartNumber != expectedResult.Parts[j].PartNumber {
t.Errorf("Test %d: %s: Part %d: Expected PartNumber to be \"%d\", but instead found \"%d\"", i+1, instanceType, j+1, expectedResult.Parts[j].PartNumber, actualMetaData.PartNumber)
}
// Asserting the Size in the PartInfo.
if actualMetaData.Size != expectedResult.Parts[j].Size {
t.Errorf("Test %d: %s: Part %d: Expected Part Size to be \"%d\", but instead found \"%d\"", i+1, instanceType, j+1, expectedResult.Parts[j].Size, actualMetaData.Size)
}
// Asserting the ETag in the PartInfo.
if actualMetaData.ETag != expectedResult.Parts[j].ETag {
t.Errorf("Test %d: %s: Part %d: Expected Etag to be \"%s\", but instead found \"%s\"", i+1, instanceType, j+1, expectedResult.Parts[j].ETag, actualMetaData.ETag)
}
}
}
}
}
+1 -1
View File
@@ -950,7 +950,7 @@ func compressSelfTest() {
// getDiskInfos returns the disk information for the provided disks.
// If a disk is nil or an error is returned the result will be nil as well.
func getDiskInfos(ctx context.Context, disks []StorageAPI) []*DiskInfo {
func getDiskInfos(ctx context.Context, disks ...StorageAPI) []*DiskInfo {
res := make([]*DiskInfo, len(disks))
for i, disk := range disks {
if disk == nil {
+9 -11
View File
@@ -3453,17 +3453,15 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
writeErrorResponse(ctx, w, toAPIError(ctx, errors.New("force-delete is forbidden in a locked-enabled bucket")), r.URL)
return
}
if vID != "" {
apiErr = enforceRetentionBypassForDelete(ctx, r, bucket, ObjectToDelete{
ObjectV: ObjectV{
ObjectName: object,
VersionID: vID,
},
}, goi, gerr)
if apiErr != ErrNone && apiErr != ErrNoSuchKey {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErr), r.URL)
return
}
apiErr = enforceRetentionBypassForDelete(ctx, r, bucket, ObjectToDelete{
ObjectV: ObjectV{
ObjectName: object,
VersionID: vID,
},
}, goi, gerr)
if apiErr != ErrNone && apiErr != ErrNoSuchKey {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErr), r.URL)
return
}
}
+1 -1
View File
@@ -1954,7 +1954,7 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
if err != nil {
t.Fatalf("Test %d: %s: Failed to look for copied object part: <ERROR> %s", i+1, instanceType, err)
}
if instanceType != FSTestStr && len(results.Parts) != 1 {
if len(results.Parts) != 1 {
t.Fatalf("Test %d: %s: Expected only one entry returned %d entries", i+1, instanceType, len(results.Parts))
}
}
+4 -4
View File
@@ -150,13 +150,13 @@ func connectLoadInitFormats(verboseLogging bool, firstDisk bool, endpoints Endpo
defer func(storageDisks []StorageAPI) {
if err != nil {
closeStorageDisks(storageDisks)
closeStorageDisks(storageDisks...)
}
}(storageDisks)
for i, err := range errs {
if err != nil {
if err == errDiskNotFound && verboseLogging {
if err != nil && !errors.Is(err, errXLBackend) {
if errors.Is(err, errDiskNotFound) && verboseLogging {
logger.Error("Unable to connect to %s: %v", endpoints[i], isServerResolvable(endpoints[i], time.Second))
} else {
logger.Error("Unable to use the drive %s: %v", endpoints[i], err)
@@ -173,7 +173,7 @@ func connectLoadInitFormats(verboseLogging bool, firstDisk bool, endpoints Endpo
// Check if we have
for i, sErr := range sErrs {
// print the error, nonetheless, which is perhaps unhandled
if sErr != errUnformattedDisk && sErr != errDiskNotFound && verboseLogging {
if !errors.Is(sErr, errUnformattedDisk) && !errors.Is(sErr, errDiskNotFound) && verboseLogging {
if sErr != nil {
logger.Error("Unable to read 'format.json' from %s: %v\n", endpoints[i], sErr)
}
+42 -28
View File
@@ -73,6 +73,20 @@ var ServerFlags = []cli.Flag{
EnvVar: "MINIO_SHUTDOWN_TIMEOUT",
Hidden: true,
},
cli.DurationFlag{
Name: "idle-timeout",
Value: xhttp.DefaultIdleTimeout,
Usage: "idle timeout is the maximum amount of time to wait for the next request when keep-alives are enabled",
EnvVar: "MINIO_IDLE_TIMEOUT",
Hidden: true,
},
cli.DurationFlag{
Name: "read-header-timeout",
Value: xhttp.DefaultReadHeaderTimeout,
Usage: "read header timeout is the amount of time allowed to read request headers",
EnvVar: "MINIO_READ_HEADER_TIMEOUT",
Hidden: true,
},
}
var serverCmd = cli.Command{
@@ -208,6 +222,7 @@ func serverHandleCmdArgs(ctx *cli.Context) {
if globalIsDistErasure {
globalIsErasure = true
}
globalIsErasureSD = (setupType == ErasureSDSetupType)
}
func serverHandleEnvVars() {
@@ -218,13 +233,11 @@ func serverHandleEnvVars() {
var globalHealStateLK sync.RWMutex
func initAllSubsystems() {
if globalIsErasure {
globalHealStateLK.Lock()
// New global heal state
globalAllHealState = newHealState(true)
globalBackgroundHealState = newHealState(false)
globalHealStateLK.Unlock()
}
globalHealStateLK.Lock()
// New global heal state
globalAllHealState = newHealState(true)
globalBackgroundHealState = newHealState(false)
globalHealStateLK.Unlock()
// Create new notification system and initialize notification peer targets
globalNotificationSys = NewNotificationSys(globalEndpoints)
@@ -264,8 +277,6 @@ func initAllSubsystems() {
// Create new bucket versioning subsystem
if globalBucketVersioningSys == nil {
globalBucketVersioningSys = NewBucketVersioningSys()
} else {
globalBucketVersioningSys.Reset()
}
// Create new bucket replication subsytem
@@ -486,6 +497,8 @@ func serverMain(ctx *cli.Context) {
UseHandler(setCriticalErrorHandler(corsHandler(handler))).
UseTLSConfig(newTLSConfig(getCert)).
UseShutdownTimeout(ctx.Duration("shutdown-timeout")).
UseIdleTimeout(ctx.Duration("idle-timeout")).
UseReadHeaderTimeout(ctx.Duration("read-header-timeout")).
UseBaseContext(GlobalContext).
UseCustomLogger(log.New(ioutil.Discard, "", 0)) // Turn-off random logging by Go stdlib
@@ -511,11 +524,8 @@ func serverMain(ctx *cli.Context) {
xhttp.SetMinIOVersion(Version)
// Enable background operations for erasure coding
if globalIsErasure {
initAutoHeal(GlobalContext, newObject)
initHealMRF(GlobalContext, newObject)
}
initAutoHeal(GlobalContext, newObject)
initHealMRF(GlobalContext, newObject)
initBackgroundExpiry(GlobalContext, newObject)
if globalActiveCred.Equal(auth.DefaultCredentials) {
@@ -563,21 +573,19 @@ func serverMain(ctx *cli.Context) {
// Background all other operations such as initializing bucket metadata etc.
go func() {
// Initialize transition tier configuration manager
if globalIsErasure {
initBackgroundReplication(GlobalContext, newObject)
initBackgroundTransition(GlobalContext, newObject)
initBackgroundReplication(GlobalContext, newObject)
initBackgroundTransition(GlobalContext, newObject)
go func() {
if err := globalTierConfigMgr.Init(GlobalContext, newObject); err != nil {
logger.LogIf(GlobalContext, err)
}
go func() {
if err := globalTierConfigMgr.Init(GlobalContext, newObject); err != nil {
logger.LogIf(GlobalContext, err)
}
globalTierJournal, err = initTierDeletionJournal(GlobalContext)
if err != nil {
logger.FatalIf(err, "Unable to initialize remote tier pending deletes journal")
}
}()
}
globalTierJournal, err = initTierDeletionJournal(GlobalContext)
if err != nil {
logger.FatalIf(err, "Unable to initialize remote tier pending deletes journal")
}
}()
// Initialize site replication manager.
globalSiteReplicationSys.Init(GlobalContext, newObject)
@@ -648,7 +656,13 @@ func newObjectLayer(ctx context.Context, endpointServerPools EndpointServerPools
// For FS only, directly use the disk.
if endpointServerPools.NEndpoints() == 1 {
// Initialize new FS object layer.
return NewFSObjectLayer(endpointServerPools[0].Endpoints[0].Path)
newObject, err = NewFSObjectLayer(endpointServerPools[0].Endpoints[0].Path)
if err == nil {
return newObject, nil
}
if err != nil && err != errFreshDisk {
return newObject, err
}
}
return newErasureServerPools(ctx, endpointServerPools)
+2 -2
View File
@@ -27,7 +27,7 @@ import (
func TestNewObjectLayer(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Tests for FS object layer.
// Tests for ErasureSD object layer.
nDisks := 1
disks, err := getRandomDisks(nDisks)
if err != nil {
@@ -39,7 +39,7 @@ func TestNewObjectLayer(t *testing.T) {
if err != nil {
t.Fatal("Unexpected object layer initialization error", err)
}
_, ok := obj.(*FSObjects)
_, ok := obj.(*erasureSingle)
if !ok {
t.Fatal("Unexpected object layer detected", reflect.TypeOf(obj))
}
+7 -7
View File
@@ -39,7 +39,7 @@ import (
"github.com/minio/pkg/bucket/policy"
)
// API suite container common to both FS and Erasure.
// API suite container common to both ErasureSD and Erasure.
type TestSuiteCommon struct {
serverType string
testServer TestServer
@@ -122,12 +122,12 @@ func runAllTests(suite *TestSuiteCommon, c *check) {
func TestServerSuite(t *testing.T) {
testCases := []*TestSuiteCommon{
// Init and run test on FS backend with signature v4.
{serverType: "FS", signer: signerV4},
// Init and run test on FS backend with signature v2.
{serverType: "FS", signer: signerV2},
// Init and run test on FS backend, with tls enabled.
{serverType: "FS", signer: signerV4, secure: true},
// Init and run test on ErasureSD backend with signature v4.
{serverType: "ErasureSD", signer: signerV4},
// Init and run test on ErasureSD backend with signature v2.
{serverType: "ErasureSD", signer: signerV2},
// Init and run test on ErasureSD backend, with tls enabled.
{serverType: "ErasureSD", signer: signerV4, secure: true},
// Init and run test on Erasure backend.
{serverType: "Erasure", signer: signerV4},
// Init and run test on ErasureSet backend.
+5
View File
@@ -27,6 +27,9 @@ const (
// FSSetupType - FS setup type enum.
FSSetupType
// ErasureSDSetupType - Erasure single drive setup enum.
ErasureSDSetupType
// ErasureSetupType - Erasure setup type enum.
ErasureSetupType
@@ -41,6 +44,8 @@ func (setupType SetupType) String() string {
switch setupType {
case FSSetupType:
return globalMinioModeFS
case ErasureSDSetupType:
return globalMinioModeErasureSD
case ErasureSetupType:
return globalMinioModeErasure
case DistErasureSetupType:
+1 -1
View File
@@ -20,7 +20,6 @@ package cmd
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"io/ioutil"
@@ -29,6 +28,7 @@ import (
"strings"
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/hash/sha256"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
)
+1 -1
View File
@@ -27,7 +27,6 @@ package cmd
import (
"bytes"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"net/http"
@@ -40,6 +39,7 @@ import (
"github.com/minio/minio-go/v7/pkg/s3utils"
"github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/hash/sha256"
xhttp "github.com/minio/minio/internal/http"
)
+66 -15
View File
@@ -876,14 +876,28 @@ func (c *SiteReplicationSys) PeerBucketConfigureReplHandler(ctx context.Context,
ExistingObjectReplicate: "enable",
}
)
ruleARN := targetARN
for _, r := range replicationConfig.Rules {
if r.ID == ruleID {
hasRule = true
ruleARN = r.Destination.Bucket
}
}
switch {
case hasRule:
err = replicationConfig.EditRule(opts)
if ruleARN != opts.DestBucket {
// remove stale replication rule and replace rule with correct target ARN
if len(replicationConfig.Rules) > 1 {
err = replicationConfig.RemoveRule(opts)
} else {
replicationConfig = replication.Config{}
}
if err == nil {
err = replicationConfig.AddRule(opts)
}
} else {
err = replicationConfig.EditRule(opts)
}
default:
err = replicationConfig.AddRule(opts)
}
@@ -902,7 +916,7 @@ func (c *SiteReplicationSys) PeerBucketConfigureReplHandler(ctx context.Context,
if err != nil {
return err
}
sameTarget, apiErr := validateReplicationDestination(ctx, bucket, newReplicationConfig)
sameTarget, apiErr := validateReplicationDestination(ctx, bucket, newReplicationConfig, true)
if apiErr != noError {
return fmt.Errorf("bucket replication config validation error: %#v", apiErr)
}
@@ -2970,8 +2984,9 @@ func (c *SiteReplicationSys) SiteReplicationMetaInfo(ctx context.Context, objAPI
CreatedAt: createdAt,
Location: globalSite.Region,
}
// Get bucket policy if present.
policy, err := globalPolicySys.Get(bucket)
policy, updatedAt, err := globalBucketMetadataSys.GetPolicyConfig(bucket)
found := true
if _, ok := err.(BucketPolicyNotFound); ok {
found = false
@@ -2984,6 +2999,7 @@ func (c *SiteReplicationSys) SiteReplicationMetaInfo(ctx context.Context, objAPI
return info, wrapSRErr(err)
}
bms.Policy = policyJSON
bms.PolicyUpdatedAt = updatedAt
}
// Get bucket tags if present.
@@ -3004,6 +3020,23 @@ func (c *SiteReplicationSys) SiteReplicationMetaInfo(ctx context.Context, objAPI
bms.TagConfigUpdatedAt = updatedAt
}
versioningCfg, updatedAt, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
found = true
if versioningCfg != nil && versioningCfg.Status == "" {
found = false
} else if err != nil {
return info, errSRBackendIssue(err)
}
if found {
versionCfgData, err := xml.Marshal(versioningCfg)
if err != nil {
return info, wrapSRErr(err)
}
versioningCfgStr := base64.StdEncoding.EncodeToString(versionCfgData)
bms.Versioning = &versioningCfgStr
bms.VersioningConfigUpdatedAt = updatedAt
}
// Get object-lock config if present.
objLockCfg, updatedAt, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
found = true
@@ -3111,13 +3144,17 @@ func (c *SiteReplicationSys) SiteReplicationMetaInfo(ctx context.Context, objAPI
}
} else {
globalIAMSys.store.rlock()
if err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, false, userPolicyMap); err != nil {
return info, errSRBackendIssue(err)
}
if err = globalIAMSys.store.loadMappedPolicies(ctx, regUser, false, userPolicyMap); err != nil {
return info, errSRBackendIssue(err)
}
stsErr := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, false, userPolicyMap)
globalIAMSys.store.runlock()
if stsErr != nil {
return info, errSRBackendIssue(stsErr)
}
globalIAMSys.store.rlock()
usrErr := globalIAMSys.store.loadMappedPolicies(ctx, regUser, false, userPolicyMap)
globalIAMSys.store.runlock()
if usrErr != nil {
return info, errSRBackendIssue(usrErr)
}
}
info.UserPolicies = make(map[string]madmin.SRPolicyMapping, len(userPolicyMap))
for user, mp := range userPolicyMap {
@@ -3172,13 +3209,17 @@ func (c *SiteReplicationSys) SiteReplicationMetaInfo(ctx context.Context, objAPI
}
} else {
globalIAMSys.store.rlock()
if err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, true, groupPolicyMap); err != nil {
return info, errSRBackendIssue(err)
}
if err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, true, groupPolicyMap); err != nil {
return info, errSRBackendIssue(err)
}
stsErr := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, true, groupPolicyMap)
globalIAMSys.store.runlock()
if stsErr != nil {
return info, errSRBackendIssue(stsErr)
}
globalIAMSys.store.rlock()
userErr := globalIAMSys.store.loadMappedPolicies(ctx, regUser, true, groupPolicyMap)
globalIAMSys.store.runlock()
if userErr != nil {
return info, errSRBackendIssue(userErr)
}
}
info.GroupPolicies = make(map[string]madmin.SRPolicyMapping, len(c.state.Peers))
@@ -3948,6 +3989,16 @@ func (c *SiteReplicationSys) healBucketReplicationConfig(ctx context.Context, ob
break
}
}
rcfg, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
if err != nil {
replMismatch = true
}
// validate remote targets on current cluster for this bucket
_, apiErr := validateReplicationDestination(ctx, bucket, rcfg, false)
if apiErr != noError {
replMismatch = true
}
if replMismatch {
err := c.PeerBucketConfigureReplHandler(ctx, bucket)
if err != nil {
+6
View File
@@ -116,6 +116,12 @@ var errDoneForNow = errors.New("done for now")
// to proceed to next entry.
var errSkipFile = errors.New("skip this file")
// Returned by FS drive mode when a fresh disk is specified.
var errFreshDisk = errors.New("FS backend requires existing disk")
// errXLBackend XL drive mode requires fresh deployment.
var errXLBackend = errors.New("XL backend requires fresh disk")
// StorageErr represents error generated by xlStorage call.
type StorageErr string
+6
View File
@@ -1140,6 +1140,10 @@ func checkDiskFatalErrs(errs []error) error {
return errFaultyDisk
}
if countErrs(errs, errXLBackend) == len(errs) {
return errXLBackend
}
return nil
}
@@ -1152,6 +1156,8 @@ func checkDiskFatalErrs(errs []error) error {
// Do not like it :-(
func logFatalErrs(err error, endpoint Endpoint, exit bool) {
switch {
case errors.Is(err, errXLBackend):
logger.Fatal(config.ErrInvalidXLValue(err), "Unable to initialize backend")
case errors.Is(err, errUnsupportedDisk):
var hint string
if endpoint.URL != nil {
+1 -1
View File
@@ -22,7 +22,6 @@ package cmd
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"hash"
@@ -32,6 +31,7 @@ import (
humanize "github.com/dustin/go-humanize"
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/hash/sha256"
xhttp "github.com/minio/minio/internal/http"
)
+13
View File
@@ -203,3 +203,16 @@ type AssumeRoleWithCertificateResponse struct {
RequestID string `xml:"RequestId,omitempty"`
} `xml:"ResponseMetadata,omitempty"`
}
// AssumeRoleWithCustomTokenResponse contains the result of a successful
// AssumeRoleWithCustomToken request.
type AssumeRoleWithCustomTokenResponse struct {
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithCustomTokenResponse" json:"-"`
Result struct {
Credentials auth.Credentials `xml:"Credentials,omitempty"`
AssumedUser string `xml:"AssumedUser,omitempty"`
} `xml:"AssumeRoleWithCustomTokenResult"`
Metadata struct {
RequestID string `xml:"RequestId,omitempty"`
} `xml:"ResponseMetadata,omitempty"`
}
+6
View File
@@ -96,6 +96,7 @@ const (
ErrSTSInsecureConnection
ErrSTSInvalidClientCertificate
ErrSTSNotInitialized
ErrSTSUpstreamError
ErrSTSInternalError
)
@@ -162,6 +163,11 @@ var stsErrCodes = stsErrorCodeMap{
Description: "STS API not initialized, please try again.",
HTTPStatusCode: http.StatusServiceUnavailable,
},
ErrSTSUpstreamError: {
Code: "InternalError",
Description: "An upstream service required for this operation failed - please try again or contact an administrator.",
HTTPStatusCode: http.StatusInternalServerError,
},
ErrSTSInternalError: {
Code: "InternalError",
Description: "We encountered an internal error generating credentials, please try again.",
+137 -8
View File
@@ -20,12 +20,12 @@ package cmd
import (
"bytes"
"context"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
@@ -33,6 +33,7 @@ import (
"github.com/minio/madmin-go"
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/config/identity/openid"
"github.com/minio/minio/internal/hash/sha256"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
iampolicy "github.com/minio/pkg/iam/policy"
@@ -54,11 +55,12 @@ const (
stsLDAPPassword = "LDAPPassword"
// STS API action constants
clientGrants = "AssumeRoleWithClientGrants"
webIdentity = "AssumeRoleWithWebIdentity"
ldapIdentity = "AssumeRoleWithLDAPIdentity"
clientCertificate = "AssumeRoleWithCertificate"
assumeRole = "AssumeRole"
clientGrants = "AssumeRoleWithClientGrants"
webIdentity = "AssumeRoleWithWebIdentity"
ldapIdentity = "AssumeRoleWithLDAPIdentity"
clientCertificate = "AssumeRoleWithCertificate"
customTokenIdentity = "AssumeRoleWithCustomToken"
assumeRole = "AssumeRole"
stsRequestBodyLimit = 10 * (1 << 20) // 10 MiB
@@ -128,6 +130,11 @@ func registerSTSRouter(router *mux.Router) {
stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithCertificate)).
Queries(stsAction, clientCertificate).
Queries(stsVersion, stsAPIVersion)
// AssumeRoleWithCustomToken
stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithCustomToken)).
Queries(stsAction, customTokenIdentity).
Queries(stsVersion, stsAPIVersion)
}
func checkAssumeRoleAuth(ctx context.Context, r *http.Request) (user auth.Credentials, isErrCodeSTS bool, stsErr STSErrorCode) {
@@ -382,7 +389,7 @@ func (sts *stsAPIHandlers) AssumeRoleWithSSO(w http.ResponseWriter, r *http.Requ
policyName = globalIAMSys.CurrentPolicies(policies)
}
if globalAuthZPlugin == nil {
if newGlobalAuthZPluginFn() == nil {
if !ok {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
fmt.Errorf("%s claim missing from the JWT token, credentials will not be generated", iamPolicyClaimNameOpenID()))
@@ -591,7 +598,7 @@ func (sts *stsAPIHandlers) AssumeRoleWithLDAPIdentity(w http.ResponseWriter, r *
// Check if this user or their groups have a policy applied.
ldapPolicies, _ := globalIAMSys.PolicyDBGet(ldapUserDN, false, groupDistNames...)
if len(ldapPolicies) == 0 && globalAuthZPlugin == nil {
if len(ldapPolicies) == 0 && newGlobalAuthZPluginFn() == nil {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
fmt.Errorf("expecting a policy to be set for user `%s` or one of their groups: `%s` - rejecting this request",
ldapUserDN, strings.Join(groupDistNames, "`,`")))
@@ -815,3 +822,125 @@ func (sts *stsAPIHandlers) AssumeRoleWithCertificate(w http.ResponseWriter, r *h
response.Metadata.RequestID = w.Header().Get(xhttp.AmzRequestID)
writeSuccessResponseXML(w, encodeResponse(response))
}
// AssumeRoleWithCustomToken implements user authentication with custom tokens.
// These tokens are opaque to MinIO and are verified by a configured (external)
// Identity Management Plugin.
//
// API endpoint: https://minio:9000?Action=AssumeRoleWithCustomToken&Token=xxx
func (sts *stsAPIHandlers) AssumeRoleWithCustomToken(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "AssumeRoleWithCustomToken")
if globalAuthNPlugin == nil {
writeSTSErrorResponse(ctx, w, true, ErrSTSNotInitialized, errors.New("STS API 'AssumeRoleWithCustomToken' is disabled"))
return
}
action := r.Form.Get(stsAction)
if action != customTokenIdentity {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Unsupported action %s", action))
return
}
defer logger.AuditLog(ctx, w, r, nil)
token := r.Form.Get(stsToken)
if token == "" {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Invalid empty `Token` parameter provided"))
return
}
durationParam := r.Form.Get(stsDurationSeconds)
var requestedDuration int
if durationParam != "" {
var err error
requestedDuration, err = strconv.Atoi(durationParam)
if err != nil {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Invalid requested duration: %s", durationParam))
return
}
}
roleArnStr := r.Form.Get(stsRoleArn)
roleArn, _, err := globalIAMSys.GetRolePolicy(roleArnStr)
if err != nil {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
fmt.Errorf("Error processing parameter %s: %v", stsRoleArn, err))
return
}
res, err := globalAuthNPlugin.Authenticate(roleArn, token)
if err != nil {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err)
return
}
// If authentication failed, return the error message to the user.
if res.Failure != nil {
writeSTSErrorResponse(ctx, w, true, ErrSTSUpstreamError, errors.New(res.Failure.Reason))
return
}
// It is required that parent user be set.
if res.Success.User == "" {
writeSTSErrorResponse(ctx, w, true, ErrSTSUpstreamError, errors.New("A valid user was not returned by the authenticator."))
return
}
// Expiry is set as minimum of requested value and value allowed by auth
// plugin.
expiry := res.Success.MaxValiditySeconds
if durationParam != "" && requestedDuration < expiry {
expiry = requestedDuration
}
parentUser := "custom:" + res.Success.User
// metadata map
m := map[string]interface{}{
expClaim: UTCNow().Add(time.Duration(expiry) * time.Second).Unix(),
parentClaim: parentUser,
subClaim: parentUser,
roleArnClaim: roleArn.String(),
}
// Add all other claims from the plugin **without** replacing any
// existing claims.
for k, v := range res.Success.Claims {
if _, ok := m[k]; !ok {
m[k] = v
}
}
tmpCredentials, err := auth.GetNewCredentialsWithMetadata(m, globalActiveCred.SecretKey)
if err != nil {
writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err)
return
}
tmpCredentials.ParentUser = parentUser
err = globalIAMSys.SetTempUser(ctx, tmpCredentials.AccessKey, tmpCredentials, "")
if err != nil {
writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err)
return
}
// Call hook for site replication.
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
Type: madmin.SRIAMItemSTSAcc,
STSCredential: &madmin.SRSTSCredential{
AccessKey: tmpCredentials.AccessKey,
SecretKey: tmpCredentials.SecretKey,
SessionToken: tmpCredentials.SessionToken,
ParentUser: tmpCredentials.ParentUser,
},
}); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
response := new(AssumeRoleWithCustomTokenResponse)
response.Result.Credentials = tmpCredentials
response.Result.AssumedUser = parentUser
response.Metadata.RequestID = w.Header().Get(xhttp.AmzRequestID)
writeSuccessResponseXML(w, encodeResponse(response))
}
+4 -4
View File
@@ -42,10 +42,10 @@ func runAllIAMSTSTests(suite *TestSuiteIAM, c *check) {
func TestIAMInternalIDPSTSServerSuite(t *testing.T) {
baseTestCases := []TestSuiteCommon{
// Init and run test on FS backend with signature v4.
{serverType: "FS", signer: signerV4},
// Init and run test on FS backend, with tls enabled.
{serverType: "FS", signer: signerV4, secure: true},
// Init and run test on ErasureSD backend with signature v4.
{serverType: "ErasureSD", signer: signerV4},
// Init and run test on ErasureSD backend, with tls enabled.
{serverType: "ErasureSD", signer: signerV4, secure: true},
// Init and run test on Erasure backend.
{serverType: "Erasure", signer: signerV4},
// Init and run test on ErasureSet backend.
+4 -3
View File
@@ -19,12 +19,13 @@ func _() {
_ = x[ErrSTSInsecureConnection-8]
_ = x[ErrSTSInvalidClientCertificate-9]
_ = x[ErrSTSNotInitialized-10]
_ = x[ErrSTSInternalError-11]
_ = x[ErrSTSUpstreamError-11]
_ = x[ErrSTSInternalError-12]
}
const _STSErrorCode_name = "STSNoneSTSAccessDeniedSTSMissingParameterSTSInvalidParameterValueSTSWebIdentityExpiredTokenSTSClientGrantsExpiredTokenSTSInvalidClientGrantsTokenSTSMalformedPolicyDocumentSTSInsecureConnectionSTSInvalidClientCertificateSTSNotInitializedSTSInternalError"
const _STSErrorCode_name = "STSNoneSTSAccessDeniedSTSMissingParameterSTSInvalidParameterValueSTSWebIdentityExpiredTokenSTSClientGrantsExpiredTokenSTSInvalidClientGrantsTokenSTSMalformedPolicyDocumentSTSInsecureConnectionSTSInvalidClientCertificateSTSNotInitializedSTSUpstreamErrorSTSInternalError"
var _STSErrorCode_index = [...]uint8{0, 7, 22, 41, 65, 91, 118, 145, 171, 192, 219, 236, 252}
var _STSErrorCode_index = [...]uint16{0, 7, 22, 41, 65, 91, 118, 145, 171, 192, 219, 236, 252, 268}
func (i STSErrorCode) String() string {
if i < 0 || i >= STSErrorCode(len(_STSErrorCode_index)-1) {
+13 -19
View File
@@ -78,6 +78,8 @@ func TestMain(m *testing.M) {
// set to 'true' when testing is invoked
globalIsTesting = true
globalIsCICD = globalIsTesting
globalActiveCred = auth.Credentials{
AccessKey: auth.DefaultAccessKey,
SecretKey: auth.DefaultSecretKey,
@@ -191,10 +193,14 @@ func prepareFS() (ObjectLayer, string, error) {
if err != nil {
return nil, "", err
}
obj, err := NewFSObjectLayer(fsDirs[0])
obj, _, err := initObjectLayer(context.Background(), mustGetPoolEndpoints(fsDirs...))
if err != nil {
return nil, "", err
}
initAllSubsystems()
globalIAMSys.Init(context.Background(), obj, globalEtcdClient, 2*time.Second)
return obj, fsDirs[0], nil
}
@@ -221,8 +227,7 @@ func prepareErasure16(ctx context.Context) (ObjectLayer, []string, error) {
// Initialize FS objects.
func initFSObjects(disk string, t *testing.T) (obj ObjectLayer) {
var err error
obj, err = NewFSObjectLayer(disk)
obj, _, err := initObjectLayer(context.Background(), mustGetPoolEndpoints(disk))
if err != nil {
t.Fatal(err)
}
@@ -242,8 +247,8 @@ type TestErrHandler interface {
}
const (
// FSTestStr is the string which is used as notation for Single node ObjectLayer in the unit tests.
FSTestStr string = "FS"
// ErasureSDStr is the string which is used as notation for Single node ObjectLayer in the unit tests.
ErasureSDStr string = "ErasureSD"
// ErasureTestStr is the string which is used as notation for Erasure ObjectLayer in the unit tests.
ErasureTestStr string = "Erasure"
@@ -1469,20 +1474,9 @@ func getRandomDisks(N int) ([]string, error) {
// Initialize object layer with the supplied disks, objectLayer is nil upon any error.
func newTestObjectLayer(ctx context.Context, endpointServerPools EndpointServerPools) (newObject ObjectLayer, err error) {
// For FS only, directly use the disk.
if endpointServerPools.NEndpoints() == 1 {
// Initialize new FS object layer.
return NewFSObjectLayer(endpointServerPools[0].Endpoints[0].Path)
}
z, err := newErasureServerPools(ctx, endpointServerPools)
if err != nil {
return nil, err
}
initAllSubsystems()
return z, nil
return newErasureServerPools(ctx, endpointServerPools)
}
// initObjectLayer - Instantiates object layer and returns it.
@@ -1750,7 +1744,7 @@ func ExecObjectLayerAPITest(t *testing.T, objAPITest objAPITestType, endpoints [
credentials := globalActiveCred
// Executing the object layer tests for single node setup.
objAPITest(objLayer, FSTestStr, bucketFS, fsAPIRouter, credentials, t)
objAPITest(objLayer, ErasureSDStr, bucketFS, fsAPIRouter, credentials, t)
objLayer, erasureDisks, err := prepareErasure16(ctx)
if err != nil {
@@ -1816,7 +1810,7 @@ func ExecObjectLayerTest(t TestErrHandler, objTest objTestType) {
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
// Executing the object layer tests for single node setup.
objTest(objLayer, FSTestStr, t)
objTest(objLayer, ErasureSDStr, t)
// Call clean up functions
cancel()
+6 -6
View File
@@ -74,7 +74,7 @@ func (api adminAPIHandlers) AddTierHandler(w http.ResponseWriter, r *http.Reques
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
@@ -132,7 +132,7 @@ func (api adminAPIHandlers) ListTierHandler(w http.ResponseWriter, r *http.Reque
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
@@ -158,7 +158,7 @@ func (api adminAPIHandlers) EditTierHandler(w http.ResponseWriter, r *http.Reque
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
@@ -210,7 +210,7 @@ func (api adminAPIHandlers) RemoveTierHandler(w http.ResponseWriter, r *http.Req
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
@@ -247,7 +247,7 @@ func (api adminAPIHandlers) VerifyTierHandler(w http.ResponseWriter, r *http.Req
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
@@ -273,7 +273,7 @@ func (api adminAPIHandlers) TierStatsHandler(w http.ResponseWriter, r *http.Requ
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
if !globalIsErasure {
if globalIsGateway {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
+2
View File
@@ -906,6 +906,8 @@ func getMinioMode() string {
mode = globalMinioModeErasure
} else if globalIsGateway {
mode = globalMinioModeGatewayPrefix + globalGatewayName
} else if globalIsErasureSD {
mode = globalMinioModeErasureSD
}
return mode
}
+9 -2
View File
@@ -50,7 +50,9 @@ func getAllFileInfoVersions(xlMetaBuf []byte, volume, path string) (FileInfoVers
var versions []FileInfo
var err error
if buf, _ := isIndexedMetaV2(xlMetaBuf); buf != nil {
if buf, _, e := isIndexedMetaV2(xlMetaBuf); e != nil {
return FileInfoVersions{}, e
} else if buf != nil {
versions, err = buf.ListVersions(volume, path)
} else {
var xlMeta xlMetaV2
@@ -87,7 +89,9 @@ func getFileInfo(xlMetaBuf []byte, volume, path, versionID string, data bool) (F
var fi FileInfo
var err error
var inData xlMetaInlineData
if buf, data := isIndexedMetaV2(xlMetaBuf); buf != nil {
if buf, data, e := isIndexedMetaV2(xlMetaBuf); e != nil {
return FileInfo{}, e
} else if buf != nil {
inData = data
fi, err = buf.ToFileInfo(volume, path, versionID)
if len(buf) != 0 && errors.Is(err, errFileNotFound) {
@@ -141,6 +145,9 @@ func getFileInfo(xlMetaBuf []byte, volume, path, versionID string, data bool) (F
// Will return -1 for unknown values.
func getXLDiskLoc(diskID string) (poolIdx, setIdx, diskIdx int) {
if api := newObjectLayerFn(); api != nil {
if globalIsErasureSD {
return 0, 0, 0
}
if ep, ok := api.(*erasureServerPools); ok {
if pool, set, disk, err := ep.getPoolAndSet(diskID); err == nil {
return pool, set, disk
+1 -1
View File
@@ -53,7 +53,7 @@ func isXLMetaFormatValid(version, format string) bool {
// Verifies if the backend format metadata is sane by validating
// the ErasureInfo, i.e. data and parity blocks.
func isXLMetaErasureInfoValid(data, parity int) bool {
return ((data >= parity) && (data != 0) && (parity != 0))
return ((data >= parity) && (data > 0) && (parity >= 0))
}
//go:generate msgp -file=$GOFILE -unexported
+12 -12
View File
@@ -793,34 +793,32 @@ func decodeVersions(buf []byte, versions int, fn func(idx int, hdr, meta []byte)
}
// isIndexedMetaV2 returns non-nil result if metadata is indexed.
// If data doesn't validate nil is also returned.
func isIndexedMetaV2(buf []byte) (meta xlMetaBuf, data xlMetaInlineData) {
// Returns 3x nil if not XLV2 or not indexed.
// If indexed and unable to parse an error will be returned.
func isIndexedMetaV2(buf []byte) (meta xlMetaBuf, data xlMetaInlineData, err error) {
buf, major, minor, err := checkXL2V1(buf)
if err != nil {
return nil, nil
}
if major != 1 || minor < 3 {
return nil, nil
if err != nil || major != 1 || minor < 3 {
return nil, nil, nil
}
meta, buf, err = msgp.ReadBytesZC(buf)
if err != nil {
return nil, nil
return nil, nil, err
}
if crc, nbuf, err := msgp.ReadUint32Bytes(buf); err == nil {
// Read metadata CRC
buf = nbuf
if got := uint32(xxhash.Sum64(meta)); got != crc {
return nil, nil
return nil, nil, fmt.Errorf("xlMetaV2.Load version(%d), CRC mismatch, want 0x%x, got 0x%x", minor, crc, got)
}
} else {
return nil, nil
return nil, nil, err
}
data = buf
if data.validate() != nil {
data.repair()
}
return meta, data
return meta, data, nil
}
type xlMetaV2ShallowVersion struct {
@@ -865,7 +863,9 @@ func (x *xlMetaV2) LoadOrConvert(buf []byte) error {
// Load all versions of the stored data.
// Note that references to the incoming buffer will be kept.
func (x *xlMetaV2) Load(buf []byte) error {
if meta, data := isIndexedMetaV2(buf); meta != nil {
if meta, data, err := isIndexedMetaV2(buf); err != nil {
return err
} else if meta != nil {
return x.loadIndexed(meta, data)
}
// Convert older format.
+8 -6
View File
@@ -62,13 +62,15 @@ func TestIsXLMetaErasureInfoValid(t *testing.T) {
{1, 5, 6, false},
{2, 5, 5, true},
{3, 0, 5, false},
{4, 5, 0, false},
{5, 5, 0, false},
{6, 5, 4, true},
{3, -1, 5, false},
{4, 5, -1, false},
{5, 5, 0, true},
{6, 5, 0, true},
{7, 5, 4, true},
}
for _, tt := range tests {
if got := isXLMetaErasureInfoValid(tt.data, tt.parity); got != tt.want {
t.Errorf("Test %d: Expected %v but received %v", tt.name, got, tt.want)
t.Errorf("Test %d: Expected %v but received %v -> %#v", tt.name, got, tt.want, tt)
}
}
}
@@ -512,7 +514,7 @@ func BenchmarkXlMetaV2Shallow(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
buf, _ := isIndexedMetaV2(enc)
buf, _, _ := isIndexedMetaV2(enc)
if buf == nil {
b.Fatal("buf == nil")
}
@@ -527,7 +529,7 @@ func BenchmarkXlMetaV2Shallow(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
buf, _ := isIndexedMetaV2(enc)
buf, _, _ := isIndexedMetaV2(enc)
if buf == nil {
b.Fatal("buf == nil")
}
+8 -2
View File
@@ -443,6 +443,9 @@ func (s *xlStorage) NSScanner(ctx context.Context, cache dataUsageCache, updates
}
}
}
vcfg, _ := globalBucketVersioningSys.Get(cache.Info.Name)
// return initialized object layer
objAPI := newObjectLayerFn()
// object layer not initialized, return.
@@ -494,9 +497,12 @@ func (s *xlStorage) NSScanner(ctx context.Context, cache dataUsageCache, updates
}
return sizeSummary{}, errSkipFile
}
versioned := vcfg != nil && vcfg.Versioned(item.objectPath())
for _, version := range fivs.Versions {
atomic.AddUint64(&globalScannerStats.accTotalVersions, 1)
oi := version.ToObjectInfo(item.bucket, item.objectPath())
oi := version.ToObjectInfo(item.bucket, item.objectPath(), versioned)
sz := item.applyActions(ctx, objAPI, oi, &sizeS)
if oi.VersionID != "" && sz == oi.Size {
sizeS.versions++
@@ -521,7 +527,7 @@ func (s *xlStorage) NSScanner(ctx context.Context, cache dataUsageCache, updates
// apply tier sweep action on free versions
for _, freeVersion := range fivs.FreeVersions {
oi := freeVersion.ToObjectInfo(item.bucket, item.objectPath())
oi := freeVersion.ToObjectInfo(item.bucket, item.objectPath(), versioned)
item.applyTierObjSweep(ctx, objAPI, oi)
}
return sizeS, nil
+1 -1
View File
@@ -309,7 +309,7 @@ Example: The following settings will increase the heal operation speed by allowi
Once set the healer settings are automatically applied without the need for server restarts.
> NOTE: Healing is not supported for gateway and single drive mode.
> NOTE: Healing is not supported for Gateway deployments.
## Environment only settings (not in config)
+27 -6
View File
@@ -38,6 +38,7 @@ var (
bucket, prefix string
debug bool
versions bool
insecure bool
)
// getMD5Sum returns MD5 sum of given data.
@@ -55,6 +56,7 @@ func main() {
flag.StringVar(&prefix, "prefix", "", "Select a prefix")
flag.BoolVar(&debug, "debug", false, "Prints HTTP network calls to S3 endpoint")
flag.BoolVar(&versions, "versions", false, "Verify all versions")
flag.BoolVar(&insecure, "insecure", false, "Disable TLS verification")
flag.Parse()
if endpoint == "" {
@@ -78,12 +80,23 @@ func main() {
log.Fatalln(err)
}
secure := strings.EqualFold(u.Scheme, "https")
transport, err := minio.DefaultTransport(secure)
if err != nil {
log.Fatalln(err)
}
if insecure {
// skip TLS verification
transport.TLSClientConfig.InsecureSkipVerify = true
}
s3Client, err := minio.New(u.Host, &minio.Options{
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
Secure: strings.EqualFold(u.Scheme, "https"),
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
Secure: secure,
Transport: transport,
})
if err != nil {
log.Fatalln()
log.Fatalln(err)
}
if debug {
@@ -114,7 +127,7 @@ func main() {
// List all objects from a bucket-name with a matching prefix.
for object := range s3Client.ListObjects(context.Background(), bucket, opts) {
if object.Err != nil {
log.Fatalln("LIST error:", object.Err)
log.Println("LIST error:", object.Err)
continue
}
if object.IsDeleteMarker {
@@ -149,6 +162,7 @@ func main() {
}
var partsMD5Sum [][]byte
var failedMD5 bool
for p := 1; p <= parts; p++ {
opts := minio.GetObjectOptions{
VersionID: object.VersionID,
@@ -157,16 +171,23 @@ func main() {
obj, err := s3Client.GetObject(context.Background(), bucket, object.Key, opts)
if err != nil {
log.Println("GET", bucket, object.Key, object.VersionID, "=>", err)
continue
failedMD5 = true
break
}
h := md5.New()
if _, err := io.Copy(h, obj); err != nil {
log.Println("MD5 calculation error:", bucket, object.Key, object.VersionID, "=>", err)
continue
failedMD5 = true
break
}
partsMD5Sum = append(partsMD5Sum, h.Sum(nil))
}
if failedMD5 {
log.Println("CORRUPTED object:", bucket, object.Key, object.VersionID)
continue
}
corrupted := false
if !multipart {
md5sum := fmt.Sprintf("%x", partsMD5Sum[0])
+3 -1
View File
@@ -1,4 +1,6 @@
# MinIO NAS Gateway [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
# MinIO NAS Gateway [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
> NAS gateway is deprecated and will be removed in future, no more fresh deployments are supported.
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.
+76
View File
@@ -0,0 +1,76 @@
# Identity Management Plugin Guide [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io)
## Introduction
To enable the integration of custom authentication methods, MinIO can be configured with an Identity Management Plugin webhook. When configured, this plugin enables the `AssumeRoleWithCustomToken` STS API extension. A user or application can now present a token to the `AssumeRoleWithCustomToken` API, and MinIO verifies this token by sending it to the Identity Management Plugin webhook. This plugin responds with some information and MinIO is able to generate temporary STS credentials to interact with object storage.
The authentication flow is similar to that of OpenID, however the token is "opaque" to MinIO - it is simply sent to the plugin for verification. CAVEAT: There is no console UI integration for this method of authentication and it is intended primarily for machine authentication.
It can be configured via MinIO's standard configuration API (i.e. using `mc admin config set/get`), or equivalently with environment variables. For brevity we show only environment variables here:
```sh
$ mc admin config set myminio identity_plugin --env
KEY:
identity_plugin enable Identity Plugin via external hook
ARGS:
MINIO_IDENTITY_PLUGIN_URL* (url) plugin hook endpoint (HTTP(S)) e.g. "http://localhost:8181/path/to/endpoint"
MINIO_IDENTITY_PLUGIN_AUTH_TOKEN (string) authorization token for plugin hook endpoint
MINIO_IDENTITY_PLUGIN_ROLE_POLICY* (string) policies to apply for plugin authorized users
MINIO_IDENTITY_PLUGIN_ROLE_ID (string) unique ID to generate the ARN
MINIO_IDENTITY_PLUGIN_COMMENT (sentence) optionally add a comment to this setting
```
If provided, the auth token parameter is sent as an authorization header.
`MINIO_IDENTITY_PLUGIN_ROLE_POLICY` is a required parameter and can be list of comma separated policy names.
On setting up the plugin, the MinIO server prints the Role ARN to its log. The Role ARN is generated by default based on the given plugin URL. To avoid this and use a configurable value set a unique role ID via `MINIO_IDENTITY_PLUGIN_ROLE_ID`.
## REST API call to plugin
To verify the custom token presented in the `AssumeRoleWithCustomToken` API, MinIO makes a POST request to the configured identity management plugin endpoint and expects a response with some details as shown below:
### Request `POST` to plugin endpoint
Query parameters:
| Parameter Name | Value Type | Purpose |
|----------------|------------|-------------------------------------------------------------------------|
| token | string | Token from the AssumeRoleWithCustomToken call for external verification |
### Response
If the token is valid and access is approved, the plugin must return a `200` (OK) HTTP status code.
A `200 OK` Response should have `application/json` content-type and body with the following structure:
```json
{
"user": <string>,
"maxValiditySeconds": <integer>,
"claims": <key-value-pairs>
}
```
| Parameter Name | Value Type | Purpose |
|--------------------|-----------------------------------------|--------------------------------------------------------|
| user | string | Identifier for owner of requested credentials |
| maxValiditySeconds | integer (>= 900 seconds and < 365 days) | Maximum allowed expiry duration for the credentials |
| claims | key-value pairs | Claims to be associated with the requested credentials |
The keys "exp", "parent" and "sub" in the `claims` object are reserved and if present are ignored by MinIO.
If the token is not valid or access is not approved, the plugin must return a `403` (forbidden) HTTP status code. The body must have an `application/json` content-type with the following structure:
```json
{
"reason": <string>
}
```
The reason message is returned to the client.
## Example Plugin Implementation
A toy example for the Identity Management Plugin is given [here](./identity-manager-plugin.go).
+86
View File
@@ -0,0 +1,86 @@
//go:build ignore
// +build ignore
// Copyright (c) 2015-2022 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
)
func writeErrorResponse(w http.ResponseWriter, err error) {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"reason": fmt.Sprintf("%v", err),
})
}
type Resp struct {
User string `json:"user"`
MaxValiditySeconds int `json:"maxValiditySeconds"`
Claims map[string]interface{} `json:"claims"`
}
var tokens map[string]Resp = map[string]Resp{
"aaa": {
User: "Alice",
MaxValiditySeconds: 3600,
Claims: map[string]interface{}{
"groups": []string{"data-science"},
},
},
"bbb": {
User: "Bart",
MaxValiditySeconds: 3600,
Claims: map[string]interface{}{
"groups": []string{"databases"},
},
},
}
func mainHandler(w http.ResponseWriter, r *http.Request) {
token := r.FormValue("token")
if token == "" {
writeErrorResponse(w, errors.New("token parameter not given"))
return
}
rsp, ok := tokens[token]
if !ok {
w.WriteHeader(http.StatusForbidden)
return
}
fmt.Printf("Allowed for token: %s user: %s\n", token, rsp.User)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(rsp)
return
}
func main() {
http.HandleFunc("/", mainHandler)
log.Print("Listing on :8081")
log.Fatal(http.ListenAndServe(":8081", nil))
}
+1 -1
View File
@@ -11,7 +11,7 @@
Visualize MinIO metrics with our official Grafana dashboard available on the [Grafana dashboard portal](https://grafana.com/grafana/dashboards/13502).
Refer to the dashboard [json file here](https://raw.githubusercontent.com/minio/minio/master/docs/metrics/prometheus/grafana/minio-overview.json).
Refer to the dashboard [json file here](https://raw.githubusercontent.com/minio/minio/master/docs/metrics/prometheus/grafana/minio-dashboard.json).
![Grafana](https://raw.githubusercontent.com/minio/minio/master/docs/metrics/prometheus/grafana/grafana-minio.png)
@@ -16,22 +16,22 @@
"name": "Bar gauge",
"version": ""
},
{
"type": "panel",
"id": "gauge",
"name": "Gauge",
"version": ""
},
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "8.0.6"
"version": "8.2.1"
},
{
"type": "panel",
"id": "graph",
"name": "Graph",
"name": "Graph (old)",
"version": ""
},
{
"type": "panel",
"id": "piechart",
"name": "Pie chart",
"version": ""
},
{
@@ -56,16 +56,23 @@
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"description": "MinIO dashboard - https://min.io/",
"description": "MinIO Grafana Dashboard - https://min.io/",
"editable": true,
"fiscalYearStartMonth": 0,
"gnetId": 13502,
"graphTooltip": 0,
"id": null,
"iteration": 1629787190164,
"iteration": 1653756543524,
"links": [
{
"icon": "external link",
@@ -77,6 +84,7 @@
"type": "dashboards"
}
],
"liveNow": false,
"panels": [
{
"cacheTimeout": null,
@@ -133,7 +141,7 @@
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
@@ -208,7 +216,7 @@
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
@@ -232,8 +240,19 @@
{
"cacheTimeout": null,
"datasource": "${DS_PROMETHEUS}",
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": [
{
"options": {
@@ -245,22 +264,13 @@
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "rgba(255, 255, 255, 0.97)",
"value": null
}
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 6,
"w": 3,
"w": 4,
"x": 6,
"y": 0
},
@@ -269,7 +279,15 @@
"links": [],
"maxDataPoints": 100,
"options": {
"orientation": "horizontal",
"displayLabels": [],
"legend": {
"displayMode": "table",
"placement": "bottom",
"values": [
"percent"
]
},
"pieType": "donut",
"reduceOptions": {
"calcs": [
"lastNotNull"
@@ -277,28 +295,36 @@
"fields": "",
"values": false
},
"showThresholdLabels": false,
"showThresholdMarkers": true,
"text": {}
"tooltip": {
"mode": "single"
}
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
"expr": "topk(1, sum(minio_cluster_capacity_usable_free_bytes{job=\"$scrape_jobs\"}) by (instance))",
"expr": "topk(1, sum(minio_cluster_capacity_usable_total_bytes{job=\"$scrape_jobs\"}) by (instance)) - topk(1, sum(minio_cluster_capacity_usable_free_bytes{job=\"$scrape_jobs\"}) by (instance))",
"format": "time_series",
"instant": false,
"interval": "1m",
"intervalFactor": 1,
"legendFormat": "",
"legendFormat": "Used",
"refId": "A",
"step": 300
},
{
"exemplar": true,
"expr": "topk(1, sum(minio_cluster_capacity_usable_free_bytes{job=\"$scrape_jobs\"}) by (instance)) ",
"hide": false,
"interval": "1m",
"legendFormat": "Free",
"refId": "B"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Current Usable Capacity",
"type": "gauge"
"title": "Capacity",
"type": "piechart"
},
{
"aliasColors": {},
@@ -310,8 +336,8 @@
"fillGradient": 0,
"gridPos": {
"h": 6,
"w": 7,
"x": 9,
"w": 6,
"x": 10,
"y": 0
},
"hiddenSeries": false,
@@ -332,7 +358,7 @@
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"pointradius": 2,
"points": false,
"renderer": "flot",
@@ -434,7 +460,7 @@
"showUnfilled": false,
"text": {}
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
@@ -512,7 +538,7 @@
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
@@ -588,7 +614,7 @@
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
@@ -668,7 +694,7 @@
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
@@ -748,7 +774,7 @@
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
@@ -828,7 +854,7 @@
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
@@ -911,7 +937,7 @@
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
@@ -962,7 +988,7 @@
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"pointradius": 2,
"points": false,
"renderer": "flot",
@@ -1058,7 +1084,7 @@
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"pointradius": 2,
"points": false,
"renderer": "flot",
@@ -1178,7 +1204,7 @@
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
@@ -1258,7 +1284,7 @@
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
@@ -1341,7 +1367,7 @@
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
@@ -1405,7 +1431,7 @@
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
@@ -1473,7 +1499,7 @@
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"targets": [
{
"exemplar": true,
@@ -1528,7 +1554,7 @@
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"pointradius": 2,
"points": false,
"renderer": "flot",
@@ -1624,7 +1650,7 @@
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"pointradius": 2,
"points": false,
"renderer": "flot",
@@ -1730,7 +1756,7 @@
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"pointradius": 5,
"points": false,
"renderer": "flot",
@@ -1833,7 +1859,7 @@
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"pointradius": 2,
"points": false,
"renderer": "flot",
@@ -1941,7 +1967,7 @@
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"pointradius": 2,
"points": false,
"renderer": "flot",
@@ -2033,7 +2059,7 @@
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"pointradius": 2,
"points": false,
"renderer": "flot",
@@ -2125,7 +2151,7 @@
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"pointradius": 2,
"points": false,
"renderer": "flot",
@@ -2219,7 +2245,7 @@
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"pointradius": 2,
"points": false,
"renderer": "flot",
@@ -2326,7 +2352,7 @@
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"pointradius": 2,
"points": false,
"renderer": "flot",
@@ -2443,7 +2469,7 @@
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"pointradius": 2,
"points": false,
"renderer": "flot",
@@ -2536,7 +2562,7 @@
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.0.6",
"pluginVersion": "8.2.1",
"pointradius": 2,
"points": false,
"renderer": "flot",
@@ -2606,8 +2632,8 @@
}
}
],
"refresh": "10s",
"schemaVersion": 30,
"refresh": false,
"schemaVersion": 31,
"style": "dark",
"tags": [
"minio"
@@ -2668,7 +2694,7 @@
]
},
"timezone": "",
"title": "MinIO Overview",
"title": "MinIO Dashboard",
"uid": "TgmJnqnnk",
"version": 18
"version": 7
}
@@ -2,7 +2,7 @@ version: '3.7'
# Settings and configurations that are common for all containers
x-minio-common: &minio-common
image: quay.io/minio/minio:RELEASE.2022-05-23T18-45-11Z
image: quay.io/minio/minio:RELEASE.2022-05-26T05-48-41Z
command: server --console-address ":9001" http://minio{1...4}/data{1...2}
expose:
- "9000"
+121
View File
@@ -0,0 +1,121 @@
//go:build ignore
// +build ignore
// Copyright (c) 2015-2022 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"context"
"flag"
"fmt"
"log"
"net/url"
"time"
"github.com/minio/minio-go/v7"
cr "github.com/minio/minio-go/v7/pkg/credentials"
)
var (
// LDAP integrated Minio endpoint
stsEndpoint string
// token to use with AssumeRoleWithCustomToken
token string
// Role ARN to use
roleArn string
// Display credentials flag
displayCreds bool
// Credential expiry duration
expiryDuration time.Duration
// Bucket to list
bucketToList string
)
func init() {
flag.StringVar(&stsEndpoint, "sts-ep", "http://localhost:9000", "STS endpoint")
flag.StringVar(&token, "t", "", "Token to use with AssumeRoleWithCustomToken STS API (required)")
flag.StringVar(&roleArn, "r", "", "RoleARN to use with the request (required)")
flag.BoolVar(&displayCreds, "d", false, "Only show generated credentials")
flag.DurationVar(&expiryDuration, "e", 0, "Request a duration of validity for the generated credential")
flag.StringVar(&bucketToList, "b", "mybucket", "Bucket to list (defaults to mybucket)")
}
func main() {
flag.Parse()
if token == "" || roleArn == "" {
flag.PrintDefaults()
return
}
// The credentials package in minio-go provides an interface to call the
// AssumeRoleWithCustomToken STS API.
var opts []cr.CustomTokenOpt
if expiryDuration != 0 {
opts = append(opts, cr.CustomTokenValidityOpt(expiryDuration))
}
// Initialize
li, err := cr.NewCustomTokenCredentials(stsEndpoint, token, roleArn, opts...)
if err != nil {
log.Fatalf("Error initializing CustomToken Identity: %v", err)
}
v, err := li.Get()
if err != nil {
log.Fatalf("Error retrieving STS credentials: %v", err)
}
if displayCreds {
fmt.Println("Only displaying credentials:")
fmt.Println("AccessKeyID:", v.AccessKeyID)
fmt.Println("SecretAccessKey:", v.SecretAccessKey)
fmt.Println("SessionToken:", v.SessionToken)
return
}
// Use generated credentials to authenticate with MinIO server
stsEndpointURL, err := url.Parse(stsEndpoint)
if err != nil {
log.Fatalf("Error parsing sts endpoint: %v", err)
}
copts := &minio.Options{
Creds: li,
Secure: stsEndpointURL.Scheme == "https",
}
minioClient, err := minio.New(stsEndpointURL.Host, copts)
if err != nil {
log.Fatalf("Error initializing client: ", err)
}
// Use minIO Client object normally like the regular client.
fmt.Printf("Calling list objects on bucket named `%s` with temp creds:\n===\n", bucketToList)
objCh := minioClient.ListObjects(context.Background(), bucketToList, minio.ListObjectsOptions{})
for obj := range objCh {
if obj.Err != nil {
log.Fatalf("Listing error: %v", obj.Err)
}
fmt.Printf("Key: %s\nSize: %d\nLast Modified: %s\n===\n", obj.Key, obj.Size, obj.LastModified)
}
}
+53
View File
@@ -0,0 +1,53 @@
# AssumeRoleWithCustomToken [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
## Introduction
To integrate with custom authentication methods using the [Identity Management Plugin](../iam/identity-management-plugin.md)), MinIO provides an STS API extension called `AssumeRoleWithCustomToken`.
After configuring the plugin, use the generated Role ARN with `AssumeRoleWithCustomToken` to get temporary credentials to access object storage.
## API Request
To make an STS API request with this method, send a POST request to the MinIO endpoint with following query parameters:
| Parameter | Type | Required | |
|-----------------|---------|----------|----------------------------------------------------------------------|
| Action | String | Yes | Value must be `AssumeRoleWithCustomToken` |
| Version | String | Yes | Value must be `2011-06-15` |
| Token | String | Yes | Token to be authenticated by identity plugin |
| RoleArn | String | Yes | Must match the Role ARN generated for the identity plugin |
| DurationSeconds | Integer | No | Duration of validity of generated credentials. Must be at least 900. |
The validity duration of the generated STS credentials is the minimum of the `DurationSeconds` parameter (if passed) and the validity duration returned by the Identity Management Plugin.
## API Response
XML response for this API is similar to [AWS STS AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html#API_AssumeRoleWithWebIdentity_ResponseElements)
## Example request and response
Sample request with `curl`:
```sh
curl -XPOST 'http://localhost:9001/?Action=AssumeRoleWithCustomToken&Version=2011-06-15&Token=aaa&RoleArn=arn:minio:iam:::role/idmp-vGxBdLkOc8mQPU1-UQbBh-yWWVQ'
```
Prettified Response:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<AssumeRoleWithCustomTokenResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleWithCustomTokenResult>
<Credentials>
<AccessKeyId>24Y5H9VHE14H47GEOKCX</AccessKeyId>
<SecretAccessKey>H+aBfQ9B1AeWWb++84hvp4tlFBo9aP+hUTdLFIeg</SecretAccessKey>
<Expiration>2022-05-25T19:56:34Z</Expiration>
<SessionToken>eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiIyNFk1SDlWSEUxNEg0N0dFT0tDWCIsImV4cCI6MTY1MzUwODU5NCwiZ3JvdXBzIjpbImRhdGEtc2NpZW5jZSJdLCJwYXJlbnQiOiJjdXN0b206QWxpY2UiLCJyb2xlQXJuIjoiYXJuOm1pbmlvOmlhbTo6OnJvbGUvaWRtcC14eHgiLCJzdWIiOiJjdXN0b206QWxpY2UifQ.1tO1LmlUNXiy-wl-ZbkJLWTpaPlhaGqHehsi21lNAmAGCImHHsPb-GA4lRq6GkvHAODN5ZYCf_S-OwpOOdxFwA</SessionToken>
</Credentials>
<AssumedUser>custom:Alice</AssumedUser>
</AssumeRoleWithCustomTokenResult>
<ResponseMetadata>
<RequestId>16F26E081E36DE63</RequestId>
</ResponseMetadata>
</AssumeRoleWithCustomTokenResponse>
```
+1 -1
View File
@@ -50,7 +50,7 @@ require (
github.com/minio/kes v0.19.2
github.com/minio/madmin-go v1.3.14
github.com/minio/minio-go/v7 v7.0.27
github.com/minio/pkg v1.1.23
github.com/minio/pkg v1.1.24
github.com/minio/selfupdate v0.4.0
github.com/minio/sha256-simd v1.0.0
github.com/minio/simdjson-go v0.4.2
+2 -2
View File
@@ -636,8 +636,8 @@ github.com/minio/minio-go/v7 v7.0.23/go.mod h1:ei5JjmxwHaMrgsMrn4U/+Nmg+d8MKS1U2
github.com/minio/minio-go/v7 v7.0.27 h1:yJCvm78B+2+ll1PqO9eSD1as6Ibw3IYnnD8PyBEB2zo=
github.com/minio/minio-go/v7 v7.0.27/go.mod h1:x81+AX5gHSfCSqw7jxRKHvxUXMlE5uKX0Vb75Xk5yYg=
github.com/minio/pkg v1.1.20/go.mod h1:Xo7LQshlxGa9shKwJ7NzQbgW4s8T/Wc1cOStR/eUiMY=
github.com/minio/pkg v1.1.23 h1:CJSoPslQCWZW3z3T79+pv9dVBDCQEK3ipiwXcoAtzY0=
github.com/minio/pkg v1.1.23/go.mod h1:z9PfmEI804KFkF6eY4LoGe8IDVvTCsYGVuaf58Dr0WI=
github.com/minio/pkg v1.1.24 h1:a2RCb6LgsCi9DvrripuvlFQRCNb5Hp1HIssnsUqLoZY=
github.com/minio/pkg v1.1.24/go.mod h1:z9PfmEI804KFkF6eY4LoGe8IDVvTCsYGVuaf58Dr0WI=
github.com/minio/selfupdate v0.4.0 h1:A7t07pN4Ch1tBTIRStW0KhUVyykz+2muCqFsITQeEW8=
github.com/minio/selfupdate v0.4.0/go.mod h1:mcDkzMgq8PRcpCRJo/NlPY7U45O5dfYl2Y0Rg7IustY=
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
+5
View File
@@ -88,6 +88,11 @@ func (v Versioning) Enabled() bool {
return v.Status == Enabled
}
// Versioned returns if 'prefix' has versioning enabled or suspended.
func (v Versioning) Versioned(prefix string) bool {
return v.PrefixEnabled(prefix) || v.PrefixSuspended(prefix)
}
// PrefixEnabled - returns true if versioning is enabled at the bucket and given
// prefix, false otherwise.
func (v Versioning) PrefixEnabled(prefix string) bool {
+3
View File
@@ -73,6 +73,7 @@ const (
IdentityOpenIDSubSys = "identity_openid"
IdentityLDAPSubSys = "identity_ldap"
IdentityTLSSubSys = "identity_tls"
IdentityPluginSubSys = "identity_plugin"
CacheSubSys = "cache"
SiteSubSys = "site"
RegionSubSys = "region"
@@ -146,6 +147,7 @@ var SubSystems = set.CreateStringSet(
IdentityLDAPSubSys,
IdentityOpenIDSubSys,
IdentityTLSSubSys,
IdentityPluginSubSys,
ScannerSubSys,
HealSubSys,
NotifyAMQPSubSys,
@@ -188,6 +190,7 @@ var SubSystemsSingleTargets = set.CreateStringSet([]string{
PolicyPluginSubSys,
IdentityLDAPSubSys,
IdentityTLSSubSys,
IdentityPluginSubSys,
HealSubSys,
ScannerSubSys,
}...)
+12
View File
@@ -19,6 +19,18 @@ package config
// UI errors
var (
ErrInvalidFSValue = newErrFn(
"Invalid drive path",
"Please provide an existing deployment with MinIO",
"MinIO does not support newer NAS gateway deployments anymore refer https://github.com/minio/minio/issues/14331",
)
ErrInvalidXLValue = newErrFn(
"Invalid drive path",
"Please provide a fresh drive for single drive MinIO setup",
"MinIO only supports fresh drive paths",
)
ErrInvalidBrowserValue = newErrFn(
"Invalid console value",
"Please check the passed value",
+1 -1
View File
@@ -20,7 +20,6 @@ package openid
import (
"crypto"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"errors"
"io"
@@ -36,6 +35,7 @@ import (
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/config/identity/openid/provider"
"github.com/minio/minio/internal/hash/sha256"
"github.com/minio/pkg/env"
iampolicy "github.com/minio/pkg/iam/policy"
xnet "github.com/minio/pkg/net"
+322
View File
@@ -0,0 +1,322 @@
// Copyright (c) 2015-2022 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package plugin
import (
"bytes"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"github.com/minio/minio/internal/arn"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/env"
xnet "github.com/minio/pkg/net"
)
// Authentication Plugin config and env variables
const (
URL = "url"
AuthToken = "auth_token"
RolePolicy = "role_policy"
RoleID = "role_id"
EnvIdentityPluginURL = "MINIO_IDENTITY_PLUGIN_URL"
EnvIdentityPluginAuthToken = "MINIO_IDENTITY_PLUGIN_AUTH_TOKEN"
EnvIdentityPluginRolePolicy = "MINIO_IDENTITY_PLUGIN_ROLE_POLICY"
EnvIdentityPluginRoleID = "MINIO_IDENTITY_PLUGIN_ROLE_ID"
)
var (
// DefaultKVS - default config for AuthN plugin config
DefaultKVS = config.KVS{
config.KV{
Key: URL,
Value: "",
},
config.KV{
Key: AuthToken,
Value: "",
},
config.KV{
Key: RolePolicy,
Value: "",
},
config.KV{
Key: RoleID,
Value: "",
},
}
defaultHelpPostfix = func(key string) string {
return config.DefaultHelpPostfix(DefaultKVS, key)
}
// Help for Identity Plugin
Help = config.HelpKVS{
config.HelpKV{
Key: URL,
Description: `plugin hook endpoint (HTTP(S)) e.g. "http://localhost:8181/path/to/endpoint"` + defaultHelpPostfix(URL),
Type: "url",
},
config.HelpKV{
Key: AuthToken,
Description: "authorization token for plugin hook endpoint" + defaultHelpPostfix(AuthToken),
Optional: true,
Type: "string",
Sensitive: true,
},
config.HelpKV{
Key: RolePolicy,
Description: "policies to apply for plugin authorized users" + defaultHelpPostfix(RolePolicy),
Type: "string",
},
config.HelpKV{
Key: RoleID,
Description: "unique ID to generate the ARN" + defaultHelpPostfix(RoleID),
Optional: true,
Type: "string",
},
config.HelpKV{
Key: config.Comment,
Description: config.DefaultComment,
Optional: true,
Type: "sentence",
},
}
)
// Allows only Base64 URL encoding characters.
var validRoleIDRegex = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
// Args for authentication plugin.
type Args struct {
URL *xnet.URL
AuthToken string
Transport http.RoundTripper
CloseRespFn func(r io.ReadCloser)
RolePolicy string
RoleARN arn.ARN
}
// Validate - validate configuration params.
func (a *Args) Validate() error {
req, err := http.NewRequest(http.MethodPost, a.URL.String(), bytes.NewReader([]byte("")))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if a.AuthToken != "" {
req.Header.Set("Authorization", a.AuthToken)
}
client := &http.Client{Transport: a.Transport}
resp, err := client.Do(req)
if err != nil {
return err
}
defer a.CloseRespFn(resp.Body)
return nil
}
// AuthNPlugin - implements pluggable authentication via webhook.
type AuthNPlugin struct {
args Args
client *http.Client
}
// Enabled returns if AuthNPlugin is enabled.
func Enabled(kvs config.KVS) bool {
return kvs.Get(URL) != ""
}
// LookupConfig lookup AuthNPlugin from config, override with any ENVs.
func LookupConfig(kv config.KVS, transport *http.Transport, closeRespFn func(io.ReadCloser), serverRegion string) (Args, error) {
args := Args{}
if err := config.CheckValidKeys(config.IdentityPluginSubSys, kv, DefaultKVS); err != nil {
return args, err
}
pluginURL := env.Get(EnvIdentityPluginURL, kv.Get(URL))
if pluginURL == "" {
return args, nil
}
authToken := env.Get(EnvIdentityPluginAuthToken, kv.Get(AuthToken))
u, err := xnet.ParseHTTPURL(pluginURL)
if err != nil {
return args, err
}
rolePolicy := env.Get(EnvIdentityPluginRolePolicy, kv.Get(RolePolicy))
if rolePolicy == "" {
return args, config.Errorf("A role policy must be specified for Identity Management Plugin")
}
resourceID := "idmp-"
roleID := env.Get(EnvIdentityPluginRoleID, kv.Get(RoleID))
if roleID == "" {
// We use a hash of the plugin URL so that the ARN remains
// constant across restarts.
h := sha1.New()
h.Write([]byte(pluginURL))
bs := h.Sum(nil)
resourceID += base64.RawURLEncoding.EncodeToString(bs)
} else {
// Check that the roleID is restricted to URL safe characters
// (base64 URL encoding chars).
if !validRoleIDRegex.MatchString(roleID) {
return args, config.Errorf("Role ID must match the regexp `^[a-zA-Z0-9_-]+$`")
}
// Use the user provided ID here.
resourceID += roleID
}
roleArn, err := arn.NewIAMRoleARN(resourceID, serverRegion)
if err != nil {
return args, config.Errorf("unable to generate ARN from the plugin config: %v", err)
}
args = Args{
URL: u,
AuthToken: authToken,
Transport: transport,
CloseRespFn: closeRespFn,
RolePolicy: rolePolicy,
RoleARN: roleArn,
}
if err = args.Validate(); err != nil {
return args, err
}
return args, nil
}
// New - initializes Authorization Management Plugin.
func New(args Args) *AuthNPlugin {
if args.URL == nil || args.URL.Scheme == "" && args.AuthToken == "" {
return nil
}
return &AuthNPlugin{
args: args,
client: &http.Client{Transport: args.Transport},
}
}
// AuthNSuccessResponse - represents the response from the authentication plugin
// service.
type AuthNSuccessResponse struct {
User string `json:"user"`
MaxValiditySeconds int `json:"maxValiditySeconds"`
Claims map[string]interface{} `json:"claims"`
}
// AuthNErrorResponse - represents an error response from the authN plugin.
type AuthNErrorResponse struct {
Reason string `json:"reason"`
}
// AuthNResponse - represents a result of the authentication operation.
type AuthNResponse struct {
Success *AuthNSuccessResponse
Failure *AuthNErrorResponse
}
const (
minValidityDurationSeconds int = 900
maxValidityDurationSeconds int = 365 * 24 * 3600
)
// Authenticate authenticates the token with the external hook endpoint and
// returns a parent user, max expiry duration for the authentication and a set
// of claims.
func (o *AuthNPlugin) Authenticate(roleArn arn.ARN, token string) (AuthNResponse, error) {
if o == nil {
return AuthNResponse{}, nil
}
if roleArn != o.args.RoleARN {
return AuthNResponse{}, fmt.Errorf("Invalid role ARN value: %s", roleArn.String())
}
var u url.URL = url.URL(*o.args.URL)
q := u.Query()
q.Set("token", token)
u.RawQuery = q.Encode()
req, err := http.NewRequest(http.MethodPost, u.String(), nil)
if err != nil {
return AuthNResponse{}, err
}
if o.args.AuthToken != "" {
req.Header.Set("Authorization", o.args.AuthToken)
}
resp, err := o.client.Do(req)
if err != nil {
return AuthNResponse{}, err
}
defer o.args.CloseRespFn(resp.Body)
switch resp.StatusCode {
case 200:
var result AuthNSuccessResponse
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
return AuthNResponse{}, err
}
if result.MaxValiditySeconds < minValidityDurationSeconds || result.MaxValiditySeconds > maxValidityDurationSeconds {
return AuthNResponse{}, fmt.Errorf("Plugin returned an invalid validity duration (%d) - should be between %d and %d",
result.MaxValiditySeconds, minValidityDurationSeconds, maxValidityDurationSeconds)
}
return AuthNResponse{
Success: &result,
}, nil
case 403:
var result AuthNErrorResponse
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
return AuthNResponse{}, err
}
return AuthNResponse{
Failure: &result,
}, nil
default:
return AuthNResponse{}, fmt.Errorf("Invalid status code %d from auth plugin", resp.StatusCode)
}
}
// GetRoleInfo - returns ARN to policies map.
func (o *AuthNPlugin) GetRoleInfo() map[arn.ARN]string {
return map[arn.ARN]string{
o.args.RoleARN: o.args.RolePolicy,
}
}
+1 -1
View File
@@ -22,13 +22,13 @@ import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"io"
"path"
"github.com/minio/minio/internal/fips"
"github.com/minio/minio/internal/hash/sha256"
"github.com/minio/minio/internal/logger"
"github.com/minio/sio"
)

Some files were not shown because too many files have changed in this diff Show More