diff --git a/api/data/info.go b/api/data/info.go index 73e277ba1..5739f6f69 100644 --- a/api/data/info.go +++ b/api/data/info.go @@ -31,6 +31,7 @@ type ( LocationConstraint string ObjectLockEnabled bool HomomorphicHashDisabled bool + APEEnabled bool } // ObjectInfo holds S3 object data. @@ -62,6 +63,7 @@ type ( BucketSettings struct { Versioning string `json:"versioning"` LockConfiguration *ObjectLockConfiguration `json:"lock_configuration"` + APE bool `json:"ape"` } // CORSConfiguration stores CORS configuration of a request. diff --git a/api/handler/put.go b/api/handler/put.go index 1a66682df..24135b047 100644 --- a/api/handler/put.go +++ b/api/handler/put.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/md5" "encoding/base64" + "encoding/hex" "encoding/json" "encoding/xml" "fmt" @@ -24,8 +25,14 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" + cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" + "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" + "git.frostfs.info/TrueCloudLab/policy-engine/schema/s3" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "go.uber.org/zap" ) @@ -744,6 +751,20 @@ func parseMetadata(r *http.Request) map[string]string { return res } +func parseCannedACL(header http.Header) (string, error) { + acl := header.Get(api.AmzACL) + if len(acl) == 0 { + return basicACLPrivate, nil + } + + if acl == basicACLPrivate || acl == basicACLPublic || + acl == cannedACLAuthRead || acl == basicACLReadOnly { + return acl, nil + } + + return "", fmt.Errorf("unknown acl: %s", acl) +} + func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reqInfo := middleware.GetReqInfo(ctx) @@ -763,16 +784,9 @@ func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) { return } - bktACL, err := parseACLHeaders(r.Header, key) + cannedACL, err := parseCannedACL(r.Header) if err != nil { - h.logAndSendError(w, "could not parse bucket acl", reqInfo, err) - return - } - resInfo := &resourceInfo{Bucket: reqInfo.BucketName} - - p.EACL, err = bucketACLToTable(bktACL, resInfo) - if err != nil { - h.logAndSendError(w, "could translate bucket acl to eacl", reqInfo, err) + h.logAndSendError(w, "could not parse canned ACL", reqInfo, err) return } @@ -787,7 +801,6 @@ func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) { if err == nil { policies = boxData.Policies p.SessionContainerCreation = boxData.Gate.SessionTokenForPut() - p.SessionEACL = boxData.Gate.SessionTokenForSetEACL() } if p.SessionContainerCreation == nil { @@ -795,12 +808,7 @@ func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) { return } - if p.SessionEACL == nil { - h.logAndSendError(w, "couldn't find session token for setEACL", reqInfo, errors.GetAPIError(errors.ErrAccessDenied)) - return - } - - if err = h.setPolicy(p, reqInfo.Namespace, createParams.LocationConstraint, policies); err != nil { + if err = h.setPlacementPolicy(p, reqInfo.Namespace, createParams.LocationConstraint, policies); err != nil { h.logAndSendError(w, "couldn't set placement policy", reqInfo, err) return } @@ -812,25 +820,165 @@ func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) { h.logAndSendError(w, "could not create bucket", reqInfo, err) return } - h.reqLogger(ctx).Info(logs.BucketIsCreated, zap.Stringer("container_id", bktInfo.CID)) - if p.ObjectLockEnabled { - sp := &layer.PutSettingsParams{ - BktInfo: bktInfo, - Settings: &data.BucketSettings{Versioning: data.VersioningEnabled}, - } - if err = h.obj.PutBucketSettings(ctx, sp); err != nil { - h.logAndSendError(w, "couldn't enable bucket versioning", reqInfo, err, - zap.String("container_id", bktInfo.CID.EncodeToString())) + chainRules := bucketCannedACLToAPERules(cannedACL, reqInfo, key, bktInfo.CID) + + target := engine.NamespaceTarget(reqInfo.Namespace) + for _, chainPolicy := range chainRules { + if err = h.ape.AddChain(target, chainPolicy); err != nil { + h.logAndSendError(w, "failed to add morph rule chain", reqInfo, err, zap.String("chain_id", string(chainPolicy.ID))) return } } + sp := &layer.PutSettingsParams{ + BktInfo: bktInfo, + Settings: &data.BucketSettings{APE: true}, + } + + if p.ObjectLockEnabled { + sp.Settings.Versioning = data.VersioningEnabled + } + + if err = h.obj.PutBucketSettings(ctx, sp); err != nil { + h.logAndSendError(w, "couldn't save bucket settings", reqInfo, err, + zap.String("container_id", bktInfo.CID.EncodeToString())) + return + } + middleware.WriteSuccessResponseHeadersOnly(w) } -func (h handler) setPolicy(prm *layer.CreateBucketParams, namespace, locationConstraint string, userPolicies []*accessbox.ContainerPolicy) error { +const s3ActionPrefix = "s3:" + +var ( + // https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html + + writeACLBucketS3Actions = []string{ + s3ActionPrefix + middleware.PutObjectOperation, + s3ActionPrefix + middleware.PostObjectOperation, + s3ActionPrefix + middleware.CopyObjectOperation, + s3ActionPrefix + middleware.UploadPartOperation, + s3ActionPrefix + middleware.UploadPartCopyOperation, + s3ActionPrefix + middleware.CreateMultipartUploadOperation, + s3ActionPrefix + middleware.CompleteMultipartUploadOperation, + } + + readACLBucketS3Actions = []string{ + s3ActionPrefix + middleware.HeadBucketOperation, + s3ActionPrefix + middleware.GetBucketLocationOperation, + s3ActionPrefix + middleware.ListObjectsV1Operation, + s3ActionPrefix + middleware.ListObjectsV2Operation, + s3ActionPrefix + middleware.ListBucketObjectVersionsOperation, + s3ActionPrefix + middleware.ListMultipartUploadsOperation, + } + + writeACLBucketNativeActions = []string{ + native.MethodPutObject, + } + + readACLBucketNativeActions = []string{ + native.MethodGetContainer, + native.MethodGetObject, + native.MethodHeadObject, + native.MethodSearchObject, + native.MethodRangeObject, + native.MethodHashObject, + } +) + +func bucketCannedACLToAPERules(cannedACL string, reqInfo *middleware.ReqInfo, key *keys.PublicKey, cnrID cid.ID) []*chain.Chain { + cnrIDStr := cnrID.EncodeToString() + + chains := []*chain.Chain{ + { + ID: getBucketCannedChainID(chain.S3, cnrID), + Rules: []chain.Rule{{ + Status: chain.Allow, + Actions: chain.Actions{Names: []string{"s3:*"}}, + Resources: chain.Resources{Names: []string{ + fmt.Sprintf(s3.ResourceFormatS3Bucket, reqInfo.BucketName), + fmt.Sprintf(s3.ResourceFormatS3BucketObjects, reqInfo.BucketName), + }}, + Condition: []chain.Condition{{ + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: s3.PropertyKeyOwner, + Value: key.Address(), + }}, + }}}, + { + ID: getBucketCannedChainID(chain.Ingress, cnrID), + Rules: []chain.Rule{{ + Status: chain.Allow, + Actions: chain.Actions{Names: []string{"*"}}, + Resources: chain.Resources{Names: []string{ + fmt.Sprintf(native.ResourceFormatNamespaceContainer, reqInfo.Namespace, cnrIDStr), + fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, reqInfo.Namespace, cnrIDStr), + }}, + Condition: []chain.Condition{{ + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: native.PropertyKeyActorPublicKey, + Value: hex.EncodeToString(key.Bytes()), + }}, + }}, + }, + } + + switch cannedACL { + case basicACLPrivate: + case cannedACLAuthRead: + fallthrough + case basicACLReadOnly: + chains[0].Rules = append(chains[0].Rules, chain.Rule{ + Status: chain.Allow, + Actions: chain.Actions{Names: readACLBucketS3Actions}, + Resources: chain.Resources{Names: []string{ + fmt.Sprintf(s3.ResourceFormatS3Bucket, reqInfo.BucketName), + fmt.Sprintf(s3.ResourceFormatS3BucketObjects, reqInfo.BucketName), + }}, + }) + + chains[1].Rules = append(chains[1].Rules, chain.Rule{ + Status: chain.Allow, + Actions: chain.Actions{Names: readACLBucketNativeActions}, + Resources: chain.Resources{Names: []string{ + fmt.Sprintf(native.ResourceFormatNamespaceContainer, reqInfo.Namespace, cnrIDStr), + fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, reqInfo.Namespace, cnrIDStr), + }}, + }) + case basicACLPublic: + chains[0].Rules = append(chains[0].Rules, chain.Rule{ + Status: chain.Allow, + Actions: chain.Actions{Names: append(readACLBucketS3Actions, writeACLBucketS3Actions...)}, + Resources: chain.Resources{Names: []string{ + fmt.Sprintf(s3.ResourceFormatS3Bucket, reqInfo.BucketName), + fmt.Sprintf(s3.ResourceFormatS3BucketObjects, reqInfo.BucketName), + }}, + }) + + chains[1].Rules = append(chains[1].Rules, chain.Rule{ + Status: chain.Allow, + Actions: chain.Actions{Names: append(readACLBucketNativeActions, writeACLBucketNativeActions...)}, + Resources: chain.Resources{Names: []string{ + fmt.Sprintf(native.ResourceFormatNamespaceContainer, reqInfo.Namespace, cnrIDStr), + fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, reqInfo.Namespace, cnrIDStr), + }}, + }) + default: + panic("unknown canned acl") // this should never happen + } + + return chains +} + +func getBucketCannedChainID(prefix chain.Name, cnrID cid.ID) chain.ID { + return chain.ID(string(prefix) + ":bktCanned" + string(cnrID[:])) +} + +func (h handler) setPlacementPolicy(prm *layer.CreateBucketParams, namespace, locationConstraint string, userPolicies []*accessbox.ContainerPolicy) error { prm.Policy = h.cfg.DefaultPlacementPolicy(namespace) prm.LocationConstraint = locationConstraint diff --git a/api/layer/container.go b/api/layer/container.go index e3cba2d3d..e03845c79 100644 --- a/api/layer/container.go +++ b/api/layer/container.go @@ -28,6 +28,7 @@ type ( const ( attributeLocationConstraint = ".s3-location-constraint" + attributeAPEEnabled = ".s3-APE-enabled" AttributeLockEnabled = "LockEnabled" ) @@ -74,6 +75,17 @@ func (n *layer) containerInfo(ctx context.Context, idCnr cid.ID) (*data.BucketIn } } + APEEnabled := cnr.Attribute(attributeAPEEnabled) + if len(APEEnabled) > 0 { + info.APEEnabled, err = strconv.ParseBool(APEEnabled) + if err != nil { + log.Error(logs.CouldNotParseContainerAPEEnabledAttribute, + zap.String("ape_enabled", APEEnabled), + zap.Error(err), + ) + } + } + zone, _ := n.features.FormContainerZone(reqInfo.Namespace) if zone != info.Zone { return nil, fmt.Errorf("ns '%s' and zone '%s' are mismatched for container '%s'", zone, info.Zone, idCnr) @@ -119,13 +131,13 @@ func (n *layer) createContainer(ctx context.Context, p *CreateBucketParams) (*da Created: TimeNow(ctx), LocationConstraint: p.LocationConstraint, ObjectLockEnabled: p.ObjectLockEnabled, + APEEnabled: true, } - var attributes [][2]string - - attributes = append(attributes, [2]string{ - attributeLocationConstraint, p.LocationConstraint, - }) + attributes := [][2]string{ + {attributeLocationConstraint, p.LocationConstraint}, + {attributeAPEEnabled, "true"}, + } if p.ObjectLockEnabled { attributes = append(attributes, [2]string{ @@ -149,10 +161,6 @@ func (n *layer) createContainer(ctx context.Context, p *CreateBucketParams) (*da bktInfo.CID = res.ContainerID bktInfo.HomomorphicHashDisabled = res.HomomorphicHashDisabled - if err = n.setContainerEACLTable(ctx, bktInfo.CID, p.EACL, p.SessionEACL); err != nil { - return nil, fmt.Errorf("set container eacl: %w", err) - } - n.cache.PutBucket(bktInfo) return bktInfo, nil diff --git a/api/layer/layer.go b/api/layer/layer.go index 76fe496e2..1049817df 100644 --- a/api/layer/layer.go +++ b/api/layer/layer.go @@ -175,9 +175,7 @@ type ( Name string Namespace string Policy netmap.PlacementPolicy - EACL *eacl.Table SessionContainerCreation *session.Container - SessionEACL *session.Container LocationConstraint string ObjectLockEnabled bool } diff --git a/internal/frostfs/frostfs.go b/internal/frostfs/frostfs.go index 95630acab..f541cd2a7 100644 --- a/internal/frostfs/frostfs.go +++ b/internal/frostfs/frostfs.go @@ -108,7 +108,7 @@ var basicACLZero acl.Basic // If prm.BasicACL is zero, 'eacl-public-read-write' is used. func (x *FrostFS) CreateContainer(ctx context.Context, prm layer.PrmContainerCreate) (*layer.ContainerCreateResult, error) { if prm.BasicACL == basicACLZero { - prm.BasicACL = acl.PublicRWExtended + prm.BasicACL = acl.PublicRW } var cnr container.Container diff --git a/internal/logs/logs.go b/internal/logs/logs.go index 45641063d..7a55f2642 100644 --- a/internal/logs/logs.go +++ b/internal/logs/logs.go @@ -141,4 +141,5 @@ const ( CouldntDeleteObjectFromStorageContinueDeleting = "couldn't delete object from storage, continue deleting from tree" CouldntPutAccessBoxIntoCache = "couldn't put accessbox into cache" InvalidAccessBoxCacheRemovingCheckInterval = "invalid accessbox check removing interval, using default value" + CouldNotParseContainerAPEEnabledAttribute = "could not parse container APE enabled attribute" ) diff --git a/pkg/service/tree/tree.go b/pkg/service/tree/tree.go index ab3d86f9c..7f94220b5 100644 --- a/pkg/service/tree/tree.go +++ b/pkg/service/tree/tree.go @@ -78,6 +78,7 @@ var ( const ( versioningKV = "Versioning" + apeKV = "APE" lockConfigurationKV = "LockConfiguration" oidKV = "OID" @@ -332,7 +333,7 @@ func newPartInfo(node NodeResponse) (*data.PartInfo, error) { } func (c *Tree) GetSettingsNode(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error) { - keysToReturn := []string{versioningKV, lockConfigurationKV} + keysToReturn := []string{versioningKV, lockConfigurationKV, apeKV} node, err := c.getSystemNode(ctx, bktInfo, []string{settingsFileName}, keysToReturn) if err != nil { return nil, fmt.Errorf("couldn't get node: %w", err) @@ -349,6 +350,10 @@ func (c *Tree) GetSettingsNode(ctx context.Context, bktInfo *data.BucketInfo) (* } } + if ape, ok := node.Get(apeKV); ok { + settings.APE, _ = strconv.ParseBool(ape) + } + return settings, nil } @@ -1384,6 +1389,7 @@ func metaFromSettings(settings *data.BucketSettings) map[string]string { results[FileNameKey] = settingsFileName results[versioningKV] = settings.Versioning results[lockConfigurationKV] = encodeLockConfiguration(settings.LockConfiguration) + results[apeKV] = strconv.FormatBool(settings.APE) return results }