[#213] Add object acl versioning

Signed-off-by: Denis Kirillov <denis@nspcc.ru>
This commit is contained in:
Denis Kirillov 2021-08-30 10:55:42 +03:00 committed by Alex Vanin
parent 5502fb97c3
commit a9be642eaf
4 changed files with 441 additions and 139 deletions

View file

@ -12,6 +12,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl" "github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
"github.com/nspcc-dev/neofs-api-go/pkg/object" "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-api-go/v2/acl"
"github.com/nspcc-dev/neofs-s3-gw/api" "github.com/nspcc-dev/neofs-s3-gw/api"
"github.com/nspcc-dev/neofs-s3-gw/api/cache" "github.com/nspcc-dev/neofs-s3-gw/api/cache"
"github.com/nspcc-dev/neofs-s3-gw/api/errors" "github.com/nspcc-dev/neofs-s3-gw/api/errors"
@ -69,6 +70,7 @@ type bucketPolicy struct {
Version string `json:"Version"` Version string `json:"Version"`
ID string `json:"Id"` ID string `json:"Id"`
Statement []statement `json:"Statement"` Statement []statement `json:"Statement"`
Bucket string `json:"-"`
} }
type statement struct { type statement struct {
@ -89,10 +91,30 @@ type ast struct {
} }
type astResource struct { type astResource struct {
Name string resourceInfo
Operations []*astOperation Operations []*astOperation
} }
type resourceInfo struct {
Bucket string
Object string
Version string
}
func (r *resourceInfo) Name() string {
if len(r.Object) == 0 {
return r.Bucket
}
if len(r.Version) == 0 {
return r.Bucket + "/" + r.Object
}
return r.Bucket + "/" + r.Object + ":" + r.Version
}
func (r *resourceInfo) IsBucket() bool {
return len(r.Object) == 0
}
type astOperation struct { type astOperation struct {
Users []string Users []string
Role eacl.Role Role eacl.Role
@ -137,27 +159,20 @@ func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
list.Resource = reqInfo.BucketName resInfo := &resourceInfo{Bucket: reqInfo.BucketName}
list.IsBucket = true astBucket, err := aclToAst(list, resInfo)
bktPolicy, err := aclToPolicy(list)
if err != nil { if err != nil {
h.logAndSendError(w, "could not translate acl to policy", reqInfo, err) h.logAndSendError(w, "could not translate acl to policy", reqInfo, err)
return return
} }
if err = h.updateBucketACL(r, bktPolicy, reqInfo.BucketName); err != nil { if err = h.updateBucketACL(r, astBucket, reqInfo.BucketName); err != nil {
h.logAndSendError(w, "could not update bucket acl", reqInfo, err) h.logAndSendError(w, "could not update bucket acl", reqInfo, err)
return return
} }
} }
func (h *handler) updateBucketACL(r *http.Request, bktPolicy *bucketPolicy, bkt string) error { func (h *handler) updateBucketACL(r *http.Request, astChild *ast, bkt string) error {
astChild, err := policyToAst(bktPolicy)
if err != nil {
return fmt.Errorf("could not translate policy to ast: %w", err)
}
bucketACL, err := h.obj.GetBucketACL(r.Context(), bkt) bucketACL, err := h.obj.GetBucketACL(r.Context(), bkt)
if err != nil { if err != nil {
return fmt.Errorf("could not get bucket eacl: %w", err) return fmt.Errorf("could not get bucket eacl: %w", err)
@ -169,8 +184,8 @@ func (h *handler) updateBucketACL(r *http.Request, bktPolicy *bucketPolicy, bkt
parentAst := tableToAst(bucketACL.EACL, bkt) parentAst := tableToAst(bucketACL.EACL, bkt)
for _, resource := range parentAst.Resources { for _, resource := range parentAst.Resources {
if resource.Name == bucketACL.Info.CID.String() { if resource.Bucket == bucketACL.Info.CID.String() {
resource.Name = bkt resource.Bucket = bkt
} }
} }
@ -179,7 +194,7 @@ func (h *handler) updateBucketACL(r *http.Request, bktPolicy *bucketPolicy, bkt
return nil return nil
} }
table, err := astToTable(resAst, bkt) table, err := astToTable(resAst)
if err != nil { if err != nil {
return fmt.Errorf("could not translate ast to table: %w", err) return fmt.Errorf("could not translate ast to table: %w", err)
} }
@ -218,6 +233,7 @@ func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
var ( var (
err error err error
reqInfo = api.GetReqInfo(r.Context()) reqInfo = api.GetReqInfo(r.Context())
versionID = reqInfo.URL.Query().Get(api.QueryVersionID)
) )
list := &AccessControlPolicy{} list := &AccessControlPolicy{}
@ -232,18 +248,22 @@ func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
list.Resource = reqInfo.BucketName + "/" + reqInfo.ObjectName resInfo := &resourceInfo{
Bucket: reqInfo.BucketName,
Object: reqInfo.ObjectName,
Version: versionID,
}
bktPolicy, err := aclToPolicy(list) astObject, err := aclToAst(list, resInfo)
if err != nil { if err != nil {
h.logAndSendError(w, "could not translate acl to policy", reqInfo, err) h.logAndSendError(w, "could not translate acl to ast", reqInfo, err)
return return
} }
p := &layer.HeadObjectParams{ p := &layer.HeadObjectParams{
Bucket: reqInfo.BucketName, Bucket: reqInfo.BucketName,
Object: reqInfo.ObjectName, Object: reqInfo.ObjectName,
VersionID: reqInfo.URL.Query().Get(api.QueryVersionID), VersionID: versionID,
} }
if _, err = h.obj.GetObjectInfo(r.Context(), p); err != nil { if _, err = h.obj.GetObjectInfo(r.Context(), p); err != nil {
@ -251,7 +271,7 @@ func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if err = h.updateBucketACL(r, bktPolicy, reqInfo.BucketName); err != nil { if err = h.updateBucketACL(r, astObject, reqInfo.BucketName); err != nil {
h.logAndSendError(w, "could not update bucket acl", reqInfo, err) h.logAndSendError(w, "could not update bucket acl", reqInfo, err)
return return
} }
@ -295,13 +315,19 @@ func checkOwner(info *cache.BucketInfo, owner string) error {
func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
reqInfo := api.GetReqInfo(r.Context()) reqInfo := api.GetReqInfo(r.Context())
bktPolicy := &bucketPolicy{} bktPolicy := &bucketPolicy{Bucket: reqInfo.BucketName}
if err := json.NewDecoder(r.Body).Decode(bktPolicy); err != nil { if err := json.NewDecoder(r.Body).Decode(bktPolicy); err != nil {
h.logAndSendError(w, "could not parse bucket policy", reqInfo, err) h.logAndSendError(w, "could not parse bucket policy", reqInfo, err)
return return
} }
if err := h.updateBucketACL(r, bktPolicy, reqInfo.BucketName); err != nil { astPolicy, err := policyToAst(bktPolicy)
if err != nil {
h.logAndSendError(w, "could not translate policy to ast", reqInfo, err)
return
}
if err = h.updateBucketACL(r, astPolicy, reqInfo.BucketName); err != nil {
h.logAndSendError(w, "could not update bucket acl", reqInfo, err) h.logAndSendError(w, "could not update bucket acl", reqInfo, err)
return return
} }
@ -465,22 +491,32 @@ func tableToAst(table *eacl.Table, bktName string) *ast {
rr := make(map[string]*astResource) rr := make(map[string]*astResource)
for _, record := range table.Records() { for _, record := range table.Records() {
resname := bktName resName := bktName
var objectName string
var version string
for _, filter := range record.Filters() { for _, filter := range record.Filters() {
if filter.Matcher() == eacl.MatchStringEqual && filter.Key() == object.AttributeFileName { if filter.Matcher() == eacl.MatchStringEqual {
resname = filter.Value() if filter.Key() == object.AttributeFileName {
objectName = filter.Value()
resName += "/" + objectName
} else if filter.Key() == acl.FilterObjectID {
version = filter.Value()
resName += "/" + version
} }
} }
r, ok := rr[resname] }
r, ok := rr[resName]
if !ok { if !ok {
r = &astResource{ r = &astResource{resourceInfo: resourceInfo{
Name: resname, Bucket: bktName,
} Object: objectName,
Version: version,
}}
} }
for _, target := range record.Targets() { for _, target := range record.Targets() {
r.Operations = addToList(r.Operations, record, target) r.Operations = addToList(r.Operations, record, target)
} }
rr[resname] = r rr[resName] = r
} }
for _, val := range rr { for _, val := range rr {
@ -493,7 +529,7 @@ func tableToAst(table *eacl.Table, bktName string) *ast {
func mergeAst(parent, child *ast) (*ast, bool) { func mergeAst(parent, child *ast) (*ast, bool) {
updated := false updated := false
for _, resource := range child.Resources { for _, resource := range child.Resources {
parentResource := getParentResource(parent, resource.Name) parentResource := getParentResource(parent, resource)
if parentResource == nil { if parentResource == nil {
parent.Resources = append(parent.Resources, resource) parent.Resources = append(parent.Resources, resource)
updated = true updated = true
@ -652,20 +688,21 @@ func removeUsers(resource *astResource, astOperation *astOperation, users []stri
} }
} }
func getParentResource(parent *ast, resource string) *astResource { func getParentResource(parent *ast, resource *astResource) *astResource {
for _, parentResource := range parent.Resources { for _, parentResource := range parent.Resources {
if resource == parentResource.Name { if resource.Bucket == parentResource.Bucket && resource.Object == parentResource.Object &&
resource.Version == parentResource.Version {
return parentResource return parentResource
} }
} }
return nil return nil
} }
func astToTable(ast *ast, bkt string) (*eacl.Table, error) { func astToTable(ast *ast) (*eacl.Table, error) {
table := eacl.NewTable() table := eacl.NewTable()
for _, resource := range ast.Resources { for _, resource := range ast.Resources {
records, err := formRecords(resource.Operations, resource.Name, bkt) records, err := formRecords(resource.Operations, resource)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -677,7 +714,7 @@ func astToTable(ast *ast, bkt string) (*eacl.Table, error) {
return table, nil return table, nil
} }
func formRecords(operations []*astOperation, resource, bkt string) ([]*eacl.Record, error) { func formRecords(operations []*astOperation, resource *astResource) ([]*eacl.Record, error) {
var res []*eacl.Record var res []*eacl.Record
for _, astOp := range operations { for _, astOp := range operations {
@ -695,9 +732,15 @@ func formRecords(operations []*astOperation, resource, bkt string) ([]*eacl.Reco
eacl.AddFormedTarget(record, eacl.RoleUser, (ecdsa.PublicKey)(*pk)) eacl.AddFormedTarget(record, eacl.RoleUser, (ecdsa.PublicKey)(*pk))
} }
} }
if resource != bkt { if len(resource.Object) != 0 {
trimmedName := strings.TrimPrefix(resource, bkt+"/") if len(resource.Version) != 0 {
record.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, trimmedName) oid := object.NewID()
if err := oid.Parse(resource.Version); err != nil {
return nil, err
}
record.AddObjectIDFilter(eacl.MatchStringEqual, oid)
}
record.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, resource.Object)
} }
res = append(res, record) res = append(res, record)
} }
@ -756,14 +799,15 @@ func policyToAst(bktPolicy *bucketPolicy) (*ast, error) {
trimmedResource := strings.TrimPrefix(resource, arnAwsPrefix) trimmedResource := strings.TrimPrefix(resource, arnAwsPrefix)
r, ok := rr[trimmedResource] r, ok := rr[trimmedResource]
if !ok { if !ok {
r = &astResource{ r = &astResource{resourceInfo: resourceInfo{Bucket: bktPolicy.Bucket}}
Name: trimmedResource, if trimmedResource != bktPolicy.Bucket {
r.Object = strings.TrimPrefix(trimmedResource, bktPolicy.Bucket+"/")
} }
} }
for _, action := range state.Action { for _, action := range state.Action {
for _, op := range actionToOpMap[action] { for _, op := range actionToOpMap[action] {
toAction := effectToAction(state.Effect) toAction := effectToAction(state.Effect)
r.Operations = addTo(r.Operations, state, op, role, toAction) r.Operations = addTo(r.Operations, state.Principal.CanonicalUser, op, role, toAction)
} }
} }
@ -782,9 +826,12 @@ func astToPolicy(ast *ast) *bucketPolicy {
bktPolicy := &bucketPolicy{} bktPolicy := &bucketPolicy{}
for _, resource := range ast.Resources { for _, resource := range ast.Resources {
if len(resource.Version) == 0 {
continue
}
allowed, denied := triageOperations(resource.Operations) allowed, denied := triageOperations(resource.Operations)
handleResourceOperations(bktPolicy, allowed, eacl.ActionAllow, resource.Name) handleResourceOperations(bktPolicy, allowed, eacl.ActionAllow, resource.Name())
handleResourceOperations(bktPolicy, denied, eacl.ActionDeny, resource.Name) handleResourceOperations(bktPolicy, denied, eacl.ActionDeny, resource.Name())
} }
return bktPolicy return bktPolicy
@ -845,7 +892,7 @@ func triageOperations(operations []*astOperation) ([]*astOperation, []*astOperat
return allowed, denied return allowed, denied
} }
func addTo(list []*astOperation, state statement, op eacl.Operation, role eacl.Role, action eacl.Action) []*astOperation { func addTo(list []*astOperation, userID string, op eacl.Operation, role eacl.Role, action eacl.Action) []*astOperation {
var found *astOperation var found *astOperation
for _, astop := range list { for _, astop := range list {
if astop.Op == op && astop.Role == role { if astop.Op == op && astop.Role == role {
@ -855,7 +902,7 @@ func addTo(list []*astOperation, state statement, op eacl.Operation, role eacl.R
if found != nil { if found != nil {
if role == eacl.RoleUser { if role == eacl.RoleUser {
found.Users = append(found.Users, state.Principal.CanonicalUser) found.Users = append(found.Users, userID)
} }
} else { } else {
astoperation := &astOperation{ astoperation := &astOperation{
@ -864,7 +911,7 @@ func addTo(list []*astOperation, state statement, op eacl.Operation, role eacl.R
Action: action, Action: action,
} }
if role == eacl.RoleUser { if role == eacl.RoleUser {
astoperation.Users = append(astoperation.Users, state.Principal.CanonicalUser) astoperation.Users = append(astoperation.Users, userID)
} }
list = append(list, astoperation) list = append(list, astoperation)
@ -873,13 +920,56 @@ func addTo(list []*astOperation, state statement, op eacl.Operation, role eacl.R
return list return list
} }
func aclToPolicy(acl *AccessControlPolicy) (*bucketPolicy, error) { func aclToAst(acl *AccessControlPolicy, resInfo *resourceInfo) (*ast, error) {
if acl.Resource == "" { res := &ast{}
return nil, fmt.Errorf("resource must not be empty")
resource := &astResource{resourceInfo: *resInfo}
ops := readOps
if resInfo.IsBucket() {
ops = append(ops, writeOps...)
}
for _, op := range ops {
operation := &astOperation{
Users: []string{acl.Owner.ID},
Role: eacl.RoleUser,
Op: op,
Action: eacl.ActionAllow,
}
resource.Operations = append(resource.Operations, operation)
}
for _, grant := range acl.AccessControlList {
if grant.Grantee.Type == acpAmazonCustomerByEmail || (grant.Grantee.Type == acpGroup && grant.Grantee.URI != allUsersGroup) {
return nil, fmt.Errorf("unsupported grantee: %v", grant.Grantee)
}
role := eacl.RoleUser
if grant.Grantee.Type == acpGroup {
role = eacl.RoleOthers
} else if grant.Grantee.ID == acl.Owner.ID {
continue
}
for _, action := range getActions(grant.Permission, resInfo.IsBucket()) {
for _, op := range actionToOpMap[action] {
resource.Operations = addTo(resource.Operations, grant.Grantee.ID, op, role, eacl.ActionAllow)
}
}
}
res.Resources = []*astResource{resource}
return res, nil
}
func aclToPolicy(acl *AccessControlPolicy, resInfo *resourceInfo) (*bucketPolicy, error) {
if resInfo.Bucket == "" {
return nil, fmt.Errorf("resource bucket must not be empty")
} }
results := []statement{ results := []statement{
getAllowStatement(acl, acl.Owner.ID, aclFullControl), getAllowStatement(resInfo, acl.Owner.ID, aclFullControl),
} }
for _, grant := range acl.AccessControlList { for _, grant := range acl.AccessControlList {
@ -893,7 +983,7 @@ func aclToPolicy(acl *AccessControlPolicy) (*bucketPolicy, error) {
} else if user == acl.Owner.ID { } else if user == acl.Owner.ID {
continue continue
} }
results = append(results, getAllowStatement(acl, user, grant.Permission)) results = append(results, getAllowStatement(resInfo, user, grant.Permission))
} }
return &bucketPolicy{ return &bucketPolicy{
@ -901,14 +991,14 @@ func aclToPolicy(acl *AccessControlPolicy) (*bucketPolicy, error) {
}, nil }, nil
} }
func getAllowStatement(acl *AccessControlPolicy, id string, permission AWSACL) statement { func getAllowStatement(resInfo *resourceInfo, id string, permission AWSACL) statement {
state := statement{ state := statement{
Effect: "Allow", Effect: "Allow",
Principal: principal{ Principal: principal{
CanonicalUser: id, CanonicalUser: id,
}, },
Action: getActions(permission, acl.IsBucket), Action: getActions(permission, resInfo.IsBucket()),
Resource: []string{arnAwsPrefix + acl.Resource}, Resource: []string{arnAwsPrefix + resInfo.Name()},
} }
if id == allUsersWildcard { if id == allUsersWildcard {
@ -1081,8 +1171,8 @@ func contains(list []eacl.Operation, op eacl.Operation) bool {
type getRecordFunc func(op eacl.Operation) *eacl.Record type getRecordFunc func(op eacl.Operation) *eacl.Record
func bucketACLToTable(acp *AccessControlPolicy) (*eacl.Table, error) { func bucketACLToTable(acp *AccessControlPolicy, resInfo *resourceInfo) (*eacl.Table, error) {
if !acp.IsBucket { if !resInfo.IsBucket() {
return nil, fmt.Errorf("allowed only bucket acl") return nil, fmt.Errorf("allowed only bucket acl")
} }

View file

@ -3,7 +3,10 @@ package handler
import ( import (
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"encoding/hex" "encoding/hex"
"io"
"net/http" "net/http"
"testing" "testing"
@ -16,6 +19,12 @@ import (
) )
func TestTableToAst(t *testing.T) { func TestTableToAst(t *testing.T) {
b := make([]byte, 32)
_, err := io.ReadFull(rand.Reader, b)
require.NoError(t, err)
oid := object.NewID()
oid.SetSHA256(sha256.Sum256(b))
key, err := keys.NewPrivateKey() key, err := keys.NewPrivateKey()
require.NoError(t, err) require.NoError(t, err)
key2, err := keys.NewPrivateKey() key2, err := keys.NewPrivateKey()
@ -33,19 +42,24 @@ func TestTableToAst(t *testing.T) {
eacl.AddFormedTarget(record2, eacl.RoleUser, *(*ecdsa.PublicKey)(key.PublicKey())) eacl.AddFormedTarget(record2, eacl.RoleUser, *(*ecdsa.PublicKey)(key.PublicKey()))
eacl.AddFormedTarget(record2, eacl.RoleUser, *(*ecdsa.PublicKey)(key2.PublicKey())) eacl.AddFormedTarget(record2, eacl.RoleUser, *(*ecdsa.PublicKey)(key2.PublicKey()))
record2.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, "objectName") record2.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, "objectName")
record2.AddObjectIDFilter(eacl.MatchStringEqual, oid)
table.AddRecord(record2) table.AddRecord(record2)
expectedAst := &ast{ expectedAst := &ast{
Resources: []*astResource{ Resources: []*astResource{
{ {
Name: "bucketName", resourceInfo: resourceInfo{Bucket: "bucketName"},
Operations: []*astOperation{{ Operations: []*astOperation{{
Role: eacl.RoleOthers, Role: eacl.RoleOthers,
Op: eacl.OperationGet, Op: eacl.OperationGet,
Action: eacl.ActionAllow, Action: eacl.ActionAllow,
}}}, }}},
{ {
Name: "objectName", resourceInfo: resourceInfo{
Bucket: "bucketName",
Object: "objectName",
Version: oid.String(),
},
Operations: []*astOperation{{ Operations: []*astOperation{{
Users: []string{ Users: []string{
hex.EncodeToString(key.PublicKey().Bytes()), hex.EncodeToString(key.PublicKey().Bytes()),
@ -58,9 +72,9 @@ func TestTableToAst(t *testing.T) {
}, },
} }
actualAst := tableToAst(table, expectedAst.Resources[0].Name) actualAst := tableToAst(table, expectedAst.Resources[0].Bucket)
if actualAst.Resources[0].Name == expectedAst.Resources[0].Name { if actualAst.Resources[0].Name() == expectedAst.Resources[0].Name() {
require.Equal(t, expectedAst, actualAst) require.Equal(t, expectedAst, actualAst)
} else { } else {
require.Equal(t, len(expectedAst.Resources), len(actualAst.Resources)) require.Equal(t, len(expectedAst.Resources), len(actualAst.Resources))
@ -90,11 +104,14 @@ func TestPolicyToAst(t *testing.T) {
Resource: []string{"arn:aws:s3:::bucketName/object"}, Resource: []string{"arn:aws:s3:::bucketName/object"},
}}, }},
} }
policy.Bucket = "bucketName"
expectedAst := &ast{ expectedAst := &ast{
Resources: []*astResource{ Resources: []*astResource{
{ {
Name: "bucketName", resourceInfo: resourceInfo{
Bucket: "bucketName",
},
Operations: []*astOperation{{ Operations: []*astOperation{{
Role: eacl.RoleOthers, Role: eacl.RoleOthers,
Op: eacl.OperationPut, Op: eacl.OperationPut,
@ -102,7 +119,10 @@ func TestPolicyToAst(t *testing.T) {
}}, }},
}, },
{ {
Name: "bucketName/object", resourceInfo: resourceInfo{
Bucket: "bucketName",
Object: "object",
},
Operations: getReadOps(key, eacl.RoleUser, eacl.ActionDeny), Operations: getReadOps(key, eacl.RoleUser, eacl.ActionDeny),
}, },
}, },
@ -111,7 +131,7 @@ func TestPolicyToAst(t *testing.T) {
actualAst, err := policyToAst(policy) actualAst, err := policyToAst(policy)
require.NoError(t, err) require.NoError(t, err)
if actualAst.Resources[0].Name == expectedAst.Resources[0].Name { if actualAst.Resources[0].Name() == expectedAst.Resources[0].Name() {
require.Equal(t, expectedAst, actualAst) require.Equal(t, expectedAst, actualAst)
} else { } else {
require.Equal(t, len(expectedAst.Resources), len(actualAst.Resources)) require.Equal(t, len(expectedAst.Resources), len(actualAst.Resources))
@ -142,7 +162,10 @@ func TestMergeAstUnModified(t *testing.T) {
child := &ast{ child := &ast{
Resources: []*astResource{ Resources: []*astResource{
{ {
Name: "objectName", resourceInfo: resourceInfo{
Bucket: "bucket",
Object: "objectName",
},
Operations: []*astOperation{{ Operations: []*astOperation{{
Users: []string{hex.EncodeToString(key.PublicKey().Bytes())}, Users: []string{hex.EncodeToString(key.PublicKey().Bytes())},
Role: eacl.RoleUser, Role: eacl.RoleUser,
@ -156,7 +179,9 @@ func TestMergeAstUnModified(t *testing.T) {
parent := &ast{ parent := &ast{
Resources: []*astResource{ Resources: []*astResource{
{ {
Name: "bucket", resourceInfo: resourceInfo{
Bucket: "bucket",
},
Operations: []*astOperation{{ Operations: []*astOperation{{
Role: eacl.RoleOthers, Role: eacl.RoleOthers,
Op: eacl.OperationGet, Op: eacl.OperationGet,
@ -176,7 +201,10 @@ func TestMergeAstModified(t *testing.T) {
child := &ast{ child := &ast{
Resources: []*astResource{ Resources: []*astResource{
{ {
Name: "objectName", resourceInfo: resourceInfo{
Bucket: "bucket",
Object: "objectName",
},
Operations: []*astOperation{{ Operations: []*astOperation{{
Role: eacl.RoleOthers, Role: eacl.RoleOthers,
Op: eacl.OperationPut, Op: eacl.OperationPut,
@ -194,7 +222,10 @@ func TestMergeAstModified(t *testing.T) {
parent := &ast{ parent := &ast{
Resources: []*astResource{ Resources: []*astResource{
{ {
Name: "objectName", resourceInfo: resourceInfo{
Bucket: "bucket",
Object: "objectName",
},
Operations: []*astOperation{{ Operations: []*astOperation{{
Users: []string{"user1"}, Users: []string{"user1"},
Role: eacl.RoleUser, Role: eacl.RoleUser,
@ -208,7 +239,10 @@ func TestMergeAstModified(t *testing.T) {
expected := &ast{ expected := &ast{
Resources: []*astResource{ Resources: []*astResource{
{ {
Name: "objectName", resourceInfo: resourceInfo{
Bucket: "bucket",
Object: "objectName",
},
Operations: []*astOperation{ Operations: []*astOperation{
child.Resources[0].Operations[0], child.Resources[0].Operations[0],
{ {
@ -231,7 +265,10 @@ func TestMergeAstModifiedConflict(t *testing.T) {
child := &ast{ child := &ast{
Resources: []*astResource{ Resources: []*astResource{
{ {
Name: "objectName", resourceInfo: resourceInfo{
Bucket: "bucket",
Object: "objectName",
},
Operations: []*astOperation{{ Operations: []*astOperation{{
Users: []string{"user1"}, Users: []string{"user1"},
Role: eacl.RoleUser, Role: eacl.RoleUser,
@ -250,7 +287,10 @@ func TestMergeAstModifiedConflict(t *testing.T) {
parent := &ast{ parent := &ast{
Resources: []*astResource{ Resources: []*astResource{
{ {
Name: "objectName", resourceInfo: resourceInfo{
Bucket: "bucket",
Object: "objectName",
},
Operations: []*astOperation{{ Operations: []*astOperation{{
Users: []string{"user1"}, Users: []string{"user1"},
Role: eacl.RoleUser, Role: eacl.RoleUser,
@ -274,7 +314,10 @@ func TestMergeAstModifiedConflict(t *testing.T) {
expected := &ast{ expected := &ast{
Resources: []*astResource{ Resources: []*astResource{
{ {
Name: "objectName", resourceInfo: resourceInfo{
Bucket: "bucket",
Object: "objectName",
},
Operations: []*astOperation{ Operations: []*astOperation{
{ {
Users: []string{"user2", "user1"}, Users: []string{"user2", "user1"},
@ -304,7 +347,9 @@ func TestAstToTable(t *testing.T) {
ast := &ast{ ast := &ast{
Resources: []*astResource{ Resources: []*astResource{
{ {
Name: "bucketName", resourceInfo: resourceInfo{
Bucket: "bucketName",
},
Operations: []*astOperation{{ Operations: []*astOperation{{
Users: []string{hex.EncodeToString(key.PublicKey().Bytes())}, Users: []string{hex.EncodeToString(key.PublicKey().Bytes())},
Role: eacl.RoleUser, Role: eacl.RoleUser,
@ -313,7 +358,10 @@ func TestAstToTable(t *testing.T) {
}}, }},
}, },
{ {
Name: "bucketName/objectName", resourceInfo: resourceInfo{
Bucket: "bucketName",
Object: "objectName",
},
Operations: []*astOperation{{ Operations: []*astOperation{{
Role: eacl.RoleOthers, Role: eacl.RoleOthers,
Op: eacl.OperationGet, Op: eacl.OperationGet,
@ -336,14 +384,16 @@ func TestAstToTable(t *testing.T) {
record2.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, "objectName") record2.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, "objectName")
expectedTable.AddRecord(record2) expectedTable.AddRecord(record2)
actualTable, err := astToTable(ast, "bucketName") actualTable, err := astToTable(ast)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expectedTable, actualTable) require.Equal(t, expectedTable, actualTable)
} }
func TestRemoveUsers(t *testing.T) { func TestRemoveUsers(t *testing.T) {
resource := &astResource{ resource := &astResource{
Name: "name", resourceInfo: resourceInfo{
Bucket: "bucket",
},
Operations: []*astOperation{{ Operations: []*astOperation{{
Users: []string{"user1", "user3"}, Users: []string{"user1", "user3"},
Role: eacl.RoleUser, Role: eacl.RoleUser,
@ -361,7 +411,7 @@ func TestRemoveUsers(t *testing.T) {
removeUsers(resource, op, []string{"user1", "user2"}) removeUsers(resource, op, []string{"user1", "user2"})
require.Equal(t, len(resource.Operations), 1) require.Equal(t, len(resource.Operations), 1)
require.Equal(t, resource.Name, resource.Name) require.Equal(t, resource.Name(), resource.Name())
require.Equal(t, resource.Operations[0].Users, []string{"user3"}) require.Equal(t, resource.Operations[0].Users, []string{"user3"})
} }
@ -392,8 +442,10 @@ func TestBucketAclToPolicy(t *testing.T) {
}, },
Permission: aclWrite, Permission: aclWrite,
}}, }},
Resource: "bucketName", }
IsBucket: true,
resInfo := &resourceInfo{
Bucket: "bucketName",
} }
expectedPolicy := &bucketPolicy{ expectedPolicy := &bucketPolicy{
@ -404,24 +456,24 @@ func TestBucketAclToPolicy(t *testing.T) {
CanonicalUser: id, CanonicalUser: id,
}, },
Action: []string{"s3:ListBucket", "s3:ListBucketVersions", "s3:ListBucketMultipartUploads", "s3:PutObject", "s3:DeleteObject"}, Action: []string{"s3:ListBucket", "s3:ListBucketVersions", "s3:ListBucketMultipartUploads", "s3:PutObject", "s3:DeleteObject"},
Resource: []string{arnAwsPrefix + acl.Resource}, Resource: []string{arnAwsPrefix + resInfo.Name()},
}, { }, {
Effect: "Allow", Effect: "Allow",
Principal: principal{AWS: allUsersWildcard}, Principal: principal{AWS: allUsersWildcard},
Action: []string{"s3:ListBucket", "s3:ListBucketVersions", "s3:ListBucketMultipartUploads"}, Action: []string{"s3:ListBucket", "s3:ListBucketVersions", "s3:ListBucketMultipartUploads"},
Resource: []string{arnAwsPrefix + acl.Resource}, Resource: []string{arnAwsPrefix + resInfo.Name()},
}, { }, {
Effect: "Allow", Effect: "Allow",
Principal: principal{ Principal: principal{
CanonicalUser: id2, CanonicalUser: id2,
}, },
Action: []string{"s3:PutObject", "s3:DeleteObject"}, Action: []string{"s3:PutObject", "s3:DeleteObject"},
Resource: []string{arnAwsPrefix + acl.Resource}, Resource: []string{arnAwsPrefix + resInfo.Name()},
}, },
}, },
} }
actualPolicy, err := aclToPolicy(acl) actualPolicy, err := aclToPolicy(acl, resInfo)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expectedPolicy, actualPolicy) require.Equal(t, expectedPolicy, actualPolicy)
} }
@ -459,8 +511,11 @@ func TestObjectAclToPolicy(t *testing.T) {
}, },
Permission: aclRead, Permission: aclRead,
}}, }},
Resource: "bucketName/object", }
IsBucket: false,
resInfo := &resourceInfo{
Bucket: "bucketName",
Object: "object",
} }
expectedPolicy := &bucketPolicy{ expectedPolicy := &bucketPolicy{
@ -471,7 +526,7 @@ func TestObjectAclToPolicy(t *testing.T) {
CanonicalUser: id, CanonicalUser: id,
}, },
Action: []string{"s3:GetObject", "s3:GetObjectVersion"}, Action: []string{"s3:GetObject", "s3:GetObjectVersion"},
Resource: []string{arnAwsPrefix + acl.Resource}, Resource: []string{arnAwsPrefix + resInfo.Name()},
}, },
{ {
Effect: "Allow", Effect: "Allow",
@ -479,17 +534,17 @@ func TestObjectAclToPolicy(t *testing.T) {
CanonicalUser: id2, CanonicalUser: id2,
}, },
Action: []string{"s3:GetObject", "s3:GetObjectVersion"}, Action: []string{"s3:GetObject", "s3:GetObjectVersion"},
Resource: []string{arnAwsPrefix + acl.Resource}, Resource: []string{arnAwsPrefix + resInfo.Name()},
}, { }, {
Effect: "Allow", Effect: "Allow",
Principal: principal{AWS: allUsersWildcard}, Principal: principal{AWS: allUsersWildcard},
Action: []string{"s3:GetObject", "s3:GetObjectVersion"}, Action: []string{"s3:GetObject", "s3:GetObjectVersion"},
Resource: []string{arnAwsPrefix + acl.Resource}, Resource: []string{arnAwsPrefix + resInfo.Name()},
}, },
}, },
} }
actualPolicy, err := aclToPolicy(acl) actualPolicy, err := aclToPolicy(acl, resInfo)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expectedPolicy, actualPolicy) require.Equal(t, expectedPolicy, actualPolicy)
} }
@ -674,8 +729,6 @@ func TestBucketAclToTable(t *testing.T) {
}, },
Permission: aclWrite, Permission: aclWrite,
}}, }},
Resource: "bucketName",
IsBucket: true,
} }
expectedTable := new(eacl.Table) expectedTable := new(eacl.Table)
@ -691,8 +744,164 @@ func TestBucketAclToTable(t *testing.T) {
for _, op := range fullOps { for _, op := range fullOps {
expectedTable.AddRecord(getOthersRecord(op, eacl.ActionDeny)) expectedTable.AddRecord(getOthersRecord(op, eacl.ActionDeny))
} }
resInfo := &resourceInfo{
Bucket: "bucketName",
}
actualTable, err := bucketACLToTable(acl) actualTable, err := bucketACLToTable(acl, resInfo)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expectedTable.Records(), actualTable.Records()) require.Equal(t, expectedTable.Records(), actualTable.Records())
} }
func TestObjectAclToAst(t *testing.T) {
b := make([]byte, 32)
_, err := io.ReadFull(rand.Reader, b)
require.NoError(t, err)
oid := object.NewID()
oid.SetSHA256(sha256.Sum256(b))
key, err := keys.NewPrivateKey()
require.NoError(t, err)
key2, err := keys.NewPrivateKey()
require.NoError(t, err)
id := hex.EncodeToString(key.PublicKey().Bytes())
id2 := hex.EncodeToString(key2.PublicKey().Bytes())
acl := &AccessControlPolicy{
Owner: Owner{
ID: id,
DisplayName: "user1",
},
AccessControlList: []*Grant{{
Grantee: &Grantee{
ID: id,
Type: acpCanonicalUser,
},
Permission: aclFullControl,
}, {
Grantee: &Grantee{
ID: id2,
Type: acpCanonicalUser,
},
Permission: aclRead,
},
},
}
resInfo := &resourceInfo{
Bucket: "bucketName",
Object: "object",
Version: oid.String(),
}
var operations []*astOperation
for _, op := range readOps {
astOp := &astOperation{Users: []string{
hex.EncodeToString(key.PublicKey().Bytes()),
hex.EncodeToString(key2.PublicKey().Bytes()),
},
Role: eacl.RoleUser,
Op: op,
Action: eacl.ActionAllow,
}
operations = append(operations, astOp)
}
expectedAst := &ast{
Resources: []*astResource{
{
resourceInfo: *resInfo,
Operations: operations,
},
},
}
actualAst, err := aclToAst(acl, resInfo)
require.NoError(t, err)
require.Equal(t, expectedAst, actualAst)
}
func TestBucketAclToAst(t *testing.T) {
b := make([]byte, 32)
_, err := io.ReadFull(rand.Reader, b)
require.NoError(t, err)
oid := object.NewID()
oid.SetSHA256(sha256.Sum256(b))
key, err := keys.NewPrivateKey()
require.NoError(t, err)
key2, err := keys.NewPrivateKey()
require.NoError(t, err)
id := hex.EncodeToString(key.PublicKey().Bytes())
id2 := hex.EncodeToString(key2.PublicKey().Bytes())
acl := &AccessControlPolicy{
Owner: Owner{
ID: id,
DisplayName: "user1",
},
AccessControlList: []*Grant{
{
Grantee: &Grantee{
ID: id2,
Type: acpCanonicalUser,
},
Permission: aclWrite,
}, {
Grantee: &Grantee{
URI: allUsersGroup,
Type: acpGroup,
},
Permission: aclRead,
},
},
}
var operations []*astOperation
for _, op := range readOps {
astOp := &astOperation{Users: []string{
hex.EncodeToString(key.PublicKey().Bytes()),
},
Role: eacl.RoleUser,
Op: op,
Action: eacl.ActionAllow,
}
operations = append(operations, astOp)
}
for _, op := range writeOps {
astOp := &astOperation{Users: []string{
hex.EncodeToString(key.PublicKey().Bytes()),
hex.EncodeToString(key2.PublicKey().Bytes()),
},
Role: eacl.RoleUser,
Op: op,
Action: eacl.ActionAllow,
}
operations = append(operations, astOp)
}
for _, op := range readOps {
astOp := &astOperation{
Role: eacl.RoleOthers,
Op: op,
Action: eacl.ActionAllow,
}
operations = append(operations, astOp)
}
resInfo := &resourceInfo{Bucket: "bucketName"}
expectedAst := &ast{
Resources: []*astResource{
{
resourceInfo: *resInfo,
Operations: operations,
},
},
}
actualAst, err := aclToAst(acl, resInfo)
require.NoError(t, err)
require.Equal(t, expectedAst, actualAst)
}

View file

@ -38,47 +38,6 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if containsACLHeaders(r) {
objectACL, err := parseACLHeaders(r)
if err != nil {
h.logAndSendError(w, "could not parse object acl", reqInfo, err)
return
}
objectACL.Resource = reqInfo.BucketName + "/" + reqInfo.ObjectName
bktPolicy, err := aclToPolicy(objectACL)
if err != nil {
h.logAndSendError(w, "could not translate object acl to bucket policy", reqInfo, err)
return
}
astChild, err := policyToAst(bktPolicy)
if err != nil {
h.logAndSendError(w, "could not translate policy to ast", reqInfo, err)
return
}
bacl, err := h.obj.GetBucketACL(r.Context(), reqInfo.BucketName)
if err != nil {
h.logAndSendError(w, "could not get bucket eacl", reqInfo, err)
return
}
parentAst := tableToAst(bacl.EACL, reqInfo.BucketName)
for _, resource := range parentAst.Resources {
if resource.Name == bacl.Info.CID.String() {
resource.Name = reqInfo.BucketName
}
}
if resAst, updated := mergeAst(parentAst, astChild); updated {
if newEaclTable, err = astToTable(resAst, reqInfo.BucketName); err != nil {
h.logAndSendError(w, "could not translate ast to table", reqInfo, err)
return
}
}
}
bktInfo, err := h.obj.GetBucketInfo(r.Context(), reqInfo.BucketName) bktInfo, err := h.obj.GetBucketInfo(r.Context(), reqInfo.BucketName)
if err != nil { if err != nil {
h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) h.logAndSendError(w, "could not get bucket eacl", reqInfo, err)
@ -108,6 +67,52 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if containsACLHeaders(r) {
objectACL, err := parseACLHeaders(r)
if err != nil {
h.logAndSendError(w, "could not parse object acl", reqInfo, err)
return
}
resInfo := &resourceInfo{
Bucket: reqInfo.BucketName,
Object: reqInfo.ObjectName,
Version: info.Version(),
}
bktPolicy, err := aclToPolicy(objectACL, resInfo)
if err != nil {
h.logAndSendError(w, "could not translate object acl to bucket policy", reqInfo, err)
return
}
astChild, err := policyToAst(bktPolicy)
if err != nil {
h.logAndSendError(w, "could not translate policy to ast", reqInfo, err)
return
}
bacl, err := h.obj.GetBucketACL(r.Context(), reqInfo.BucketName)
if err != nil {
h.logAndSendError(w, "could not get bucket eacl", reqInfo, err)
return
}
parentAst := tableToAst(bacl.EACL, reqInfo.BucketName)
for _, resource := range parentAst.Resources {
if resource.Bucket == bacl.Info.CID.String() {
resource.Bucket = reqInfo.BucketName
}
}
if resAst, updated := mergeAst(parentAst, astChild); updated {
if newEaclTable, err = astToTable(resAst); err != nil {
h.logAndSendError(w, "could not translate ast to table", reqInfo, err)
return
}
}
}
if tagSet != nil { if tagSet != nil {
if err = h.obj.PutObjectTagging(r.Context(), &layer.PutTaggingParams{ObjectInfo: info, TagSet: tagSet}); err != nil { if err = h.obj.PutObjectTagging(r.Context(), &layer.PutTaggingParams{ObjectInfo: info, TagSet: tagSet}); err != nil {
h.logAndSendError(w, "could not upload object tagging", reqInfo, err) h.logAndSendError(w, "could not upload object tagging", reqInfo, err)
@ -191,9 +196,9 @@ func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "could not parse bucket acl", reqInfo, err) h.logAndSendError(w, "could not parse bucket acl", reqInfo, err)
return return
} }
bktACL.IsBucket = true resInfo := &resourceInfo{Bucket: reqInfo.BucketName}
p.EACL, err = bucketACLToTable(bktACL) p.EACL, err = bucketACLToTable(bktACL, resInfo)
if err != nil { if err != nil {
h.logAndSendError(w, "could translate bucket acl to eacl", reqInfo, err) h.logAndSendError(w, "could translate bucket acl to eacl", reqInfo, err)
return return

View file

@ -57,8 +57,6 @@ type AccessControlPolicy struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ AccessControlPolicy" json:"-"` XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ AccessControlPolicy" json:"-"`
Owner Owner Owner Owner
AccessControlList []*Grant `xml:"AccessControlList>Grant"` AccessControlList []*Grant `xml:"AccessControlList>Grant"`
Resource string `xml:"-"`
IsBucket bool `xml:"-"`
} }
// Grant is container for Grantee data. // Grant is container for Grantee data.