mirror of
https://github.com/pgsty/minio.git
synced 2026-10-30 12:30:39 +03:00
fix: isolate Snowball replication trust per entry
Evaluate PutObject and ReplicateObject permissions with immutable per-entry request snapshots during concurrent Snowball extraction. Preserve the first API error without sharing mutable handler state, and cover prefix-scoped trust under the race detector. Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
+13
-3
@@ -786,10 +786,20 @@ func isPutActionAllowedWithRequestTags(ctx context.Context, atype authType, buck
|
|||||||
return s3Err
|
return s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.GetReqInfo(ctx).Cred = cred
|
reqInfo := logger.GetReqInfo(ctx)
|
||||||
logger.GetReqInfo(ctx).Owner = owner
|
if reqInfo == nil {
|
||||||
logger.GetReqInfo(ctx).Region = region
|
return ErrAccessDenied
|
||||||
|
}
|
||||||
|
reqInfo.Lock()
|
||||||
|
reqInfo.Cred = cred
|
||||||
|
reqInfo.Owner = owner
|
||||||
|
reqInfo.Region = region
|
||||||
|
reqInfo.Unlock()
|
||||||
|
|
||||||
|
return isPutActionAllowedWithCred(bucketName, objectName, r, action, requestTags, cred, owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPutActionAllowedWithCred(bucketName, objectName string, r *http.Request, action policy.Action, requestTags *string, cred auth.Credentials, owner bool) APIErrorCode {
|
||||||
// Do not check for PutObjectRetentionAction permission,
|
// Do not check for PutObjectRetentionAction permission,
|
||||||
// if mode and retain until date are not set.
|
// if mode and retain until date are not set.
|
||||||
// Can happen when bucket has default lock config set
|
// Can happen when bucket has default lock config set
|
||||||
|
|||||||
+41
-14
@@ -2491,6 +2491,11 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
|
|||||||
sha256hex = getContentSha256Cksum(r, serviceS3)
|
sha256hex = getContentSha256Cksum(r, serviceS3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
entryRequestBase := r.Clone(ctx)
|
||||||
|
// The streaming reader fills r.Trailer while untar writes small entries in
|
||||||
|
// parallel. Entry authorization never consumes trailers, so keep them out
|
||||||
|
// of the immutable request template cloned by those goroutines.
|
||||||
|
entryRequestBase.Trailer = nil
|
||||||
|
|
||||||
hreader, err := hash.NewReader(ctx, reader, size, md5hex, sha256hex, size)
|
hreader, err := hash.NewReader(ctx, reader, size, md5hex, sha256hex, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2515,9 +2520,9 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
|
|||||||
rawReplica := hasReplicaStatus(r.Header)
|
rawReplica := hasReplicaStatus(r.Header)
|
||||||
markerExact := hasReplicationMarker(r.Header)
|
markerExact := hasReplicationMarker(r.Header)
|
||||||
trustedRequestCtx := withReplicationTrust(ctx, true, rawReplica)
|
trustedRequestCtx := withReplicationTrust(ctx, true, rawReplica)
|
||||||
trustedRequest := r.WithContext(trustedRequestCtx)
|
trustedRequest := entryRequestBase.WithContext(trustedRequestCtx)
|
||||||
cleanRequestCtx := withReplicationTrust(ctx, false, false)
|
cleanRequestCtx := withReplicationTrust(ctx, false, false)
|
||||||
cleanRequest := cloneRequestWithoutReplicationHeaders(r, cleanRequestCtx)
|
cleanRequest := cloneRequestWithoutReplicationHeaders(entryRequestBase, cleanRequestCtx)
|
||||||
trustedReqParams := extractReqParams(trustedRequest)
|
trustedReqParams := extractReqParams(trustedRequest)
|
||||||
cleanReqParams := extractReqParams(cleanRequest)
|
cleanReqParams := extractReqParams(cleanRequest)
|
||||||
|
|
||||||
@@ -2533,29 +2538,44 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
|
|||||||
if sc == "" {
|
if sc == "" {
|
||||||
sc = storageclass.STANDARD
|
sc = storageclass.STANDARD
|
||||||
}
|
}
|
||||||
|
reqInfo := logger.GetReqInfo(ctx)
|
||||||
|
if reqInfo == nil {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reqInfo.RLock()
|
||||||
|
tarCred := reqInfo.Cred
|
||||||
|
tarOwner := reqInfo.Owner
|
||||||
|
reqInfo.RUnlock()
|
||||||
|
var tarS3Err atomic.Int32
|
||||||
|
setTarS3Err := func(code APIErrorCode) {
|
||||||
|
tarS3Err.CompareAndSwap(int32(ErrNone), int32(code))
|
||||||
|
}
|
||||||
|
|
||||||
putObjectTar := func(reader io.Reader, info os.FileInfo, object string) error {
|
putObjectTar := func(reader io.Reader, info os.FileInfo, object string) error {
|
||||||
size := info.Size()
|
size := info.Size()
|
||||||
if s3Err = isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.PutObjectAction); s3Err != ErrNone {
|
entryAuthReq := entryRequestBase.Clone(ctx)
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
entryS3Err := isPutActionAllowedWithCred(bucket, object, entryAuthReq, policy.PutObjectAction, nil, tarCred, tarOwner)
|
||||||
return errors.New(errorCodes.ToAPIErr(s3Err).Code)
|
if entryS3Err != ErrNone {
|
||||||
|
setTarS3Err(entryS3Err)
|
||||||
|
return errors.New(errorCodes.ToAPIErr(entryS3Err).Code)
|
||||||
}
|
}
|
||||||
replicationPermitted := false
|
replicationPermitted := false
|
||||||
if rawReplica || markerExact {
|
if tarCred.AccessKey != "" && (rawReplica || markerExact) {
|
||||||
replicationPermitted = replicationPermissionAllowed(ctx, r, bucket, object, policy.ReplicateObjectAction)
|
replicationPermitted = isPutActionAllowedWithCred(bucket, object, entryAuthReq, policy.ReplicateObjectAction, nil, tarCred, tarOwner) == ErrNone
|
||||||
}
|
}
|
||||||
if rawReplica && !replicationPermitted {
|
if rawReplica && !replicationPermitted {
|
||||||
s3Err = ErrAccessDenied
|
setTarS3Err(ErrAccessDenied)
|
||||||
return errors.New(errorCodes.ToAPIErr(s3Err).Code)
|
return errors.New(errorCodes.ToAPIErr(ErrAccessDenied).Code)
|
||||||
}
|
}
|
||||||
entryTrusted := markerExact && replicationPermitted
|
entryTrusted := markerExact && replicationPermitted
|
||||||
replicaTrusted := entryTrusted && rawReplica
|
replicaTrusted := entryTrusted && rawReplica
|
||||||
entryCtx := cleanRequestCtx
|
entryCtx := cleanRequestCtx
|
||||||
entryReq := cleanRequest
|
entryReq := cloneRequestWithoutReplicationHeaders(entryAuthReq, cleanRequestCtx)
|
||||||
reqParams := cleanReqParams
|
reqParams := cleanReqParams
|
||||||
if entryTrusted {
|
if entryTrusted {
|
||||||
entryCtx = trustedRequestCtx
|
entryCtx = trustedRequestCtx
|
||||||
entryReq = trustedRequest
|
entryReq = entryAuthReq.WithContext(trustedRequestCtx)
|
||||||
reqParams = trustedReqParams
|
reqParams = trustedReqParams
|
||||||
}
|
}
|
||||||
metadata := map[string]string{
|
metadata := map[string]string{
|
||||||
@@ -2592,7 +2612,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
|
|||||||
pReader := NewPutObjReader(rawReader)
|
pReader := NewPutObjReader(rawReader)
|
||||||
|
|
||||||
if replicaTrusted {
|
if replicaTrusted {
|
||||||
if err = extractReplicationMetadataFromMime(entryCtx, textproto.MIMEHeader(entryReq.Header), metadata); err != nil {
|
if err := extractReplicationMetadataFromMime(entryCtx, textproto.MIMEHeader(entryReq.Header), metadata); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
metadata[xhttp.AmzBucketReplicationStatus] = replication.Replica.String()
|
metadata[xhttp.AmzBucketReplicationStatus] = replication.Replica.String()
|
||||||
@@ -2657,7 +2677,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s3err != ErrNone {
|
if s3err != ErrNone {
|
||||||
s3Err = s3err
|
setTarS3Err(s3err)
|
||||||
return ObjectLocked{}
|
return ObjectLocked{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2746,7 +2766,14 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = untar(ctx, hreader, putObjectTar, opts); err != nil {
|
err = untar(ctx, hreader, putObjectTar, opts)
|
||||||
|
if code := APIErrorCode(tarS3Err.Load()); code != ErrNone {
|
||||||
|
s3Err = code
|
||||||
|
if err == nil {
|
||||||
|
err = errors.New(errorCodes.ToAPIErr(code).Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
apiErr := errorCodes.ToAPIErr(s3Err)
|
apiErr := errorCodes.ToAPIErr(s3Err)
|
||||||
// If not set, convert or use BadRequest
|
// If not set, convert or use BadRequest
|
||||||
if s3Err == ErrNone {
|
if s3Err == ErrNone {
|
||||||
|
|||||||
@@ -106,11 +106,9 @@ func hasReplicationRequestHeaders(h http.Header) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cloneRequestWithoutReplicationHeaders(r *http.Request, ctx context.Context) *http.Request {
|
func cloneRequestWithoutReplicationHeaders(r *http.Request, ctx context.Context) *http.Request {
|
||||||
clone := new(http.Request)
|
clone := r.Clone(ctx)
|
||||||
*clone = *r
|
|
||||||
clone.Header = r.Header.Clone()
|
|
||||||
stripReplicationRequestHeaders(clone.Header)
|
stripReplicationRequestHeaders(clone.Header)
|
||||||
return clone.WithContext(ctx)
|
return clone
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyReplicationTrust binds the handler context to the effective request.
|
// applyReplicationTrust binds the handler context to the effective request.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
@@ -21,11 +22,14 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/madmin-go/v3"
|
||||||
"github.com/minio/minio/internal/auth"
|
"github.com/minio/minio/internal/auth"
|
||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
|
"github.com/minio/pkg/v3/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAPIReplicationTrustProtectsSSECReads(t *testing.T) {
|
func TestAPIReplicationTrustProtectsSSECReads(t *testing.T) {
|
||||||
@@ -272,6 +276,153 @@ func testAPIPutObjectReplicationTrust(obj ObjectLayer, instanceType, bucketName
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPISnowballReplicationTrustIsPerEntry(t *testing.T) {
|
||||||
|
defer DetectTestLeak(t)()
|
||||||
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
|
||||||
|
t: t,
|
||||||
|
objAPITest: testAPISnowballReplicationTrustIsPerEntry,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAPISnowballReplicationTrustIsPerEntry(obj ObjectLayer, instanceType, bucketName string,
|
||||||
|
apiRouter http.Handler, _ auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
|
const (
|
||||||
|
allowedPrefix = "snowball/allowed/"
|
||||||
|
deniedPrefix = "snowball/denied/"
|
||||||
|
sourceETag = "0123456789abcdef0123456789abcdef"
|
||||||
|
)
|
||||||
|
creds := newSnowballReplicationTrustUser(t, instanceType, bucketName, allowedPrefix)
|
||||||
|
|
||||||
|
var body bytes.Buffer
|
||||||
|
tw := tar.NewWriter(&body)
|
||||||
|
objects := make([]struct {
|
||||||
|
name string
|
||||||
|
trusted bool
|
||||||
|
}, 0, 32)
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
for _, entry := range []struct {
|
||||||
|
prefix string
|
||||||
|
trusted bool
|
||||||
|
}{
|
||||||
|
{prefix: allowedPrefix, trusted: true},
|
||||||
|
{prefix: deniedPrefix},
|
||||||
|
} {
|
||||||
|
name := entry.prefix + strconv.Itoa(i)
|
||||||
|
data := []byte("snowball replication trust " + name)
|
||||||
|
if err := tw.WriteHeader(&tar.Header{Name: name, Mode: 0o600, Size: int64(len(data))}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := tw.Write(data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
objects = append(objects, struct {
|
||||||
|
name string
|
||||||
|
trusted bool
|
||||||
|
}{name: name, trusted: entry.trusted})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tw.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := map[string]string{
|
||||||
|
xhttp.AmzSnowballExtract: "true",
|
||||||
|
xhttp.MinIOSourceReplicationRequest: "true",
|
||||||
|
xhttp.MinIOSourceETag: sourceETag,
|
||||||
|
}
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
trailer bool
|
||||||
|
}{
|
||||||
|
{name: "signed-v4"},
|
||||||
|
{name: "streaming-unsigned-trailer", trailer: true},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
var req *http.Request
|
||||||
|
var err error
|
||||||
|
if test.trailer {
|
||||||
|
req, err = newStreamingUnsignedTrailerRequest(http.MethodPut,
|
||||||
|
getPutObjectURL("", bucketName, "snowball.tar"), body.Bytes(), UTCNow())
|
||||||
|
if err == nil {
|
||||||
|
for name, value := range headers {
|
||||||
|
req.Header.Set(name, value)
|
||||||
|
}
|
||||||
|
err = signRequestV4(req, creds.AccessKey, creds.SecretKey)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
req, err = newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, "snowball.tar"),
|
||||||
|
int64(body.Len()), bytes.NewReader(body.Bytes()), creds.AccessKey, creds.SecretKey, headers)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
apiRouter.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("%s: Snowball PUT status %d: %s", instanceType, rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, object := range objects {
|
||||||
|
info, err := obj.GetObjectInfo(t.Context(), bucketName, object.name, ObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: get %s: %v", instanceType, object.name, err)
|
||||||
|
}
|
||||||
|
if object.trusted && info.ETag != sourceETag {
|
||||||
|
t.Errorf("%s: trusted entry %s ETag = %q, want source ETag", instanceType, object.name, info.ETag)
|
||||||
|
}
|
||||||
|
if !object.trusted && info.ETag == sourceETag {
|
||||||
|
t.Errorf("%s: untrusted entry %s preserved source ETag", instanceType, object.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSnowballReplicationTrustUser(t *testing.T, instanceType, bucketName, allowedPrefix string) auth.Credentials {
|
||||||
|
t.Helper()
|
||||||
|
ctx := t.Context()
|
||||||
|
accessKey, secretKey, err := auth.GenerateCredentials()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: generate credentials: %v", instanceType, err)
|
||||||
|
}
|
||||||
|
creds := auth.Credentials{AccessKey: accessKey, SecretKey: secretKey}
|
||||||
|
if _, err = globalIAMSys.CreateUser(ctx, creds.AccessKey, madmin.AddOrUpdateUserReq{
|
||||||
|
SecretKey: creds.SecretKey,
|
||||||
|
Status: madmin.AccountEnabled,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("%s: create Snowball user: %v", instanceType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
policyJSON := `{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": ["s3:PutObject"],
|
||||||
|
"Resource": ["arn:aws:s3:::` + bucketName + `/*"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": ["s3:ReplicateObject"],
|
||||||
|
"Resource": ["arn:aws:s3:::` + bucketName + `/` + allowedPrefix + `*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
parsed, err := policy.ParseConfig(strings.NewReader(policyJSON))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: parse Snowball policy: %v", instanceType, err)
|
||||||
|
}
|
||||||
|
policyName := "snowball-replication-trust-" + mustGetUUID()
|
||||||
|
if _, err = globalIAMSys.SetPolicy(ctx, policyName, *parsed); err != nil {
|
||||||
|
t.Fatalf("%s: install Snowball policy: %v", instanceType, err)
|
||||||
|
}
|
||||||
|
if _, err = globalIAMSys.PolicyDBSet(ctx, creds.AccessKey, policyName, regUser, false); err != nil {
|
||||||
|
t.Fatalf("%s: attach Snowball policy: %v", instanceType, err)
|
||||||
|
}
|
||||||
|
return creds
|
||||||
|
}
|
||||||
|
|
||||||
func TestAPICopyObjectMarkerOnlyDoesNotCopyCiphertext(t *testing.T) {
|
func TestAPICopyObjectMarkerOnlyDoesNotCopyCiphertext(t *testing.T) {
|
||||||
defer DetectTestLeak(t)()
|
defer DetectTestLeak(t)()
|
||||||
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
|
||||||
|
|||||||
Reference in New Issue
Block a user