fix(auth): authorize attributes and archive reads with stored tags

Require both Attributes actions on the addressed version and GetObject permission for archive listings. Preserve conditional allows, missing-object masks and retryable storage errors.

Fixes #342

Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-11-11 12:24:21 +08:00
parent 745fbf0437
commit f746d07076
5 changed files with 271 additions and 82 deletions
+71 -68
View File
@@ -28,10 +28,10 @@ import (
"sort"
"strings"
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/crypto"
xhttp "github.com/minio/minio/internal/http"
xioutil "github.com/minio/minio/internal/ioutil"
"github.com/minio/minio/internal/logger"
"github.com/minio/zipindex"
"github.com/pgsty/silo-pkg/v3/policy"
)
@@ -82,35 +82,9 @@ func (api objectAPIHandlers) getObjectInArchiveFileHandler(ctx context.Context,
getObjectInfo := objectAPI.GetObjectInfo
// Check for auth type to return S3 compatible error.
// type to return the correct error (NoSuchKey vs AccessDenied)
if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectAction, bucket, zipPath); s3Error != ErrNone {
if getRequestAuthType(r) == authTypeAnonymous {
// As per "Permission" section in
// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html
// If the object you request does not exist,
// the error Amazon S3 returns depends on
// whether you also have the s3:ListBucket
// permission.
// * If you have the s3:ListBucket permission
// on the bucket, Amazon S3 will return an
// HTTP status code 404 ("no such key")
// error.
// * if you don’t have the s3:ListBucket
// permission, Amazon S3 will return an HTTP
// status code 403 ("access denied") error.`
if globalPolicySys.IsAllowed(policy.BucketPolicyArgs{
Action: policy.ListBucketAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, "", auth.AnonymousCredentials),
IsOwner: false,
}) {
_, err = getObjectInfo(ctx, bucket, zipPath, opts)
if toAPIError(ctx, err).Code == "NoSuchKey" {
s3Error = ErrNoSuchKey
}
}
}
logger.GetReqInfo(ctx).BucketName = bucket
logger.GetReqInfo(ctx).ObjectName = zipPath
if s3Error := authenticateRequest(ctx, r, policy.GetObjectAction); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
return
}
@@ -128,6 +102,10 @@ func (api objectAPIHandlers) getObjectInArchiveFileHandler(ctx context.Context,
// Validate pre-conditions if any.
opts.CheckPrecondFn = func(oi ObjectInfo) bool {
if s3Error := authorizeRequestWithExistingTags(ctx, r, policy.GetObjectAction, oi.UserTags); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
return true
}
if _, err := DecryptObjectInfo(&oi, r); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return true
@@ -138,14 +116,21 @@ func (api objectAPIHandlers) getObjectInArchiveFileHandler(ctx context.Context,
zipObjInfo, err := getObjectInfo(ctx, bucket, zipPath, opts)
if err != nil {
err = archiveReadError(ctx, r, bucket, zipPath, err)
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
if opts.CheckPrecondFn(zipObjInfo) {
return
}
zipInfo := zipObjInfo.ArchiveInfo(r.Header)
if len(zipInfo) == 0 {
zipInfo, _, err = readObjectZIPInfo(ctx, objectAPI, bucket, zipPath, opts)
}
if isErrPreconditionFailed(err) {
return
}
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
@@ -177,6 +162,9 @@ func (api objectAPIHandlers) getObjectInArchiveFileHandler(ctx context.Context,
end := min(file.Offset+int64(file.CompressedSize64)+64<<10, zipObjInfo.Size)
rs := &HTTPRangeSpec{Start: file.Offset, End: end}
gr, err := objectAPI.GetObjectNInfo(ctx, bucket, zipPath, rs, nil, opts)
if isErrPreconditionFailed(err) {
return
}
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
@@ -224,22 +212,40 @@ func (api objectAPIHandlers) getObjectInArchiveFileHandler(ctx context.Context,
}
// listObjectsV2InArchive generates S3 listing result ListObjectsV2Info from zip file, all parameters are already validated by the caller.
func listObjectsV2InArchive(ctx context.Context, objectAPI ObjectLayer, bucket, prefix, token, delimiter string, maxKeys int, startAfter string, h http.Header) (ListObjectsV2Info, error) {
func listObjectsV2InArchive(ctx context.Context, objectAPI ObjectLayer, bucket, prefix, token, delimiter string, maxKeys int, startAfter string, r *http.Request) (ListObjectsV2Info, error) {
zipPath, _, err := splitZipExtensionPath(prefix)
if err != nil {
// Return empty listing
return ListObjectsV2Info{}, nil
}
zipObjInfo, err := objectAPI.GetObjectInfo(ctx, bucket, zipPath, ObjectOptions{})
// The bucket listing permission does not grant access to ZIP contents.
logger.GetReqInfo(ctx).ObjectName = zipPath
opts := ObjectOptions{}
zipObjInfo, err := objectAPI.GetObjectInfo(ctx, bucket, zipPath, opts)
if err != nil {
// Return empty listing
return ListObjectsV2Info{}, nil
if s3Error := authorizeRequestWithUnreadTags(ctx, r, policy.GetObjectAction, err); s3Error != ErrNone {
return ListObjectsV2Info{}, PrefixAccessDenied{Bucket: bucket, Object: zipPath}
}
if isErrObjectNotFound(err) {
return ListObjectsV2Info{}, nil
}
return ListObjectsV2Info{}, err
}
zipInfo := zipObjInfo.ArchiveInfo(h)
if s3Error := authorizeRequestWithExistingTags(ctx, r, policy.GetObjectAction, zipObjInfo.UserTags); s3Error != ErrNone {
return ListObjectsV2Info{}, PrefixAccessDenied{Bucket: bucket, Object: zipPath}
}
var denied bool
opts.CheckPrecondFn = func(oi ObjectInfo) bool {
denied = authorizeRequestWithExistingTags(ctx, r, policy.GetObjectAction, oi.UserTags) != ErrNone
return denied
}
zipInfo := zipObjInfo.ArchiveInfo(r.Header)
if len(zipInfo) == 0 {
zipInfo, _, err = readObjectZIPInfo(ctx, objectAPI, bucket, zipPath, ObjectOptions{})
zipInfo, _, err = readObjectZIPInfo(ctx, objectAPI, bucket, zipPath, opts)
}
if denied {
return ListObjectsV2Info{}, PrefixAccessDenied{Bucket: bucket, Object: zipPath}
}
if err != nil {
return ListObjectsV2Info{}, err
@@ -372,42 +378,19 @@ func (api objectAPIHandlers) headObjectInArchiveFileHandler(ctx context.Context,
return
}
if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectAction, bucket, zipPath); s3Error != ErrNone {
if getRequestAuthType(r) == authTypeAnonymous {
// As per "Permission" section in
// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html
// If the object you request does not exist,
// the error Amazon S3 returns depends on
// whether you also have the s3:ListBucket
// permission.
// * If you have the s3:ListBucket permission
// on the bucket, Amazon S3 will return an
// HTTP status code 404 ("no such key")
// error.
// * if you don’t have the s3:ListBucket
// permission, Amazon S3 will return an HTTP
// status code 403 ("access denied") error.`
if globalPolicySys.IsAllowed(policy.BucketPolicyArgs{
Action: policy.ListBucketAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, "", auth.AnonymousCredentials),
IsOwner: false,
}) {
_, err = getObjectInfo(ctx, bucket, zipPath, opts)
if toAPIError(ctx, err).Code == "NoSuchKey" {
s3Error = ErrNoSuchKey
}
}
}
errCode := errorCodes.ToAPIErr(s3Error)
w.Header().Set(xMinIOErrCodeHeader, errCode.Code)
w.Header().Set(xMinIOErrDescHeader, "\""+errCode.Description+"\"")
writeErrorResponseHeadersOnly(w, errCode)
logger.GetReqInfo(ctx).BucketName = bucket
logger.GetReqInfo(ctx).ObjectName = zipPath
if s3Error := authenticateRequest(ctx, r, policy.GetObjectAction); s3Error != ErrNone {
writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(s3Error))
return
}
// Validate pre-conditions if any.
opts.CheckPrecondFn = func(oi ObjectInfo) bool {
if s3Error := authorizeRequestWithExistingTags(ctx, r, policy.GetObjectAction, oi.UserTags); s3Error != ErrNone {
writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(s3Error))
return true
}
return checkPreconditions(ctx, w, r, oi, opts)
}
@@ -424,14 +407,21 @@ func (api objectAPIHandlers) headObjectInArchiveFileHandler(ctx context.Context,
zipObjInfo, err := getObjectInfo(ctx, bucket, zipPath, opts)
if err != nil {
err = archiveReadError(ctx, r, bucket, zipPath, err)
writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
return
}
if opts.CheckPrecondFn(zipObjInfo) {
return
}
zipInfo := zipObjInfo.ArchiveInfo(r.Header)
if len(zipInfo) == 0 {
zipInfo, _, err = readObjectZIPInfo(ctx, objectAPI, bucket, zipPath, opts)
}
if isErrPreconditionFailed(err) {
return
}
if err != nil {
writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
return
@@ -470,6 +460,19 @@ func (api objectAPIHandlers) headObjectInArchiveFileHandler(ctx context.Context,
w.WriteHeader(http.StatusOK)
}
// archiveReadError preserves the anonymous ListBucket missing-object response.
// All other failures are disclosed only to callers authorized to read the ZIP.
func archiveReadError(ctx context.Context, r *http.Request, bucket, object string, err error) error {
if getRequestAuthType(r) == authTypeAnonymous && isErrObjectNotFound(err) &&
authorizeBucketRequest(ctx, r, policy.ListBucketAction) == ErrNone {
return err
}
if authorizeRequestWithUnreadTags(ctx, r, policy.GetObjectAction, err) != ErrNone {
return PrefixAccessDenied{Bucket: bucket, Object: object}
}
return err
}
// readObjectZIPInfo builds an index without modifying the object. A GET, HEAD
// or archive listing must remain read-only even when no index was stored.
func readObjectZIPInfo(ctx context.Context, objectAPI ObjectLayer, bucket, object string, opts ObjectOptions) ([]byte, ObjectInfo, error) {