mirror of
https://github.com/pgsty/minio.git
synced 2026-11-11 10:05:34 +03:00
296c9710b1
Signed-off-by: Feng Ruohang <rh@vonng.com>
206 lines
7.5 KiB
Go
206 lines
7.5 KiB
Go
// Copyright (c) 2026 Feng Ruohang
|
|
//
|
|
// This file is part of Silo 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 cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/minio/minio/internal/crypto"
|
|
"github.com/minio/minio/internal/hash/sha256"
|
|
xioutil "github.com/minio/minio/internal/ioutil"
|
|
)
|
|
|
|
type replicaContentGuardFn func(context.Context, string, string, ObjectInfo, func() (*GetObjectReader, error)) error
|
|
|
|
// verifyLockedReplica compares the actual data, never a sender's ETag or
|
|
// checksum assertion. A peer may retransmit a protected version to update its
|
|
// metadata, but may not replace its content. Call under the object write lock.
|
|
func verifyLockedReplica(ctx context.Context, incoming, stored ObjectInfo, openIncoming, openStored func() (*GetObjectReader, error)) error {
|
|
if !enforceRetentionForDeletion(ctx, stored) {
|
|
return nil
|
|
}
|
|
locked := ObjectLocked{Bucket: stored.Bucket, Object: stored.Name, VersionID: stored.VersionID}
|
|
if storedObjectLockState(stored.UserDefined).complianceWouldWeaken(storedObjectLockState(incoming.UserDefined)) {
|
|
return locked
|
|
}
|
|
inSSEC, oldSSEC := crypto.SSEC.IsEncrypted(incoming.UserDefined), crypto.SSEC.IsEncrypted(stored.UserDefined)
|
|
if inSSEC != oldSSEC {
|
|
return locked
|
|
}
|
|
if inSSEC {
|
|
inSize, inErr := incoming.GetActualSize()
|
|
oldSize, oldErr := stored.GetActualSize()
|
|
if inErr != nil || oldErr != nil || inSize != oldSize {
|
|
return locked
|
|
}
|
|
// Without the customer key, compare ciphertext and its interpretation.
|
|
// Equal ciphertext with a changed seal or multipart framing is not the
|
|
// same object, even if the supplied ETag and size are unchanged.
|
|
for _, key := range []string{
|
|
crypto.MetaIV, crypto.MetaAlgorithm, crypto.MetaSealedKeySSEC,
|
|
ReservedMetadataPrefix + "compression",
|
|
} {
|
|
if incoming.UserDefined[key] != stored.UserDefined[key] {
|
|
return locked
|
|
}
|
|
}
|
|
if crypto.IsMultiPart(incoming.UserDefined) != crypto.IsMultiPart(stored.UserDefined) || incoming.isMultipart() != stored.isMultipart() {
|
|
return locked
|
|
}
|
|
if crypto.IsMultiPart(incoming.UserDefined) {
|
|
if len(incoming.Parts) != len(stored.Parts) {
|
|
return locked
|
|
}
|
|
for i, p := range incoming.Parts {
|
|
old := stored.Parts[i]
|
|
if p.Number != old.Number || p.Size != old.Size {
|
|
return locked
|
|
}
|
|
}
|
|
}
|
|
}
|
|
in, err := openIncoming()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer in.Close()
|
|
old, err := openStored()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer old.Close()
|
|
ih, oh := sha256.New(), sha256.New()
|
|
inSize, err := io.Copy(ih, in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
oldSize, err := io.Copy(oh, old)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if inSize != oldSize || !bytes.Equal(ih.Sum(nil), oh.Sum(nil)) {
|
|
return locked
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (er erasureObjects) guardReplicaContent(ctx context.Context, bucket, object string, incoming ObjectInfo, openIncoming func() (*GetObjectReader, error)) error {
|
|
opts := ObjectOptions{VersionID: incoming.VersionID, NoLock: true, NoAuditLog: true, ReplicationRequest: true}
|
|
if opts.VersionID == "" {
|
|
opts.VersionID = nullVersionID
|
|
}
|
|
stored, err := er.getObjectInfo(ctx, bucket, object, opts)
|
|
if isErrObjectNotFound(err) || isErrVersionNotFound(err) || isErrMethodNotAllowed(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return verifyLockedReplica(ctx, incoming, stored, openIncoming, func() (*GetObjectReader, error) {
|
|
return er.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, opts)
|
|
})
|
|
}
|
|
|
|
func (z *erasureServerPools) guardReplicaContent(ctx context.Context, bucket, object string, incoming ObjectInfo, openIncoming func() (*GetObjectReader, error)) error {
|
|
opts := ObjectOptions{VersionID: incoming.VersionID, NoLock: true, NoAuditLog: true, ReplicationRequest: true}
|
|
if opts.VersionID == "" {
|
|
opts.VersionID = nullVersionID
|
|
}
|
|
copies, err := z.objectPoolInfos(ctx, bucket, object, opts)
|
|
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, copy := range copies {
|
|
if err := verifyLockedReplica(ctx, incoming, copy.ObjInfo, openIncoming, func() (*GetObjectReader, error) {
|
|
return z.serverPools[copy.Index].GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, opts)
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkReplicaContent reads the staged version before rename commits it. Its
|
|
// data is decoded/decrypted just like a GET (raw ciphertext for SSE-C), so this
|
|
// covers multipart, compression and destination SSE-S3/KMS re-encryption.
|
|
// disks and metadata are in erasure shard order, as passed to renameData.
|
|
func (er erasureObjects) checkReplicaContent(ctx context.Context, bucket, object, stageBucket, stageObject string, metadata []FileInfo, disks []StorageAPI, opts ObjectOptions) error {
|
|
if (!opts.ReplicaLockReconcile && !opts.ReplicationRequest) || opts.DataMovement {
|
|
return nil
|
|
}
|
|
var fi FileInfo
|
|
for i, disk := range disks {
|
|
if disk != nil {
|
|
fi = metadata[i]
|
|
break
|
|
}
|
|
}
|
|
incoming := fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
|
|
guard := opts.replicaContentGuard
|
|
if guard == nil {
|
|
guard = er.guardReplicaContent
|
|
}
|
|
return guard(ctx, bucket, object, incoming, func() (*GetObjectReader, error) {
|
|
// A marker-only peer write does not reconcile the stored protection.
|
|
// It therefore cannot replace a protected version, even with equal data.
|
|
if !opts.ReplicaLockReconcile {
|
|
return nil, ObjectLocked{Bucket: bucket, Object: decodeDirObject(object), VersionID: incoming.VersionID}
|
|
}
|
|
if incoming.IsCompressed() {
|
|
// A replica may assert the source's logical size. A compressed GET
|
|
// limits its output to that size, so verify it against the lengths
|
|
// recorded while consuming the uploaded parts before comparing GETs.
|
|
// Otherwise a forged zero size could hide a non-empty replacement.
|
|
var actual int64
|
|
for _, part := range incoming.Parts {
|
|
actual += part.ActualSize
|
|
}
|
|
declared, err := incoming.GetActualSize()
|
|
if err != nil || declared != actual {
|
|
return nil, ObjectLocked{Bucket: bucket, Object: decodeDirObject(object), VersionID: incoming.VersionID}
|
|
}
|
|
}
|
|
readOpts := ObjectOptions{NoDecryption: crypto.SSEC.IsEncrypted(incoming.UserDefined)}
|
|
fn, off, length, err := NewGetObjectReader(nil, incoming, readOpts, http.Header{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// The ordinary reader expects original drive order, not shard order.
|
|
readDisks, readMeta := make([]StorageAPI, len(disks)), make([]FileInfo, len(metadata))
|
|
for i, shard := range fi.Erasure.Distribution {
|
|
readDisks[i], readMeta[i] = disks[shard-1], metadata[shard-1]
|
|
readMeta[i].Erasure.Index = shard
|
|
}
|
|
pr, pw := xioutil.WaitPipe()
|
|
go func() {
|
|
pw.CloseWithError(er.getObjectWithFileInfo(ctx, stageBucket, stageObject, off, length, pw, fi, readMeta, readDisks))
|
|
}()
|
|
reader, err := fn(pr, http.Header{}, func() { pr.CloseWithError(nil) })
|
|
if err != nil {
|
|
pr.CloseWithError(err)
|
|
}
|
|
return reader, err
|
|
})
|
|
}
|