Add ObjectTagging Support (#8754)

This PR adds support for AWS S3 ObjectTagging API as explained here
https://docs.aws.amazon.com/AmazonS3/latest/dev/object-tagging.html
This commit is contained in:
Nitish Tiwari 2020-01-20 22:15:59 +05:30 committed by kannappanr
parent dd93eee1e3
commit 61c17c8933
26 changed files with 887 additions and 87 deletions

View File

@ -36,6 +36,7 @@ import (
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/objectlock" "github.com/minio/minio/pkg/objectlock"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/tagging"
) )
// APIError structure // APIError structure
@ -150,6 +151,7 @@ const (
ErrPastObjectLockRetainDate ErrPastObjectLockRetainDate
ErrUnknownWORMModeDirective ErrUnknownWORMModeDirective
ErrObjectLockInvalidHeaders ErrObjectLockInvalidHeaders
ErrInvalidTagDirective
// Add new error codes here. // Add new error codes here.
// SSE-S3 related API errors // SSE-S3 related API errors
@ -830,6 +832,11 @@ var errorCodes = errorCodeMap{
Description: "Your metadata headers exceed the maximum allowed metadata size.", Description: "Your metadata headers exceed the maximum allowed metadata size.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrInvalidTagDirective: {
Code: "InvalidArgument",
Description: "Unknown tag directive.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidEncryptionMethod: { ErrInvalidEncryptionMethod: {
Code: "InvalidRequest", Code: "InvalidRequest",
Description: "The encryption method specified is not supported", Description: "The encryption method specified is not supported",
@ -1780,6 +1787,12 @@ func toAPIError(ctx context.Context, err error) APIError {
// their internal error types. This code is only // their internal error types. This code is only
// useful with gateway implementations. // useful with gateway implementations.
switch e := err.(type) { switch e := err.(type) {
case tagging.Error:
apiErr = APIError{
Code: "InvalidTag",
Description: e.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case policy.Error: case policy.Error:
apiErr = APIError{ apiErr = APIError{
Code: "MalformedPolicy", Code: "MalformedPolicy",

View File

@ -22,6 +22,7 @@ import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"time" "time"
@ -95,6 +96,14 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
w.Header().Set(xhttp.XCache, objInfo.CacheStatus.String()) w.Header().Set(xhttp.XCache, objInfo.CacheStatus.String())
w.Header().Set(xhttp.XCacheLookup, objInfo.CacheLookupStatus.String()) w.Header().Set(xhttp.XCacheLookup, objInfo.CacheLookupStatus.String())
} }
// Set tag count if object has tags
tags, _ := url.ParseQuery(objInfo.UserTags)
tagCount := len(tags)
if tagCount != 0 {
w.Header().Set(xhttp.AmzTagCount, strconv.Itoa(tagCount))
}
// Set all other user defined metadata. // Set all other user defined metadata.
for k, v := range objInfo.UserDefined { for k, v := range objInfo.UserDefined {
if HasPrefix(k, ReservedMetadataPrefix) { if HasPrefix(k, ReservedMetadataPrefix) {

View File

@ -105,8 +105,12 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool)
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(collectAPIStats("abortmultipartupload", httpTraceAll(api.AbortMultipartUploadHandler))).Queries("uploadId", "{uploadId:.*}") bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(collectAPIStats("abortmultipartupload", httpTraceAll(api.AbortMultipartUploadHandler))).Queries("uploadId", "{uploadId:.*}")
// GetObjectACL - this is a dummy call. // GetObjectACL - this is a dummy call.
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjectacl", httpTraceHdrs(api.GetObjectACLHandler))).Queries("acl", "") bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjectacl", httpTraceHdrs(api.GetObjectACLHandler))).Queries("acl", "")
// GetObjectTagging - this is a dummy call. // GetObjectTagging
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjecttagging", httpTraceHdrs(api.GetObjectTaggingHandler))).Queries("tagging", "") bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjecttagging", httpTraceHdrs(api.GetObjectTaggingHandler))).Queries("tagging", "")
// PutObjectTagging
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobjecttagging", httpTraceHdrs(api.PutObjectTaggingHandler))).Queries("tagging", "")
// DeleteObjectTagging
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(collectAPIStats("deleteobjecttagging", httpTraceHdrs(api.DeleteObjectTaggingHandler))).Queries("tagging", "")
// SelectObjectContent // SelectObjectContent
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(collectAPIStats("selectobjectcontent", httpTraceHdrs(api.SelectObjectContentHandler))).Queries("select", "").Queries("select-type", "2") bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(collectAPIStats("selectobjectcontent", httpTraceHdrs(api.SelectObjectContentHandler))).Queries("select", "").Queries("select-type", "2")
// GetObjectRetention // GetObjectRetention

View File

@ -22,26 +22,13 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/tagging"
) )
// Data types used for returning dummy tagging XML. // Data types used for returning dummy tagging XML.
// These variables shouldn't be used elsewhere. // These variables shouldn't be used elsewhere.
// They are only defined to be used in this file alone. // They are only defined to be used in this file alone.
type tagging struct {
XMLName xml.Name `xml:"Tagging"`
TagSet tagSet `xml:"TagSet"`
}
type tagSet struct {
Tag []tagElem `xml:"Tag"`
}
type tagElem struct {
Key string `xml:"Key"`
Value string `xml:"Value"`
}
// GetBucketWebsite - GET bucket website, a dummy api // GetBucketWebsite - GET bucket website, a dummy api
func (api objectAPIHandlers) GetBucketWebsiteHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) GetBucketWebsiteHandler(w http.ResponseWriter, r *http.Request) {
writeSuccessResponseHeadersOnly(w) writeSuccessResponseHeadersOnly(w)
@ -171,47 +158,8 @@ func (api objectAPIHandlers) GetBucketTaggingHandler(w http.ResponseWriter, r *h
return return
} }
tags := &tagging{} tags := &tagging.Tagging{}
tags.TagSet.Tag = append(tags.TagSet.Tag, tagElem{}) tags.TagSet.Tags = append(tags.TagSet.Tags, tagging.Tag{})
if err := xml.NewEncoder(w).Encode(tags); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
w.(http.Flusher).Flush()
}
// GetObjectTaggingHandler - GET object tagging, a dummy api
func (api objectAPIHandlers) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "GetObjectTagging")
vars := mux.Vars(r)
bucket := vars["bucket"]
object := vars["object"]
objAPI := api.ObjectAPI()
if objAPI == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
return
}
// Allow getObjectTagging if policy action is set, since this is a dummy call
// we are simply re-purposing the bucketPolicyAction.
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
// Validate if object exists, before proceeding further...
_, err := objAPI.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
tags := &tagging{}
tags.TagSet.Tag = append(tags.TagSet.Tag, tagElem{})
if err := xml.NewEncoder(w).Encode(tags); err != nil { if err := xml.NewEncoder(w).Encode(tags); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))

View File

@ -182,9 +182,14 @@ func (m fsMetaV1) ToObjectInfo(bucket, object string, fi os.FileInfo) ObjectInfo
objInfo.Expires = t.UTC() objInfo.Expires = t.UTC()
} }
} }
// Add user tags to the object info
objInfo.UserTags = m.Meta[xhttp.AmzObjectTagging]
// etag/md5Sum has already been extracted. We need to // etag/md5Sum has already been extracted. We need to
// remove to avoid it from appearing as part of // remove to avoid it from appearing as part of
// response headers. e.g, X-Minio-* or X-Amz-*. // response headers. e.g, X-Minio-* or X-Amz-*.
// Tags have also been extracted, we remove that as well.
objInfo.UserDefined = cleanMetadata(m.Meta) objInfo.UserDefined = cleanMetadata(m.Meta)
// All the parts per object. // All the parts per object.

View File

@ -37,6 +37,7 @@ import (
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/minio/minio-go/v6/pkg/s3utils" "github.com/minio/minio-go/v6/pkg/s3utils"
"github.com/minio/minio/cmd/config" "github.com/minio/minio/cmd/config"
xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/lifecycle" "github.com/minio/minio/pkg/lifecycle"
"github.com/minio/minio/pkg/lock" "github.com/minio/minio/pkg/lock"
@ -44,6 +45,7 @@ import (
"github.com/minio/minio/pkg/mimedb" "github.com/minio/minio/pkg/mimedb"
"github.com/minio/minio/pkg/mountinfo" "github.com/minio/minio/pkg/mountinfo"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/tagging"
) )
// Default etag is used for pre-existing objects. // Default etag is used for pre-existing objects.
@ -1235,6 +1237,58 @@ func (fs *FSObjects) ListObjects(ctx context.Context, bucket, prefix, marker, de
fs.listDirFactory(), fs.getObjectInfo, fs.getObjectInfo) fs.listDirFactory(), fs.getObjectInfo, fs.getObjectInfo)
} }
// GetObjectTag - get object tags from an existing object
func (fs *FSObjects) GetObjectTag(ctx context.Context, bucket, object string) (tagging.Tagging, error) {
oi, err := fs.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
if err != nil {
return tagging.Tagging{}, err
}
tags, err := tagging.FromString(oi.UserTags)
if err != nil {
return tagging.Tagging{}, err
}
return tags, nil
}
// PutObjectTag - replace or add tags to an existing object
func (fs *FSObjects) PutObjectTag(ctx context.Context, bucket, object string, tags string) error {
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, bucket, object, fs.metaJSONFile)
fsMeta := fsMetaV1{}
wlk, err := fs.rwPool.Write(fsMetaPath)
if err != nil {
logger.LogIf(ctx, err)
return toObjectErr(err, bucket, object)
}
// This close will allow for locks to be synchronized on `fs.json`.
defer wlk.Close()
// Read objects' metadata in `fs.json`.
if _, err = fsMeta.ReadFrom(ctx, wlk); err != nil {
// For any error to read fsMeta, set default ETag and proceed.
fsMeta = fs.defaultFsJSON(object)
}
// clean fsMeta.Meta of tag key, before updating the new tags
delete(fsMeta.Meta, xhttp.AmzObjectTagging)
// Do not update for empty tags
if tags != "" {
fsMeta.Meta[xhttp.AmzObjectTagging] = tags
}
if _, err = fsMeta.WriteTo(wlk); err != nil {
return toObjectErr(err, bucket, object)
}
return nil
}
// DeleteObjectTag - delete object tags from an existing object
func (fs *FSObjects) DeleteObjectTag(ctx context.Context, bucket, object string) error {
return fs.PutObjectTag(ctx, bucket, object, "")
}
// ReloadFormat - no-op for fs, Valid only for XL. // ReloadFormat - no-op for fs, Valid only for XL.
func (fs *FSObjects) ReloadFormat(ctx context.Context, dryRun bool) error { func (fs *FSObjects) ReloadFormat(ctx context.Context, dryRun bool) error {
logger.LogIf(ctx, NotImplemented{}) logger.LogIf(ctx, NotImplemented{})

View File

@ -24,6 +24,7 @@ import (
"github.com/minio/minio/pkg/lifecycle" "github.com/minio/minio/pkg/lifecycle"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/tagging"
) )
// GatewayLocker implements custom NeNSLock implementation // GatewayLocker implements custom NeNSLock implementation
@ -173,6 +174,24 @@ func (a GatewayUnsupported) GetMetrics(ctx context.Context) (*Metrics, error) {
return &Metrics{}, NotImplemented{} return &Metrics{}, NotImplemented{}
} }
// PutObjectTag - not implemented.
func (a GatewayUnsupported) PutObjectTag(ctx context.Context, bucket, object string, tags string) error {
logger.LogIf(ctx, NotImplemented{})
return NotImplemented{}
}
// GetObjectTag - not implemented.
func (a GatewayUnsupported) GetObjectTag(ctx context.Context, bucket, object string) (tagging.Tagging, error) {
logger.LogIf(ctx, NotImplemented{})
return tagging.Tagging{}, NotImplemented{}
}
// DeleteObjectTag - not implemented.
func (a GatewayUnsupported) DeleteObjectTag(ctx context.Context, bucket, object string) error {
logger.LogIf(ctx, NotImplemented{})
return NotImplemented{}
}
// IsNotificationSupported returns whether bucket notification is applicable for this layer. // IsNotificationSupported returns whether bucket notification is applicable for this layer.
func (a GatewayUnsupported) IsNotificationSupported() bool { func (a GatewayUnsupported) IsNotificationSupported() bool {
return false return false

View File

@ -467,8 +467,8 @@ func ignoreNotImplementedBucketResources(req *http.Request) bool {
// Checks requests for not implemented Object resources // Checks requests for not implemented Object resources
func ignoreNotImplementedObjectResources(req *http.Request) bool { func ignoreNotImplementedObjectResources(req *http.Request) bool {
for name := range req.URL.Query() { for name := range req.URL.Query() {
// Enable GetObjectACL and GetObjectTagging dummy calls specifically. // Enable GetObjectACL dummy call specifically.
if (name == "acl" || name == "tagging") && req.Method == http.MethodGet { if name == "acl" && req.Method == http.MethodGet {
return false return false
} }
if notimplementedObjectResourceNames[name] { if notimplementedObjectResourceNames[name] {
@ -497,7 +497,6 @@ var notimplementedBucketResourceNames = map[string]bool{
var notimplementedObjectResourceNames = map[string]bool{ var notimplementedObjectResourceNames = map[string]bool{
"acl": true, "acl": true,
"restore": true, "restore": true,
"tagging": true,
"torrent": true, "torrent": true,
} }

View File

@ -34,6 +34,12 @@ import (
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/handlers" "github.com/minio/minio/pkg/handlers"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/tagging"
)
const (
copyDirective = "COPY"
replaceDirective = "REPLACE"
) )
// Parses location constraint from the incoming reader. // Parses location constraint from the incoming reader.
@ -69,30 +75,27 @@ var supportedHeaders = []string{
"content-encoding", "content-encoding",
"content-disposition", "content-disposition",
xhttp.AmzStorageClass, xhttp.AmzStorageClass,
xhttp.AmzObjectTagging,
"expires", "expires",
// Add more supported headers here. // Add more supported headers here.
} }
// isMetadataDirectiveValid - check if metadata-directive is valid. // isDirectiveValid - check if tagging-directive is valid.
func isMetadataDirectiveValid(h http.Header) bool { func isDirectiveValid(v string) bool {
_, ok := h[http.CanonicalHeaderKey(xhttp.AmzMetadataDirective)] // Check if set metadata-directive is valid.
if ok { return isDirectiveCopy(v) || isDirectiveReplace(v)
// Check atleast set metadata-directive is valid. }
return (isMetadataCopy(h) || isMetadataReplace(h))
} // Check if the directive COPY is requested.
// By default if x-amz-metadata-directive is not we func isDirectiveCopy(value string) bool {
// By default if directive is not set we
// treat it as 'COPY' this function returns true. // treat it as 'COPY' this function returns true.
return true return value == copyDirective || value == ""
} }
// Check if the metadata COPY is requested. // Check if the directive REPLACE is requested.
func isMetadataCopy(h http.Header) bool { func isDirectiveReplace(value string) bool {
return h.Get(xhttp.AmzMetadataDirective) == "COPY" return value == replaceDirective
}
// Check if the metadata REPLACE is requested.
func isMetadataReplace(h http.Header) bool {
return h.Get(xhttp.AmzMetadataDirective) == "REPLACE"
} }
// Splits an incoming path into bucket and object components. // Splits an incoming path into bucket and object components.
@ -174,6 +177,26 @@ func extractMetadataFromMap(ctx context.Context, v map[string][]string, m map[st
return nil return nil
} }
// extractTags extracts tag key and value from given http header. It then
// - Parses the input format X-Amz-Tagging:"Key1=Value1&Key2=Value2" into a map[string]string
// with entries in the format X-Amg-Tag-Key1:Value1, X-Amz-Tag-Key2:Value2
// - Validates the tags
// - Returns the Tag in original string format "Key1=Value1&Key2=Value2"
func extractTags(ctx context.Context, tags string) (string, error) {
// Check if the metadata has tagging related header
if tags != "" {
tagging, err := tagging.FromString(tags)
if err != nil {
return "", err
}
if err := tagging.Validate(); err != nil {
return "", err
}
return tagging.String(), nil
}
return "", nil
}
// The Query string for the redirect URL the client is // The Query string for the redirect URL the client is
// redirected on successful upload. // redirected on successful upload.
func getRedirectPostRawQuery(objInfo ObjectInfo) string { func getRedirectPostRawQuery(objInfo ObjectInfo) string {

View File

@ -56,6 +56,11 @@ const (
// S3 storage class // S3 storage class
AmzStorageClass = "x-amz-storage-class" AmzStorageClass = "x-amz-storage-class"
// S3 object tagging
AmzObjectTagging = "X-Amz-Tagging"
AmzTagCount = "X-Amz-Tag-Count"
AmzTagDirective = "X-Amz-Tagging-Directive"
// S3 extensions // S3 extensions
AmzCopySourceIfModifiedSince = "x-amz-copy-source-if-modified-since" AmzCopySourceIfModifiedSince = "x-amz-copy-source-if-modified-since"
AmzCopySourceIfUnmodifiedSince = "x-amz-copy-source-if-unmodified-since" AmzCopySourceIfUnmodifiedSince = "x-amz-copy-source-if-unmodified-since"

View File

@ -154,6 +154,9 @@ type ObjectInfo struct {
// User-Defined metadata // User-Defined metadata
UserDefined map[string]string UserDefined map[string]string
// User-Defined object tags
UserTags string
// List of individual parts, maximum size of upto 10,000 // List of individual parts, maximum size of upto 10,000
Parts []ObjectPartInfo `json:"-"` Parts []ObjectPartInfo `json:"-"`

View File

@ -25,6 +25,7 @@ import (
"github.com/minio/minio/pkg/lifecycle" "github.com/minio/minio/pkg/lifecycle"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/tagging"
) )
// CheckCopyPreconditionFn returns true if copy precondition check failed. // CheckCopyPreconditionFn returns true if copy precondition check failed.
@ -125,4 +126,9 @@ type ObjectLayer interface {
// Check Readiness // Check Readiness
IsReady(ctx context.Context) bool IsReady(ctx context.Context) bool
// ObjectTagging operations
PutObjectTag(context.Context, string, string, string) error
GetObjectTag(context.Context, string, string) (tagging.Tagging, error)
DeleteObjectTag(context.Context, string, string) error
} }

View File

@ -239,8 +239,8 @@ func getCompleteMultipartMD5(parts []CompletePart) string {
func cleanMetadata(metadata map[string]string) map[string]string { func cleanMetadata(metadata map[string]string) map[string]string {
// Remove STANDARD StorageClass // Remove STANDARD StorageClass
metadata = removeStandardStorageClass(metadata) metadata = removeStandardStorageClass(metadata)
// Clean meta etag keys 'md5Sum', 'etag', "expires". // Clean meta etag keys 'md5Sum', 'etag', "expires", "x-amz-tagging".
return cleanMetadataKeys(metadata, "md5Sum", "etag", "expires") return cleanMetadataKeys(metadata, "md5Sum", "etag", "expires", xhttp.AmzObjectTagging)
} }
// Filter X-Amz-Storage-Class field only if it is set to STANDARD. // Filter X-Amz-Storage-Class field only if it is set to STANDARD.

View File

@ -50,6 +50,7 @@ import (
"github.com/minio/minio/pkg/ioutil" "github.com/minio/minio/pkg/ioutil"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/s3select" "github.com/minio/minio/pkg/s3select"
"github.com/minio/minio/pkg/tagging"
sha256 "github.com/minio/sha256-simd" sha256 "github.com/minio/sha256-simd"
"github.com/minio/sio" "github.com/minio/sio"
) )
@ -604,13 +605,13 @@ func getCpObjMetadataFromHeader(ctx context.Context, r *http.Request, userMeta m
// if x-amz-metadata-directive says REPLACE then // if x-amz-metadata-directive says REPLACE then
// we extract metadata from the input headers. // we extract metadata from the input headers.
if isMetadataReplace(r.Header) { if isDirectiveReplace(r.Header.Get(xhttp.AmzMetadataDirective)) {
return extractMetadata(ctx, r) return extractMetadata(ctx, r)
} }
// if x-amz-metadata-directive says COPY then we // if x-amz-metadata-directive says COPY then we
// return the default metadata. // return the default metadata.
if isMetadataCopy(r.Header) { if isDirectiveCopy(r.Header.Get(xhttp.AmzMetadataDirective)) {
return defaultMeta, nil return defaultMeta, nil
} }
@ -618,6 +619,24 @@ func getCpObjMetadataFromHeader(ctx context.Context, r *http.Request, userMeta m
return defaultMeta, nil return defaultMeta, nil
} }
// Extract tags relevant for an CopyObject operation based on conditional
// header values specified in X-Amz-Tagging-Directive.
func getCpObjTagsFromHeader(ctx context.Context, r *http.Request, tags string) (string, error) {
// if x-amz-tagging-directive says REPLACE then
// we extract tags from the input headers.
if isDirectiveReplace(r.Header.Get(xhttp.AmzTagDirective)) {
if tags := r.Header.Get(xhttp.AmzObjectTagging); tags != "" {
return extractTags(ctx, tags)
}
// Copy is default behavior if x-amz-tagging-directive is set, but x-amz-tagging is
// is not set
return tags, nil
}
// Copy is default behavior if x-amz-tagging-directive is not set.
return tags, nil
}
// Returns a minio-go Client configured to access remote host described by destDNSRecord // Returns a minio-go Client configured to access remote host described by destDNSRecord
// Applicable only in a federated deployment // Applicable only in a federated deployment
var getRemoteInstanceClient = func(r *http.Request, host string) (*miniogo.Core, error) { var getRemoteInstanceClient = func(r *http.Request, host string) (*miniogo.Core, error) {
@ -737,11 +756,17 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
} }
// Check if metadata directive is valid. // Check if metadata directive is valid.
if !isMetadataDirectiveValid(r.Header) { if !isDirectiveValid(r.Header.Get(xhttp.AmzMetadataDirective)) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidMetadataDirective), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidMetadataDirective), r.URL, guessIsBrowserReq(r))
return return
} }
// check if tag directive is valid
if !isDirectiveValid(r.Header.Get(xhttp.AmzTagDirective)) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidTagDirective), r.URL, guessIsBrowserReq(r))
return
}
// This request header needs to be set prior to setting ObjectOptions // This request header needs to be set prior to setting ObjectOptions
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) { if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256) r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
@ -795,7 +820,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
defer gr.Close() defer gr.Close()
srcInfo := gr.ObjInfo srcInfo := gr.ObjInfo
/// maximum Upload size for object in a single CopyObject operation. // maximum Upload size for object in a single CopyObject operation.
if isMaxObjectSize(srcInfo.Size) { if isMaxObjectSize(srcInfo.Size) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL, guessIsBrowserReq(r))
return return
@ -968,6 +993,17 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
tags, err := getCpObjTagsFromHeader(ctx, r, srcInfo.UserTags)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
if tags != "" {
srcInfo.UserDefined[xhttp.AmzObjectTagging] = tags
}
getObjectInfo := objectAPI.GetObjectInfo getObjectInfo := objectAPI.GetObjectInfo
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
getObjectInfo = api.CacheAPI().GetObjectInfo getObjectInfo = api.CacheAPI().GetObjectInfo
@ -1002,12 +1038,13 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
// Ensure that metadata does not contain sensitive information // Ensure that metadata does not contain sensitive information
crypto.RemoveSensitiveEntries(srcInfo.UserDefined) crypto.RemoveSensitiveEntries(srcInfo.UserDefined)
// Check if x-amz-metadata-directive was not set to REPLACE and source, // Check if x-amz-metadata-directive or x-amz-tagging-directive was not set to REPLACE and source,
// desination are same objects. Apply this restriction also when // destination are same objects. Apply this restriction also when
// metadataOnly is true indicating that we are not overwriting the object. // metadataOnly is true indicating that we are not overwriting the object.
// if encryption is enabled we do not need explicit "REPLACE" metadata to // if encryption is enabled we do not need explicit "REPLACE" metadata to
// be enabled as well - this is to allow for key-rotation. // be enabled as well - this is to allow for key-rotation.
if !isMetadataReplace(r.Header) && srcInfo.metadataOnly && !crypto.IsEncrypted(srcInfo.UserDefined) { if !isDirectiveReplace(r.Header.Get(xhttp.AmzMetadataDirective)) && !isDirectiveReplace(r.Header.Get(xhttp.AmzTagDirective)) &&
srcInfo.metadataOnly && !crypto.IsEncrypted(srcInfo.UserDefined) {
// If x-amz-metadata-directive is not set to REPLACE then we need // If x-amz-metadata-directive is not set to REPLACE then we need
// to error out if source and destination are same. // to error out if source and destination are same.
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidCopyDest), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidCopyDest), r.URL, guessIsBrowserReq(r))
@ -1155,6 +1192,14 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
return return
} }
if tags := r.Header.Get(http.CanonicalHeaderKey(xhttp.AmzObjectTagging)); tags != "" {
metadata[xhttp.AmzObjectTagging], err = extractTags(ctx, tags)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
}
if rAuthType == authTypeStreamingSigned { if rAuthType == authTypeStreamingSigned {
if contentEncoding, ok := metadata["content-encoding"]; ok { if contentEncoding, ok := metadata["content-encoding"]; ok {
contentEncoding = trimAwsChunkedContentEncoding(contentEncoding) contentEncoding = trimAwsChunkedContentEncoding(contentEncoding)
@ -2774,3 +2819,103 @@ func (api objectAPIHandlers) GetObjectRetentionHandler(w http.ResponseWriter, r
Host: handlers.GetSourceIP(r), Host: handlers.GetSourceIP(r),
}) })
} }
// GetObjectTaggingHandler - GET object tagging
func (api objectAPIHandlers) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "GetObjectTagging")
defer logger.AuditLog(w, r, "GetObjectTagging", mustGetClaimsFromToken(r))
vars := mux.Vars(r)
bucket := vars["bucket"]
object := vars["object"]
objAPI := api.ObjectAPI()
if objAPI == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
return
}
// Allow getObjectTagging if policy action is set.
if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectTaggingAction, bucket, object); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
// Get object tags
tags, err := objAPI.GetObjectTag(ctx, bucket, object)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
writeSuccessResponseXML(w, encodeResponse(tags))
}
// PutObjectTaggingHandler - PUT object tagging
func (api objectAPIHandlers) PutObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "PutObjectTagging")
defer logger.AuditLog(w, r, "PutObjectTagging", mustGetClaimsFromToken(r))
vars := mux.Vars(r)
bucket := vars["bucket"]
object := vars["object"]
objAPI := api.ObjectAPI()
if objAPI == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
return
}
// Allow putObjectTagging if policy action is set
if s3Error := checkRequestAuthType(ctx, r, policy.PutObjectTaggingAction, bucket, object); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
tagging, err := tagging.ParseTagging(io.LimitReader(r.Body, r.ContentLength))
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
// Put object tags
err = objAPI.PutObjectTag(ctx, bucket, object, tagging.String())
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
writeSuccessResponseHeadersOnly(w)
}
// DeleteObjectTaggingHandler - DELETE object tagging
func (api objectAPIHandlers) DeleteObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "DeleteObjectTagging")
defer logger.AuditLog(w, r, "DeleteObjectTagging", mustGetClaimsFromToken(r))
vars := mux.Vars(r)
bucket := vars["bucket"]
object := vars["object"]
objAPI := api.ObjectAPI()
if objAPI == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
return
}
// Allow deleteObjectTagging if policy action is set
if s3Error := checkRequestAuthType(ctx, r, policy.DeleteObjectTaggingAction, bucket, object); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
// Delete object tags
err := objAPI.DeleteObjectTag(ctx, bucket, object)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
writeSuccessResponseHeadersOnly(w)
}

View File

@ -35,6 +35,7 @@ import (
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
"github.com/minio/minio/pkg/tagging"
) )
// setsStorageAPI is encapsulated type for Close() // setsStorageAPI is encapsulated type for Close()
@ -1661,6 +1662,21 @@ func (s *xlSets) ListObjectsHeal(ctx context.Context, bucket, prefix, marker, de
return s.listObjects(ctx, bucket, prefix, marker, delimiter, maxKeys, true) return s.listObjects(ctx, bucket, prefix, marker, delimiter, maxKeys, true)
} }
// PutObjectTag - replace or add tags to an existing object
func (s *xlSets) PutObjectTag(ctx context.Context, bucket, object string, tags string) error {
return s.getHashedSet(object).PutObjectTag(ctx, bucket, object, tags)
}
// DeleteObjectTag - delete object tags from an existing object
func (s *xlSets) DeleteObjectTag(ctx context.Context, bucket, object string) error {
return s.getHashedSet(object).DeleteObjectTag(ctx, bucket, object)
}
// GetObjectTag - get object tags from an existing object
func (s *xlSets) GetObjectTag(ctx context.Context, bucket, object string) (tagging.Tagging, error) {
return s.getHashedSet(object).GetObjectTag(ctx, bucket, object)
}
// GetMetrics - no op // GetMetrics - no op
func (s *xlSets) GetMetrics(ctx context.Context) (*Metrics, error) { func (s *xlSets) GetMetrics(ctx context.Context) (*Metrics, error) {
logger.LogIf(ctx, NotImplemented{}) logger.LogIf(ctx, NotImplemented{})

View File

@ -245,9 +245,13 @@ func (m xlMetaV1) ToObjectInfo(bucket, object string) ObjectInfo {
// Extract etag from metadata. // Extract etag from metadata.
objInfo.ETag = extractETag(m.Meta) objInfo.ETag = extractETag(m.Meta)
// Add user tags to the object info
objInfo.UserTags = m.Meta[xhttp.AmzObjectTagging]
// etag/md5Sum has already been extracted. We need to // etag/md5Sum has already been extracted. We need to
// remove to avoid it from appearing as part of // remove to avoid it from appearing as part of
// response headers. e.g, X-Minio-* or X-Amz-*. // response headers. e.g, X-Minio-* or X-Amz-*.
// Tags have also been extracted, we remove that as well.
objInfo.UserDefined = cleanMetadata(m.Meta) objInfo.UserDefined = cleanMetadata(m.Meta)
// All the parts per object. // All the parts per object.

View File

@ -27,6 +27,7 @@ import (
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/mimedb" "github.com/minio/minio/pkg/mimedb"
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
"github.com/minio/minio/pkg/tagging"
) )
// list all errors which can be ignored in object operations. // list all errors which can be ignored in object operations.
@ -969,7 +970,7 @@ func (xl xlObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuat
return listObjectsV2Info, err return listObjectsV2Info, err
} }
// Send the successul but partial upload, however ignore // Send the successful but partial upload, however ignore
// if the channel is blocked by other items. // if the channel is blocked by other items.
func (xl xlObjects) addPartialUpload(bucket, key string) { func (xl xlObjects) addPartialUpload(bucket, key string) {
select { select {
@ -977,3 +978,60 @@ func (xl xlObjects) addPartialUpload(bucket, key string) {
default: default:
} }
} }
// PutObjectTag - replace or add tags to an existing object
func (xl xlObjects) PutObjectTag(ctx context.Context, bucket, object string, tags string) error {
disks := xl.getDisks()
// Read metadata associated with the object from all disks.
metaArr, errs := readAllXLMetadata(ctx, disks, bucket, object)
_, writeQuorum, err := objectQuorumFromMeta(ctx, xl, metaArr, errs)
if err != nil {
return err
}
for i, xlMeta := range metaArr {
// clean xlMeta.Meta of tag key, before updating the new tags
delete(xlMeta.Meta, xhttp.AmzObjectTagging)
// Don't update for empty tags
if tags != "" {
xlMeta.Meta[xhttp.AmzObjectTagging] = tags
}
metaArr[i].Meta = xlMeta.Meta
}
tempObj := mustGetUUID()
// Write unique `xl.json` for each disk.
if disks, err = writeUniqueXLMetadata(ctx, disks, minioMetaTmpBucket, tempObj, metaArr, writeQuorum); err != nil {
return toObjectErr(err, bucket, object)
}
// Atomically rename `xl.json` from tmp location to destination for each disk.
if _, err = renameXLMetadata(ctx, disks, minioMetaTmpBucket, tempObj, bucket, object, writeQuorum); err != nil {
return toObjectErr(err, bucket, object)
}
return nil
}
// DeleteObjectTag - delete object tags from an existing object
func (xl xlObjects) DeleteObjectTag(ctx context.Context, bucket, object string) error {
return xl.PutObjectTag(ctx, bucket, object, "")
}
// GetObjectTag - get object tags from an existing object
func (xl xlObjects) GetObjectTag(ctx context.Context, bucket, object string) (tagging.Tagging, error) {
// GetObjectInfo will return tag value as well
oi, err := xl.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
if err != nil {
return tagging.Tagging{}, err
}
tags, err := tagging.FromString(oi.UserTags)
if err != nil {
return tagging.Tagging{}, err
}
return tags, nil
}

View File

@ -31,6 +31,7 @@ import (
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
"github.com/minio/minio/pkg/tagging"
) )
type xlZones struct { type xlZones struct {
@ -1374,3 +1375,63 @@ func (z *xlZones) GetMetrics(ctx context.Context) (*Metrics, error) {
func (z *xlZones) IsReady(ctx context.Context) bool { func (z *xlZones) IsReady(ctx context.Context) bool {
return z.zones[0].IsReady(ctx) return z.zones[0].IsReady(ctx)
} }
// PutObjectTag - replace or add tags to an existing object
func (z *xlZones) PutObjectTag(ctx context.Context, bucket, object string, tags string) error {
if z.SingleZone() {
return z.zones[0].PutObjectTag(ctx, bucket, object, tags)
}
for _, zone := range z.zones {
err := zone.PutObjectTag(ctx, bucket, object, tags)
if err != nil {
if isErrBucketNotFound(err) {
continue
}
return err
}
return nil
}
return BucketNotFound{
Bucket: bucket,
}
}
// DeleteObjectTag - delete object tags from an existing object
func (z *xlZones) DeleteObjectTag(ctx context.Context, bucket, object string) error {
if z.SingleZone() {
return z.zones[0].DeleteObjectTag(ctx, bucket, object)
}
for _, zone := range z.zones {
err := zone.DeleteObjectTag(ctx, bucket, object)
if err != nil {
if isErrBucketNotFound(err) {
continue
}
return err
}
return nil
}
return BucketNotFound{
Bucket: bucket,
}
}
// GetObjectTag - get object tags from an existing object
func (z *xlZones) GetObjectTag(ctx context.Context, bucket, object string) (tagging.Tagging, error) {
if z.SingleZone() {
return z.zones[0].GetObjectTag(ctx, bucket, object)
}
for _, zone := range z.zones {
tags, err := zone.GetObjectTag(ctx, bucket, object)
if err != nil {
if isErrBucketNotFound(err) {
continue
}
return tags, err
}
return tags, nil
}
return tagging.Tagging{}, BucketNotFound{
Bucket: bucket,
}
}

View File

@ -51,7 +51,6 @@ We found the following APIs to be redundant or less useful outside of AWS S3. If
- ObjectACL (Use [bucket policies](https://docs.min.io/docs/minio-client-complete-guide#policy) instead) - ObjectACL (Use [bucket policies](https://docs.min.io/docs/minio-client-complete-guide#policy) instead)
- ObjectTorrent - ObjectTorrent
- ObjectVersions - ObjectVersions
- ObjectTagging
### Object name restrictions on MinIO ### Object name restrictions on MinIO
Object names that contain characters `^*|\/&";` are unsupported on Windows and other file systems which do not support filenames with these characters. Note that this list is not exhaustive, and depends on the maintainers of the filesystem itself. Object names that contain characters `^*|\/&";` are unsupported on Windows and other file systems which do not support filenames with these characters. Note that this list is not exhaustive, and depends on the maintainers of the filesystem itself.

View File

@ -28,10 +28,12 @@ import (
"math/rand" "math/rand"
"net/http" "net/http"
"os" "os"
"reflect"
"strings" "strings"
"time" "time"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3"
@ -132,6 +134,50 @@ func randString(n int, src rand.Source, prefix string) string {
return prefix + string(b[0:30-len(prefix)]) return prefix + string(b[0:30-len(prefix)])
} }
func isObjectTaggingImplemented(s3Client *s3.S3) bool {
bucket := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-")
object := randString(60, rand.NewSource(time.Now().UnixNano()), "")
startTime := time.Now()
function := "isObjectTaggingImplemented"
args := map[string]interface{}{
"bucketName": bucket,
"objectName": object,
}
defer cleanup(s3Client, bucket, object, function, args, startTime, true)
_, err := s3Client.CreateBucket(&s3.CreateBucketInput{
Bucket: aws.String(bucket),
})
if err != nil {
failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal()
return false
}
_, err = s3Client.PutObject(&s3.PutObjectInput{
Body: aws.ReadSeekCloser(strings.NewReader("testfile")),
Bucket: aws.String(bucket),
Key: aws.String(object),
})
if err != nil {
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to success but got %v", err), err).Fatal()
return false
}
_, err = s3Client.GetObjectTagging(&s3.GetObjectTaggingInput{
Bucket: aws.String(bucket),
Key: aws.String(object),
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "NotImplemented" {
return false
}
}
}
return true
}
func cleanup(s3Client *s3.S3, bucket string, object string, function string, func cleanup(s3Client *s3.S3, bucket string, object string, function string,
args map[string]interface{}, startTime time.Time, deleteBucket bool) { args map[string]interface{}, startTime time.Time, deleteBucket bool) {
@ -474,6 +520,95 @@ func testSelectObject(s3Client *s3.S3) {
successLogger(function, args, startTime).Info() successLogger(function, args, startTime).Info()
} }
func testObjectTagging(s3Client *s3.S3) {
startTime := time.Now()
function := "testObjectTagging"
bucket := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-")
object := randString(60, rand.NewSource(time.Now().UnixNano()), "")
args := map[string]interface{}{
"bucketName": bucket,
"objectName": object,
}
_, err := s3Client.CreateBucket(&s3.CreateBucketInput{
Bucket: aws.String(bucket),
})
if err != nil {
failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal()
return
}
defer cleanup(s3Client, bucket, object, function, args, startTime, true)
taginput := "Tag1=Value1"
tagInputSet := []*s3.Tag{
{
Key: aws.String("Tag1"),
Value: aws.String("Value1"),
},
}
_, err = s3Client.PutObject(&s3.PutObjectInput{
Body: aws.ReadSeekCloser(strings.NewReader("testfile")),
Bucket: aws.String(bucket),
Key: aws.String(object),
Tagging: &taginput,
})
if err != nil {
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to success but got %v", err), err).Fatal()
return
}
tagop, err := s3Client.GetObjectTagging(&s3.GetObjectTaggingInput{
Bucket: aws.String(bucket),
Key: aws.String(object),
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObjectTagging expected to success but got %v", awsErr.Code()), err).Fatal()
return
}
}
if !reflect.DeepEqual(tagop.TagSet, tagInputSet) {
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObject Tag input did not match with GetObjectTagging output %v", nil), nil).Fatal()
return
}
taginputSet1 := []*s3.Tag{
{
Key: aws.String("Key4"),
Value: aws.String("Value4"),
},
}
_, err = s3Client.PutObjectTagging(&s3.PutObjectTaggingInput{
Bucket: aws.String(bucket),
Key: aws.String(object),
Tagging: &s3.Tagging{
TagSet: taginputSet1,
},
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObjectTagging expected to success but got %v", awsErr.Code()), err).Fatal()
return
}
}
tagop, err = s3Client.GetObjectTagging(&s3.GetObjectTaggingInput{
Bucket: aws.String(bucket),
Key: aws.String(object),
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObjectTagging expected to success but got %v", awsErr.Code()), err).Fatal()
return
}
}
if !reflect.DeepEqual(tagop.TagSet, taginputSet1) {
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObjectTagging input did not match with GetObjectTagging output %v", nil), nil).Fatal()
return
}
}
func main() { func main() {
endpoint := os.Getenv("SERVER_ENDPOINT") endpoint := os.Getenv("SERVER_ENDPOINT")
accessKey := os.Getenv("ACCESS_KEY") accessKey := os.Getenv("ACCESS_KEY")
@ -508,4 +643,7 @@ func main() {
testPresignedPutInvalidHash(s3Client) testPresignedPutInvalidHash(s3Client)
testListObjects(s3Client) testListObjects(s3Client)
testSelectObject(s3Client) testSelectObject(s3Client)
if isObjectTaggingImplemented(s3Client) {
testObjectTagging(s3Client)
}
} }

View File

@ -114,6 +114,15 @@ const (
// PutBucketObjectLockConfigurationAction - PutBucketObjectLockConfiguration Rest API action // PutBucketObjectLockConfigurationAction - PutBucketObjectLockConfiguration Rest API action
PutBucketObjectLockConfigurationAction = "s3:PutBucketObjectLockConfiguration" PutBucketObjectLockConfigurationAction = "s3:PutBucketObjectLockConfiguration"
// GetObjectTaggingAction - Get Object Tags API action
GetObjectTaggingAction = "s3:GetObjectTagging"
// PutObjectTaggingAction - Put Object Tags API action
PutObjectTaggingAction = "s3:PutObjectTagging"
// DeleteObjectTaggingAction - Delete Object Tags API action
DeleteObjectTaggingAction = "s3:DeleteObjectTagging"
// AllActions - all API actions // AllActions - all API actions
AllActions = "s3:*" AllActions = "s3:*"
) )
@ -149,6 +158,9 @@ var supportedActions = map[Action]struct{}{
GetBucketObjectLockConfigurationAction: {}, GetBucketObjectLockConfigurationAction: {},
BypassGovernanceModeAction: {}, BypassGovernanceModeAction: {},
BypassGovernanceRetentionAction: {}, BypassGovernanceRetentionAction: {},
GetObjectTaggingAction: {},
PutObjectTaggingAction: {},
DeleteObjectTaggingAction: {},
} }
// isObjectAction - returns whether action is object type or not. // isObjectAction - returns whether action is object type or not.
@ -164,6 +176,8 @@ func (action Action) isObjectAction() bool {
return true return true
case PutObjectLegalHoldAction, GetObjectLegalHoldAction: case PutObjectLegalHoldAction, GetObjectLegalHoldAction:
return true return true
case GetObjectTaggingAction, PutObjectTaggingAction, DeleteObjectTaggingAction:
return true
} }
return false return false
@ -283,4 +297,7 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
BypassGovernanceRetentionAction: condition.NewKeySet(condition.CommonKeys...), BypassGovernanceRetentionAction: condition.NewKeySet(condition.CommonKeys...),
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...), GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...), PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
PutObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
GetObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
DeleteObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
} }

View File

@ -106,6 +106,13 @@ const (
GetBucketObjectLockConfigurationAction = "s3:GetBucketObjectLockConfiguration" GetBucketObjectLockConfigurationAction = "s3:GetBucketObjectLockConfiguration"
// PutBucketObjectLockConfigurationAction - PutObjectLockConfiguration Rest API action // PutBucketObjectLockConfigurationAction - PutObjectLockConfiguration Rest API action
PutBucketObjectLockConfigurationAction = "s3:PutBucketObjectLockConfiguration" PutBucketObjectLockConfigurationAction = "s3:PutBucketObjectLockConfiguration"
// GetObjectTaggingAction - Get Object Tags API action
GetObjectTaggingAction = "s3:GetObjectTagging"
// PutObjectTaggingAction - Put Object Tags API action
PutObjectTaggingAction = "s3:PutObjectTagging"
// DeleteObjectTaggingAction - Delete Object Tags API action
DeleteObjectTaggingAction = "s3:DeleteObjectTagging"
) )
// isObjectAction - returns whether action is object type or not. // isObjectAction - returns whether action is object type or not.
@ -121,6 +128,8 @@ func (action Action) isObjectAction() bool {
return true return true
case BypassGovernanceModeAction, BypassGovernanceRetentionAction: case BypassGovernanceModeAction, BypassGovernanceRetentionAction:
return true return true
case GetObjectTaggingAction, PutObjectTaggingAction, DeleteObjectTaggingAction:
return true
} }
return false return false
@ -153,6 +162,8 @@ func (action Action) IsValid() bool {
return true return true
case PutBucketObjectLockConfigurationAction, GetBucketObjectLockConfigurationAction: case PutBucketObjectLockConfigurationAction, GetBucketObjectLockConfigurationAction:
return true return true
case GetObjectTaggingAction, PutObjectTaggingAction, DeleteObjectTaggingAction:
return true
} }
return false return false
@ -243,4 +254,7 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...), GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...), GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...), PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
PutObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
GetObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
DeleteObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
} }

44
pkg/tagging/error.go Normal file
View File

@ -0,0 +1,44 @@
/*
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tagging
import (
"fmt"
)
// Error is the generic type for any error happening during tag
// parsing.
type Error struct {
err error
}
// Errorf - formats according to a format specifier and returns
// the string as a value that satisfies error of type tagging.Error
func Errorf(format string, a ...interface{}) error {
return Error{err: fmt.Errorf(format, a...)}
}
// Unwrap the internal error.
func (e Error) Unwrap() error { return e.err }
// Error 'error' compatible method.
func (e Error) Error() string {
if e.err == nil {
return "tagging: cause <nil>"
}
return e.err.Error()
}

62
pkg/tagging/tag.go Normal file
View File

@ -0,0 +1,62 @@
/*
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tagging
import (
"encoding/xml"
"unicode/utf8"
)
// Tag - single tag
type Tag struct {
XMLName xml.Name `xml:"Tag"`
Key string `xml:"Key"`
Value string `xml:"Value"`
}
// Validate - validates the tag element
func (t Tag) Validate() error {
if err := t.validateKey(); err != nil {
return err
}
if err := t.validateValue(); err != nil {
return err
}
return nil
}
// validateKey - checks if key is valid or not.
func (t Tag) validateKey() error {
// cannot be longer than maxTagKeyLength characters
if utf8.RuneCountInString(t.Key) > maxTagKeyLength {
return ErrInvalidTagKey
}
// cannot be empty
if len(t.Key) == 0 {
return ErrInvalidTagKey
}
return nil
}
// validateValue - checks if value is valid or not.
func (t Tag) validateValue() error {
// cannot be longer than maxTagValueLength characters
if utf8.RuneCountInString(t.Value) > maxTagValueLength {
return ErrInvalidTagValue
}
return nil
}

113
pkg/tagging/tagging.go Normal file
View File

@ -0,0 +1,113 @@
/*
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tagging
import (
"bytes"
"encoding/xml"
"io"
"net/url"
)
// S3 API limits for tags
// Ref: https://docs.aws.amazon.com/AmazonS3/latest/dev/object-tagging.html
const (
maxTags = 10
maxTagKeyLength = 128
maxTagValueLength = 256
)
// errors returned by tagging package
var (
ErrTooManyTags = Errorf("Cannot have more than 10 object tags")
ErrInvalidTagKey = Errorf("The TagKey you have provided is invalid")
ErrInvalidTagValue = Errorf("The TagValue you have provided is invalid")
ErrInvalidTag = Errorf("Cannot provide multiple Tags with the same key")
)
// Tagging - object tagging interface
type Tagging struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Tagging"`
TagSet TagSet `xml:"TagSet"`
}
// Validate - validates the tagging configuration
func (t Tagging) Validate() error {
// Tagging can't have more than 10 tags
if len(t.TagSet.Tags) > maxTags {
return ErrTooManyTags
}
// Validate all the rules in the tagging config
for _, ts := range t.TagSet.Tags {
if t.TagSet.ContainsDuplicate(ts.Key) {
return ErrInvalidTag
}
if err := ts.Validate(); err != nil {
return err
}
}
return nil
}
// String - returns a string in format "tag1=value1&tag2=value2" with all the
// tags in this Tagging Struct
func (t Tagging) String() string {
var buf bytes.Buffer
for _, tag := range t.TagSet.Tags {
if buf.Len() > 0 {
buf.WriteString("&")
}
buf.WriteString(tag.Key + "=")
buf.WriteString(tag.Value)
}
return buf.String()
}
// FromString - returns a Tagging struct when given a string in format
// "tag1=value1&tag2=value2"
func FromString(tagStr string) (Tagging, error) {
tags, err := url.ParseQuery(tagStr)
if err != nil {
return Tagging{}, err
}
var idx = 0
parsedTags := make([]Tag, len(tags))
for k := range tags {
parsedTags[idx].Key = k
parsedTags[idx].Value = tags.Get(k)
idx++
}
return Tagging{
TagSet: TagSet{
Tags: parsedTags,
},
}, nil
}
// ParseTagging - parses incoming xml data in given reader
// into Tagging interface. After parsing, also validates the
// parsed fields based on S3 API constraints.
func ParseTagging(reader io.Reader) (*Tagging, error) {
var t Tagging
if err := xml.NewDecoder(reader).Decode(&t); err != nil {
return nil, err
}
if err := t.Validate(); err != nil {
return nil, err
}
return &t, nil
}

41
pkg/tagging/tagset.go Normal file
View File

@ -0,0 +1,41 @@
/*
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tagging
import (
"encoding/xml"
)
// TagSet - Set of tags under Tagging
type TagSet struct {
XMLName xml.Name `xml:"TagSet"`
Tags []Tag `xml:"Tag"`
}
// ContainsDuplicate - returns true if duplicate keys are present in TagSet
func (t TagSet) ContainsDuplicate(key string) bool {
var found bool
for _, tag := range t.Tags {
if tag.Key == key {
if found {
return true
}
found = true
}
}
return false
}