From a9be642eafa8367e85aa9d0b6293af33a57178b3 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Mon, 30 Aug 2021 10:55:42 +0300 Subject: [PATCH] [#213] Add object acl versioning Signed-off-by: Denis Kirillov --- api/handler/acl.go | 208 +++++++++++++++++++++--------- api/handler/acl_test.go | 279 +++++++++++++++++++++++++++++++++++----- api/handler/put.go | 91 ++++++------- api/handler/response.go | 2 - 4 files changed, 441 insertions(+), 139 deletions(-) diff --git a/api/handler/acl.go b/api/handler/acl.go index b5b244b8..86556552 100644 --- a/api/handler/acl.go +++ b/api/handler/acl.go @@ -12,6 +12,7 @@ import ( "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/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/cache" "github.com/nspcc-dev/neofs-s3-gw/api/errors" @@ -69,6 +70,7 @@ type bucketPolicy struct { Version string `json:"Version"` ID string `json:"Id"` Statement []statement `json:"Statement"` + Bucket string `json:"-"` } type statement struct { @@ -89,10 +91,30 @@ type ast struct { } type astResource struct { - Name string + resourceInfo 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 { Users []string Role eacl.Role @@ -137,27 +159,20 @@ func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) { return } - list.Resource = reqInfo.BucketName - list.IsBucket = true - - bktPolicy, err := aclToPolicy(list) + resInfo := &resourceInfo{Bucket: reqInfo.BucketName} + astBucket, err := aclToAst(list, resInfo) if err != nil { h.logAndSendError(w, "could not translate acl to policy", reqInfo, err) 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) return } } -func (h *handler) updateBucketACL(r *http.Request, bktPolicy *bucketPolicy, bkt string) error { - astChild, err := policyToAst(bktPolicy) - if err != nil { - return fmt.Errorf("could not translate policy to ast: %w", err) - } - +func (h *handler) updateBucketACL(r *http.Request, astChild *ast, bkt string) error { bucketACL, err := h.obj.GetBucketACL(r.Context(), bkt) if err != nil { 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) for _, resource := range parentAst.Resources { - if resource.Name == bucketACL.Info.CID.String() { - resource.Name = bkt + if resource.Bucket == bucketACL.Info.CID.String() { + resource.Bucket = bkt } } @@ -179,7 +194,7 @@ func (h *handler) updateBucketACL(r *http.Request, bktPolicy *bucketPolicy, bkt return nil } - table, err := astToTable(resAst, bkt) + table, err := astToTable(resAst) if err != nil { return fmt.Errorf("could not translate ast to table: %w", err) } @@ -216,8 +231,9 @@ func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) { var ( - err error - reqInfo = api.GetReqInfo(r.Context()) + err error + reqInfo = api.GetReqInfo(r.Context()) + versionID = reqInfo.URL.Query().Get(api.QueryVersionID) ) list := &AccessControlPolicy{} @@ -232,18 +248,22 @@ func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) { 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 { - h.logAndSendError(w, "could not translate acl to policy", reqInfo, err) + h.logAndSendError(w, "could not translate acl to ast", reqInfo, err) return } p := &layer.HeadObjectParams{ Bucket: reqInfo.BucketName, Object: reqInfo.ObjectName, - VersionID: reqInfo.URL.Query().Get(api.QueryVersionID), + VersionID: versionID, } 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 } - 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) return } @@ -295,13 +315,19 @@ func checkOwner(info *cache.BucketInfo, owner string) error { func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { reqInfo := api.GetReqInfo(r.Context()) - bktPolicy := &bucketPolicy{} + bktPolicy := &bucketPolicy{Bucket: reqInfo.BucketName} if err := json.NewDecoder(r.Body).Decode(bktPolicy); err != nil { h.logAndSendError(w, "could not parse bucket policy", reqInfo, err) 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) return } @@ -465,22 +491,32 @@ func tableToAst(table *eacl.Table, bktName string) *ast { rr := make(map[string]*astResource) for _, record := range table.Records() { - resname := bktName + resName := bktName + var objectName string + var version string for _, filter := range record.Filters() { - if filter.Matcher() == eacl.MatchStringEqual && filter.Key() == object.AttributeFileName { - resname = filter.Value() + if filter.Matcher() == eacl.MatchStringEqual { + 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 { - r = &astResource{ - Name: resname, - } + r = &astResource{resourceInfo: resourceInfo{ + Bucket: bktName, + Object: objectName, + Version: version, + }} } for _, target := range record.Targets() { r.Operations = addToList(r.Operations, record, target) } - rr[resname] = r + rr[resName] = r } for _, val := range rr { @@ -493,7 +529,7 @@ func tableToAst(table *eacl.Table, bktName string) *ast { func mergeAst(parent, child *ast) (*ast, bool) { updated := false for _, resource := range child.Resources { - parentResource := getParentResource(parent, resource.Name) + parentResource := getParentResource(parent, resource) if parentResource == nil { parent.Resources = append(parent.Resources, resource) 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 { - if resource == parentResource.Name { + if resource.Bucket == parentResource.Bucket && resource.Object == parentResource.Object && + resource.Version == parentResource.Version { return parentResource } } return nil } -func astToTable(ast *ast, bkt string) (*eacl.Table, error) { +func astToTable(ast *ast) (*eacl.Table, error) { table := eacl.NewTable() for _, resource := range ast.Resources { - records, err := formRecords(resource.Operations, resource.Name, bkt) + records, err := formRecords(resource.Operations, resource) if err != nil { return nil, err } @@ -677,7 +714,7 @@ func astToTable(ast *ast, bkt string) (*eacl.Table, error) { 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 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)) } } - if resource != bkt { - trimmedName := strings.TrimPrefix(resource, bkt+"/") - record.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, trimmedName) + if len(resource.Object) != 0 { + if len(resource.Version) != 0 { + 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) } @@ -756,14 +799,15 @@ func policyToAst(bktPolicy *bucketPolicy) (*ast, error) { trimmedResource := strings.TrimPrefix(resource, arnAwsPrefix) r, ok := rr[trimmedResource] if !ok { - r = &astResource{ - Name: trimmedResource, + r = &astResource{resourceInfo: resourceInfo{Bucket: bktPolicy.Bucket}} + if trimmedResource != bktPolicy.Bucket { + r.Object = strings.TrimPrefix(trimmedResource, bktPolicy.Bucket+"/") } } for _, action := range state.Action { for _, op := range actionToOpMap[action] { 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{} for _, resource := range ast.Resources { + if len(resource.Version) == 0 { + continue + } allowed, denied := triageOperations(resource.Operations) - handleResourceOperations(bktPolicy, allowed, eacl.ActionAllow, resource.Name) - handleResourceOperations(bktPolicy, denied, eacl.ActionDeny, resource.Name) + handleResourceOperations(bktPolicy, allowed, eacl.ActionAllow, resource.Name()) + handleResourceOperations(bktPolicy, denied, eacl.ActionDeny, resource.Name()) } return bktPolicy @@ -845,7 +892,7 @@ func triageOperations(operations []*astOperation) ([]*astOperation, []*astOperat 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 for _, astop := range list { 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 role == eacl.RoleUser { - found.Users = append(found.Users, state.Principal.CanonicalUser) + found.Users = append(found.Users, userID) } } else { astoperation := &astOperation{ @@ -864,7 +911,7 @@ func addTo(list []*astOperation, state statement, op eacl.Operation, role eacl.R Action: action, } if role == eacl.RoleUser { - astoperation.Users = append(astoperation.Users, state.Principal.CanonicalUser) + astoperation.Users = append(astoperation.Users, userID) } list = append(list, astoperation) @@ -873,13 +920,56 @@ func addTo(list []*astOperation, state statement, op eacl.Operation, role eacl.R return list } -func aclToPolicy(acl *AccessControlPolicy) (*bucketPolicy, error) { - if acl.Resource == "" { - return nil, fmt.Errorf("resource must not be empty") +func aclToAst(acl *AccessControlPolicy, resInfo *resourceInfo) (*ast, error) { + res := &ast{} + + 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{ - getAllowStatement(acl, acl.Owner.ID, aclFullControl), + getAllowStatement(resInfo, acl.Owner.ID, aclFullControl), } for _, grant := range acl.AccessControlList { @@ -893,7 +983,7 @@ func aclToPolicy(acl *AccessControlPolicy) (*bucketPolicy, error) { } else if user == acl.Owner.ID { continue } - results = append(results, getAllowStatement(acl, user, grant.Permission)) + results = append(results, getAllowStatement(resInfo, user, grant.Permission)) } return &bucketPolicy{ @@ -901,14 +991,14 @@ func aclToPolicy(acl *AccessControlPolicy) (*bucketPolicy, error) { }, nil } -func getAllowStatement(acl *AccessControlPolicy, id string, permission AWSACL) statement { +func getAllowStatement(resInfo *resourceInfo, id string, permission AWSACL) statement { state := statement{ Effect: "Allow", Principal: principal{ CanonicalUser: id, }, - Action: getActions(permission, acl.IsBucket), - Resource: []string{arnAwsPrefix + acl.Resource}, + Action: getActions(permission, resInfo.IsBucket()), + Resource: []string{arnAwsPrefix + resInfo.Name()}, } if id == allUsersWildcard { @@ -1081,8 +1171,8 @@ func contains(list []eacl.Operation, op eacl.Operation) bool { type getRecordFunc func(op eacl.Operation) *eacl.Record -func bucketACLToTable(acp *AccessControlPolicy) (*eacl.Table, error) { - if !acp.IsBucket { +func bucketACLToTable(acp *AccessControlPolicy, resInfo *resourceInfo) (*eacl.Table, error) { + if !resInfo.IsBucket() { return nil, fmt.Errorf("allowed only bucket acl") } diff --git a/api/handler/acl_test.go b/api/handler/acl_test.go index 207d9467..c732ce19 100644 --- a/api/handler/acl_test.go +++ b/api/handler/acl_test.go @@ -3,7 +3,10 @@ package handler import ( "context" "crypto/ecdsa" + "crypto/rand" + "crypto/sha256" "encoding/hex" + "io" "net/http" "testing" @@ -16,6 +19,12 @@ import ( ) 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() require.NoError(t, err) 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)(key2.PublicKey())) record2.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, "objectName") + record2.AddObjectIDFilter(eacl.MatchStringEqual, oid) table.AddRecord(record2) expectedAst := &ast{ Resources: []*astResource{ { - Name: "bucketName", + resourceInfo: resourceInfo{Bucket: "bucketName"}, Operations: []*astOperation{{ Role: eacl.RoleOthers, Op: eacl.OperationGet, Action: eacl.ActionAllow, }}}, { - Name: "objectName", + resourceInfo: resourceInfo{ + Bucket: "bucketName", + Object: "objectName", + Version: oid.String(), + }, Operations: []*astOperation{{ Users: []string{ 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) } else { 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"}, }}, } + policy.Bucket = "bucketName" expectedAst := &ast{ Resources: []*astResource{ { - Name: "bucketName", + resourceInfo: resourceInfo{ + Bucket: "bucketName", + }, Operations: []*astOperation{{ Role: eacl.RoleOthers, 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), }, }, @@ -111,7 +131,7 @@ func TestPolicyToAst(t *testing.T) { actualAst, err := policyToAst(policy) 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) } else { require.Equal(t, len(expectedAst.Resources), len(actualAst.Resources)) @@ -142,7 +162,10 @@ func TestMergeAstUnModified(t *testing.T) { child := &ast{ Resources: []*astResource{ { - Name: "objectName", + resourceInfo: resourceInfo{ + Bucket: "bucket", + Object: "objectName", + }, Operations: []*astOperation{{ Users: []string{hex.EncodeToString(key.PublicKey().Bytes())}, Role: eacl.RoleUser, @@ -156,7 +179,9 @@ func TestMergeAstUnModified(t *testing.T) { parent := &ast{ Resources: []*astResource{ { - Name: "bucket", + resourceInfo: resourceInfo{ + Bucket: "bucket", + }, Operations: []*astOperation{{ Role: eacl.RoleOthers, Op: eacl.OperationGet, @@ -176,7 +201,10 @@ func TestMergeAstModified(t *testing.T) { child := &ast{ Resources: []*astResource{ { - Name: "objectName", + resourceInfo: resourceInfo{ + Bucket: "bucket", + Object: "objectName", + }, Operations: []*astOperation{{ Role: eacl.RoleOthers, Op: eacl.OperationPut, @@ -194,7 +222,10 @@ func TestMergeAstModified(t *testing.T) { parent := &ast{ Resources: []*astResource{ { - Name: "objectName", + resourceInfo: resourceInfo{ + Bucket: "bucket", + Object: "objectName", + }, Operations: []*astOperation{{ Users: []string{"user1"}, Role: eacl.RoleUser, @@ -208,7 +239,10 @@ func TestMergeAstModified(t *testing.T) { expected := &ast{ Resources: []*astResource{ { - Name: "objectName", + resourceInfo: resourceInfo{ + Bucket: "bucket", + Object: "objectName", + }, Operations: []*astOperation{ child.Resources[0].Operations[0], { @@ -231,7 +265,10 @@ func TestMergeAstModifiedConflict(t *testing.T) { child := &ast{ Resources: []*astResource{ { - Name: "objectName", + resourceInfo: resourceInfo{ + Bucket: "bucket", + Object: "objectName", + }, Operations: []*astOperation{{ Users: []string{"user1"}, Role: eacl.RoleUser, @@ -250,7 +287,10 @@ func TestMergeAstModifiedConflict(t *testing.T) { parent := &ast{ Resources: []*astResource{ { - Name: "objectName", + resourceInfo: resourceInfo{ + Bucket: "bucket", + Object: "objectName", + }, Operations: []*astOperation{{ Users: []string{"user1"}, Role: eacl.RoleUser, @@ -274,7 +314,10 @@ func TestMergeAstModifiedConflict(t *testing.T) { expected := &ast{ Resources: []*astResource{ { - Name: "objectName", + resourceInfo: resourceInfo{ + Bucket: "bucket", + Object: "objectName", + }, Operations: []*astOperation{ { Users: []string{"user2", "user1"}, @@ -304,7 +347,9 @@ func TestAstToTable(t *testing.T) { ast := &ast{ Resources: []*astResource{ { - Name: "bucketName", + resourceInfo: resourceInfo{ + Bucket: "bucketName", + }, Operations: []*astOperation{{ Users: []string{hex.EncodeToString(key.PublicKey().Bytes())}, Role: eacl.RoleUser, @@ -313,7 +358,10 @@ func TestAstToTable(t *testing.T) { }}, }, { - Name: "bucketName/objectName", + resourceInfo: resourceInfo{ + Bucket: "bucketName", + Object: "objectName", + }, Operations: []*astOperation{{ Role: eacl.RoleOthers, Op: eacl.OperationGet, @@ -336,14 +384,16 @@ func TestAstToTable(t *testing.T) { record2.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, "objectName") expectedTable.AddRecord(record2) - actualTable, err := astToTable(ast, "bucketName") + actualTable, err := astToTable(ast) require.NoError(t, err) require.Equal(t, expectedTable, actualTable) } func TestRemoveUsers(t *testing.T) { resource := &astResource{ - Name: "name", + resourceInfo: resourceInfo{ + Bucket: "bucket", + }, Operations: []*astOperation{{ Users: []string{"user1", "user3"}, Role: eacl.RoleUser, @@ -361,7 +411,7 @@ func TestRemoveUsers(t *testing.T) { removeUsers(resource, op, []string{"user1", "user2"}) 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"}) } @@ -392,8 +442,10 @@ func TestBucketAclToPolicy(t *testing.T) { }, Permission: aclWrite, }}, - Resource: "bucketName", - IsBucket: true, + } + + resInfo := &resourceInfo{ + Bucket: "bucketName", } expectedPolicy := &bucketPolicy{ @@ -404,24 +456,24 @@ func TestBucketAclToPolicy(t *testing.T) { CanonicalUser: id, }, Action: []string{"s3:ListBucket", "s3:ListBucketVersions", "s3:ListBucketMultipartUploads", "s3:PutObject", "s3:DeleteObject"}, - Resource: []string{arnAwsPrefix + acl.Resource}, + Resource: []string{arnAwsPrefix + resInfo.Name()}, }, { Effect: "Allow", Principal: principal{AWS: allUsersWildcard}, Action: []string{"s3:ListBucket", "s3:ListBucketVersions", "s3:ListBucketMultipartUploads"}, - Resource: []string{arnAwsPrefix + acl.Resource}, + Resource: []string{arnAwsPrefix + resInfo.Name()}, }, { Effect: "Allow", Principal: principal{ CanonicalUser: id2, }, 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.Equal(t, expectedPolicy, actualPolicy) } @@ -459,8 +511,11 @@ func TestObjectAclToPolicy(t *testing.T) { }, Permission: aclRead, }}, - Resource: "bucketName/object", - IsBucket: false, + } + + resInfo := &resourceInfo{ + Bucket: "bucketName", + Object: "object", } expectedPolicy := &bucketPolicy{ @@ -471,7 +526,7 @@ func TestObjectAclToPolicy(t *testing.T) { CanonicalUser: id, }, Action: []string{"s3:GetObject", "s3:GetObjectVersion"}, - Resource: []string{arnAwsPrefix + acl.Resource}, + Resource: []string{arnAwsPrefix + resInfo.Name()}, }, { Effect: "Allow", @@ -479,17 +534,17 @@ func TestObjectAclToPolicy(t *testing.T) { CanonicalUser: id2, }, Action: []string{"s3:GetObject", "s3:GetObjectVersion"}, - Resource: []string{arnAwsPrefix + acl.Resource}, + Resource: []string{arnAwsPrefix + resInfo.Name()}, }, { Effect: "Allow", Principal: principal{AWS: allUsersWildcard}, 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.Equal(t, expectedPolicy, actualPolicy) } @@ -674,8 +729,6 @@ func TestBucketAclToTable(t *testing.T) { }, Permission: aclWrite, }}, - Resource: "bucketName", - IsBucket: true, } expectedTable := new(eacl.Table) @@ -691,8 +744,164 @@ func TestBucketAclToTable(t *testing.T) { for _, op := range fullOps { expectedTable.AddRecord(getOthersRecord(op, eacl.ActionDeny)) } + resInfo := &resourceInfo{ + Bucket: "bucketName", + } - actualTable, err := bucketACLToTable(acl) + actualTable, err := bucketACLToTable(acl, resInfo) require.NoError(t, err) 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) +} diff --git a/api/handler/put.go b/api/handler/put.go index 36f1c8d8..fa460a1a 100644 --- a/api/handler/put.go +++ b/api/handler/put.go @@ -38,47 +38,6 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { 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) if err != nil { 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 } + 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 err = h.obj.PutObjectTagging(r.Context(), &layer.PutTaggingParams{ObjectInfo: info, TagSet: tagSet}); err != nil { 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) return } - bktACL.IsBucket = true + resInfo := &resourceInfo{Bucket: reqInfo.BucketName} - p.EACL, err = bucketACLToTable(bktACL) + p.EACL, err = bucketACLToTable(bktACL, resInfo) if err != nil { h.logAndSendError(w, "could translate bucket acl to eacl", reqInfo, err) return diff --git a/api/handler/response.go b/api/handler/response.go index 918b37fa..2649a2f5 100644 --- a/api/handler/response.go +++ b/api/handler/response.go @@ -57,8 +57,6 @@ type AccessControlPolicy struct { XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ AccessControlPolicy" json:"-"` Owner Owner AccessControlList []*Grant `xml:"AccessControlList>Grant"` - Resource string `xml:"-"` - IsBucket bool `xml:"-"` } // Grant is container for Grantee data.