From 1f2cf0ed67a7b07c101d18a64b41a790b89882d6 Mon Sep 17 00:00:00 2001
From: Denis Kirillov <d.kirillov@yadro.com>
Date: Mon, 12 Feb 2024 11:00:04 +0300
Subject: [PATCH] [#306] Use APE instead of eACL on bucket creation

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
---
 api/data/info.go            |   2 +
 api/handler/put.go          | 200 +++++++++++++++++++++++++++++++-----
 api/layer/container.go      |  26 +++--
 api/layer/layer.go          |   2 -
 internal/frostfs/frostfs.go |   2 +-
 internal/logs/logs.go       |   1 +
 pkg/service/tree/tree.go    |   8 +-
 7 files changed, 202 insertions(+), 39 deletions(-)

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
 }