From e539f2507654f21773641b433553495359486523 Mon Sep 17 00:00:00 2001 From: Angira Kekteeva Date: Thu, 11 Aug 2022 04:05:01 +0400 Subject: [PATCH] [#575] Move EACL Table conversion in neofs package Signed-off-by: Angira Kekteeva --- api/handler/acl.go | 545 +++++--------------------- api/handler/acl_test.go | 656 ++++++-------------------------- api/handler/api.go | 1 + api/handler/multipart_upload.go | 2 +- api/handler/neofs.go | 13 + api/handler/put.go | 12 +- internal/neofs/acl.go | 255 +++++++++++++ internal/neofs/acl_test.go | 310 +++++++++++++++ internal/neofs/neofs.go | 96 +++++ 9 files changed, 910 insertions(+), 980 deletions(-) create mode 100644 api/handler/neofs.go create mode 100644 internal/neofs/acl.go create mode 100644 internal/neofs/acl_test.go diff --git a/api/handler/acl.go b/api/handler/acl.go index 76771863..13a6f98f 100644 --- a/api/handler/acl.go +++ b/api/handler/acl.go @@ -2,7 +2,6 @@ package handler import ( "context" - "crypto/ecdsa" "crypto/elliptic" "encoding/hex" "encoding/json" @@ -10,8 +9,6 @@ import ( stderrors "errors" "fmt" "net/http" - "sort" - "strconv" "strings" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -21,31 +18,29 @@ import ( "github.com/nspcc-dev/neofs-s3-gw/api/errors" "github.com/nspcc-dev/neofs-s3-gw/api/layer" "github.com/nspcc-dev/neofs-sdk-go/eacl" - "github.com/nspcc-dev/neofs-sdk-go/object" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" "github.com/nspcc-dev/neofs-sdk-go/session" "go.uber.org/zap" ) var ( - writeOps = []eacl.Operation{eacl.OperationPut, eacl.OperationDelete} - readOps = []eacl.Operation{eacl.OperationGet, eacl.OperationHead, + WriteOps = []eacl.Operation{eacl.OperationPut, eacl.OperationDelete} + ReadOps = []eacl.Operation{eacl.OperationGet, eacl.OperationHead, eacl.OperationSearch, eacl.OperationRange, eacl.OperationRangeHash} - fullOps = []eacl.Operation{eacl.OperationGet, eacl.OperationHead, eacl.OperationPut, + FullOps = []eacl.Operation{eacl.OperationGet, eacl.OperationHead, eacl.OperationPut, eacl.OperationDelete, eacl.OperationSearch, eacl.OperationRange, eacl.OperationRangeHash} ) var actionToOpMap = map[string][]eacl.Operation{ s3DeleteObject: {eacl.OperationDelete}, - s3GetObject: readOps, + s3GetObject: ReadOps, s3PutObject: {eacl.OperationPut}, - s3ListBucket: readOps, + s3ListBucket: ReadOps, } const ( arnAwsPrefix = "arn:aws:s3:::" allUsersWildcard = "*" - allUsersGroup = "http://acs.amazonaws.com/groups/global/AllUsers" + AllUsersGroup = "http://acs.amazonaws.com/groups/global/AllUsers" s3DeleteObject = "s3:DeleteObject" s3GetObject = "s3:GetObject" @@ -60,18 +55,18 @@ const ( type AWSACL string const ( - aclFullControl AWSACL = "FULL_CONTROL" - aclWrite AWSACL = "WRITE" - aclRead AWSACL = "READ" + ACLFullControl AWSACL = "FULL_CONTROL" + ACLWrite AWSACL = "WRITE" + ACLRead AWSACL = "READ" ) // GranteeType is aws grantee permission type constants. type GranteeType string const ( - acpCanonicalUser GranteeType = "CanonicalUser" - acpAmazonCustomerByEmail GranteeType = "AmazonCustomerByEmail" - acpGroup GranteeType = "Group" + AcpCanonicalUser GranteeType = "CanonicalUser" + AcpAmazonCustomerByEmail GranteeType = "AmazonCustomerByEmail" + AcpGroup GranteeType = "Group" ) type bucketPolicy struct { @@ -94,27 +89,18 @@ type principal struct { CanonicalUser string `json:"CanonicalUser,omitempty"` } -type orderedAstResource struct { - Index int - Resource *astResource +type AstResource struct { + ResourceInfo + Operations []*AstOperation } -type ast struct { - Resources []*astResource -} - -type astResource struct { - resourceInfo - Operations []*astOperation -} - -type resourceInfo struct { +type ResourceInfo struct { Bucket string Object string Version string } -func (r *resourceInfo) Name() string { +func (r *ResourceInfo) Name() string { if len(r.Object) == 0 { return r.Bucket } @@ -124,40 +110,20 @@ func (r *resourceInfo) Name() string { return r.Bucket + "/" + r.Object + ":" + r.Version } -func (r *resourceInfo) IsBucket() bool { +func (r *ResourceInfo) IsBucket() bool { return len(r.Object) == 0 } -type astOperation struct { +type AstOperation struct { Users []string Op eacl.Operation Action eacl.Action } -func (a astOperation) IsGroupGrantee() bool { +func (a AstOperation) IsGroupGrantee() bool { return len(a.Users) == 0 } -const ( - serviceRecordResourceKey = "Resource" - serviceRecordGroupLengthKey = "GroupLength" -) - -type ServiceRecord struct { - Resource string - GroupRecordsLength int -} - -func (s ServiceRecord) ToEACLRecord() *eacl.Record { - serviceRecord := eacl.NewRecord() - serviceRecord.SetAction(eacl.ActionAllow) - serviceRecord.SetOperation(eacl.OperationGet) - serviceRecord.AddFilter(eacl.HeaderFromService, eacl.MatchUnknown, serviceRecordResourceKey, s.Resource) - serviceRecord.AddFilter(eacl.HeaderFromService, eacl.MatchUnknown, serviceRecordGroupLengthKey, strconv.Itoa(s.GroupRecordsLength)) - eacl.AddFormedTarget(serviceRecord, eacl.RoleSystem) - return serviceRecord -} - func (h *handler) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) { reqInfo := api.GetReqInfo(r.Context()) @@ -222,7 +188,7 @@ func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) { return } - resInfo := &resourceInfo{Bucket: reqInfo.BucketName} + resInfo := &ResourceInfo{Bucket: reqInfo.BucketName} astBucket, err := aclToAst(list, resInfo) if err != nil { h.logAndSendError(w, "could not translate acl to policy", reqInfo, err) @@ -242,13 +208,13 @@ func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -func (h *handler) updateBucketACL(r *http.Request, astChild *ast, bktInfo *data.BucketInfo, sessionToken *session.Container) (bool, error) { +func (h *handler) updateBucketACL(r *http.Request, astChild *Ast, bktInfo *data.BucketInfo, sessionToken *session.Container) (bool, error) { bucketACL, err := h.obj.GetBucketACL(r.Context(), bktInfo) if err != nil { return false, fmt.Errorf("could not get bucket eacl: %w", err) } - parentAst := tableToAst(bucketACL.EACL, bktInfo.Name) + parentAst := h.NeoFS.TableToAst(bucketACL.EACL, bktInfo.Name) strCID := bucketACL.Info.CID.EncodeToString() for _, resource := range parentAst.Resources { @@ -257,12 +223,12 @@ func (h *handler) updateBucketACL(r *http.Request, astChild *ast, bktInfo *data. } } - resAst, updated := mergeAst(parentAst, astChild) + resAst, updated := MergeAst(parentAst, astChild) if !updated { return false, nil } - table, err := astToTable(resAst) + table, err := h.NeoFS.AstToTable(resAst) if err != nil { return false, fmt.Errorf("could not translate ast to table: %w", err) } @@ -339,7 +305,7 @@ func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) { return } - resInfo := &resourceInfo{ + resInfo := &ResourceInfo{ Bucket: reqInfo.BucketName, Object: reqInfo.ObjectName, Version: versionID, @@ -403,7 +369,7 @@ func (h *handler) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) return } - ast := tableToAst(bucketACL.EACL, reqInfo.BucketName) + ast := h.NeoFS.TableToAst(bucketACL.EACL, reqInfo.BucketName) bktPolicy := astToPolicy(ast) w.WriteHeader(http.StatusOK) @@ -469,9 +435,9 @@ func parseACLHeaders(header http.Header, key *keys.PublicKey) (*AccessControlPol Grantee: &Grantee{ ID: hex.EncodeToString(key.Bytes()), DisplayName: key.Address(), - Type: acpCanonicalUser, + Type: AcpCanonicalUser, }, - Permission: aclFullControl, + Permission: ACLFullControl, }} cannedACL := header.Get(api.AmzACL) @@ -509,7 +475,7 @@ func addGrantees(list []*Grant, headers http.Header, hdr string) ([]*Grant, erro } for _, grantee := range grantees { - if grantee.Type == acpAmazonCustomerByEmail || (grantee.Type == acpGroup && grantee.URI != allUsersGroup) { + if grantee.Type == AcpAmazonCustomerByEmail || (grantee.Type == AcpGroup && grantee.URI != AllUsersGroup) { return nil, stderrors.New("unsupported grantee type") } @@ -524,11 +490,11 @@ func addGrantees(list []*Grant, headers http.Header, hdr string) ([]*Grant, erro func grantHdrToPermission(grant string) (AWSACL, error) { switch grant { case api.AmzGrantFullControl: - return aclFullControl, nil + return ACLFullControl, nil case api.AmzGrantRead: - return aclRead, nil + return ACLRead, nil case api.AmzGrantWrite: - return aclWrite, nil + return ACLWrite, nil } return "", fmt.Errorf("unsuppoted header: %s", grant) } @@ -559,17 +525,17 @@ func formGrantee(granteeType, value string) (*Grantee, error) { case "id": return &Grantee{ ID: value, - Type: acpCanonicalUser, + Type: AcpCanonicalUser, }, nil case "uri": return &Grantee{ URI: value, - Type: acpGroup, + Type: AcpGroup, }, nil case "emailAddress": return &Grantee{ EmailAddress: value, - Type: acpAmazonCustomerByEmail, + Type: AcpAmazonCustomerByEmail, }, nil } // do not return grantee type to avoid sensitive data logging (#489) @@ -582,20 +548,20 @@ func addPredefinedACP(acp *AccessControlPolicy, cannedACL string) (*AccessContro case basicACLPublic: acp.AccessControlList = append(acp.AccessControlList, &Grant{ Grantee: &Grantee{ - URI: allUsersGroup, - Type: acpGroup, + URI: AllUsersGroup, + Type: AcpGroup, }, - Permission: aclFullControl, + Permission: ACLFullControl, }) case cannedACLAuthRead: fallthrough case basicACLReadOnly: acp.AccessControlList = append(acp.AccessControlList, &Grant{ Grantee: &Grantee{ - URI: allUsersGroup, - Type: acpGroup, + URI: AllUsersGroup, + Type: AcpGroup, }, - Permission: aclRead, + Permission: ACLRead, }) default: return nil, errors.GetAPIError(errors.ErrInvalidArgument) @@ -604,89 +570,7 @@ func addPredefinedACP(acp *AccessControlPolicy, cannedACL string) (*AccessContro return acp, nil } -func tableToAst(table *eacl.Table, bktName string) *ast { - resourceMap := make(map[string]orderedAstResource) - - var groupRecordsLeft int - var currentResource orderedAstResource - for i, record := range table.Records() { - if serviceRec := tryServiceRecord(record); serviceRec != nil { - resInfo := resourceInfoFromName(serviceRec.Resource, bktName) - groupRecordsLeft = serviceRec.GroupRecordsLength - - currentResource = getResourceOrCreate(resourceMap, i, resInfo) - resourceMap[resInfo.Name()] = currentResource - } else if groupRecordsLeft != 0 { - groupRecordsLeft-- - addOperationsAndUpdateMap(currentResource, record, resourceMap) - } else { - resInfo := resInfoFromFilters(bktName, record.Filters()) - resource := getResourceOrCreate(resourceMap, i, resInfo) - addOperationsAndUpdateMap(resource, record, resourceMap) - } - } - - return &ast{ - Resources: formReverseOrderResources(resourceMap), - } -} - -func formReverseOrderResources(resourceMap map[string]orderedAstResource) []*astResource { - orderedResources := make([]orderedAstResource, 0, len(resourceMap)) - for _, resource := range resourceMap { - orderedResources = append(orderedResources, resource) - } - sort.Slice(orderedResources, func(i, j int) bool { - return orderedResources[i].Index >= orderedResources[j].Index // reverse order - }) - - result := make([]*astResource, len(orderedResources)) - for i, ordered := range orderedResources { - res := ordered.Resource - for j, k := 0, len(res.Operations)-1; j < k; j, k = j+1, k-1 { - res.Operations[j], res.Operations[k] = res.Operations[k], res.Operations[j] - } - - result[i] = res - } - - return result -} - -func addOperationsAndUpdateMap(orderedRes orderedAstResource, record eacl.Record, resMap map[string]orderedAstResource) { - for _, target := range record.Targets() { - orderedRes.Resource.Operations = addToList(orderedRes.Resource.Operations, record, target) - } - resMap[orderedRes.Resource.Name()] = orderedRes -} - -func getResourceOrCreate(resMap map[string]orderedAstResource, index int, resInfo resourceInfo) orderedAstResource { - resource, ok := resMap[resInfo.Name()] - if !ok { - resource = orderedAstResource{ - Index: index, - Resource: &astResource{resourceInfo: resInfo}, - } - } - return resource -} - -func resInfoFromFilters(bucketName string, filters []eacl.Filter) resourceInfo { - resInfo := resourceInfo{Bucket: bucketName} - for _, filter := range filters { - if filter.Matcher() == eacl.MatchStringEqual { - if filter.Key() == object.AttributeFileName { - resInfo.Object = filter.Value() - } else if filter.Key() == v2acl.FilterObjectID { - resInfo.Version = filter.Value() - } - } - } - - return resInfo -} - -func mergeAst(parent, child *ast) (*ast, bool) { +func MergeAst(parent, child *Ast) (*Ast, bool) { updated := false for _, resource := range child.Resources { parentResource := getParentResource(parent, resource) @@ -696,7 +580,7 @@ func mergeAst(parent, child *ast) (*ast, bool) { continue } - var newOps []*astOperation + var newOps []*AstOperation for _, astOp := range resource.Operations { ops := getAstOps(parentResource, astOp) switch len(ops) { @@ -760,7 +644,7 @@ func mergeAst(parent, child *ast) (*ast, bool) { return parent, updated } -func handleAddOperations(parentResource *astResource, astOp, existedOp *astOperation) bool { +func handleAddOperations(parentResource *AstResource, astOp, existedOp *AstOperation) bool { var needToAdd []string for _, user := range astOp.Users { if !containsStr(existedOp.Users, user) { @@ -774,7 +658,7 @@ func handleAddOperations(parentResource *astResource, astOp, existedOp *astOpera return false } -func handleRemoveOperations(parentResource *astResource, astOp, existedOp *astOperation) bool { +func handleRemoveOperations(parentResource *AstResource, astOp, existedOp *AstOperation) bool { var needToRemove []string for _, user := range astOp.Users { if containsStr(existedOp.Users, user) { @@ -798,8 +682,8 @@ func containsStr(list []string, element string) bool { return false } -func getAstOps(resource *astResource, childOp *astOperation) []*astOperation { - var res []*astOperation +func getAstOps(resource *AstResource, childOp *AstOperation) []*AstOperation { + var res []*AstOperation for _, astOp := range resource.Operations { if astOp.IsGroupGrantee() == childOp.IsGroupGrantee() && astOp.Op == childOp.Op { res = append(res, astOp) @@ -808,7 +692,7 @@ func getAstOps(resource *astResource, childOp *astOperation) []*astOperation { return res } -func removeAstOp(resource *astResource, group bool, op eacl.Operation, action eacl.Action) { +func removeAstOp(resource *AstResource, group bool, op eacl.Operation, action eacl.Action) { for i, astOp := range resource.Operations { if astOp.IsGroupGrantee() == group && astOp.Op == op && astOp.Action == action { resource.Operations = append(resource.Operations[:i], resource.Operations[i+1:]...) @@ -817,7 +701,7 @@ func removeAstOp(resource *astResource, group bool, op eacl.Operation, action ea } } -func addUsers(resource *astResource, astO *astOperation, users []string) { +func addUsers(resource *AstResource, astO *AstOperation, users []string) { for _, astOp := range resource.Operations { if astOp.IsGroupGrantee() == astO.IsGroupGrantee() && astOp.Op == astO.Op && astOp.Action == astO.Action { astOp.Users = append(astO.Users, users...) @@ -826,9 +710,9 @@ func addUsers(resource *astResource, astO *astOperation, users []string) { } } -func removeUsers(resource *astResource, astOperation *astOperation, users []string) { +func removeUsers(resource *AstResource, AstOperation *AstOperation, users []string) { for ind, astOp := range resource.Operations { - if !astOp.IsGroupGrantee() && astOp.Op == astOperation.Op && astOp.Action == astOperation.Action { + if !astOp.IsGroupGrantee() && astOp.Op == AstOperation.Op && astOp.Action == AstOperation.Action { filteredUsers := astOp.Users[:0] // new slice without allocation for _, user := range astOp.Users { if !containsStr(users, user) { @@ -845,7 +729,7 @@ func removeUsers(resource *astResource, astOperation *astOperation, users []stri } } -func getParentResource(parent *ast, resource *astResource) *astResource { +func getParentResource(parent *Ast, resource *AstResource) *AstResource { for _, parentResource := range parent.Resources { if resource.Bucket == parentResource.Bucket && resource.Object == parentResource.Object && resource.Version == parentResource.Version { @@ -855,137 +739,10 @@ func getParentResource(parent *ast, resource *astResource) *astResource { return nil } -func astToTable(ast *ast) (*eacl.Table, error) { - table := eacl.NewTable() - - for i := len(ast.Resources) - 1; i >= 0; i-- { - records, err := formRecords(ast.Resources[i]) - if err != nil { - return nil, fmt.Errorf("form records: %w", err) - } - - serviceRecord := ServiceRecord{ - Resource: ast.Resources[i].Name(), - GroupRecordsLength: len(records), - } - table.AddRecord(serviceRecord.ToEACLRecord()) - - for _, rec := range records { - table.AddRecord(rec) - } - } - - return table, nil -} - -func tryServiceRecord(record eacl.Record) *ServiceRecord { - if record.Action() != eacl.ActionAllow || record.Operation() != eacl.OperationGet || - len(record.Targets()) != 1 || len(record.Filters()) != 2 { - return nil - } - - target := record.Targets()[0] - if target.Role() != eacl.RoleSystem { - return nil - } - - resourceFilter := record.Filters()[0] - recordsFilter := record.Filters()[1] - if resourceFilter.From() != eacl.HeaderFromService || recordsFilter.From() != eacl.HeaderFromService || - resourceFilter.Matcher() != eacl.MatchUnknown || recordsFilter.Matcher() != eacl.MatchUnknown || - resourceFilter.Key() != serviceRecordResourceKey || recordsFilter.Key() != serviceRecordGroupLengthKey { - return nil - } - - groupLength, err := strconv.Atoi(recordsFilter.Value()) - if err != nil { - return nil - } - - return &ServiceRecord{ - Resource: resourceFilter.Value(), - GroupRecordsLength: groupLength, - } -} - -func formRecords(resource *astResource) ([]*eacl.Record, error) { - var res []*eacl.Record - - for i := len(resource.Operations) - 1; i >= 0; i-- { - astOp := resource.Operations[i] - record := eacl.NewRecord() - record.SetOperation(astOp.Op) - record.SetAction(astOp.Action) - if astOp.IsGroupGrantee() { - eacl.AddFormedTarget(record, eacl.RoleOthers) - } else { - targetKeys := make([]ecdsa.PublicKey, 0, len(astOp.Users)) - for _, user := range astOp.Users { - pk, err := keys.NewPublicKeyFromString(user) - if err != nil { - return nil, fmt.Errorf("public key from string: %w", err) - } - targetKeys = append(targetKeys, (ecdsa.PublicKey)(*pk)) - } - // Unknown role is used, because it is ignored when keys are set - eacl.AddFormedTarget(record, eacl.RoleUnknown, targetKeys...) - } - if len(resource.Object) != 0 { - if len(resource.Version) != 0 { - var id oid.ID - if err := id.DecodeString(resource.Version); err != nil { - return nil, fmt.Errorf("parse object version (oid): %w", err) - } - record.AddObjectIDFilter(eacl.MatchStringEqual, id) - } else { - record.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, resource.Object) - } - } - res = append(res, record) - } - - return res, nil -} - -func addToList(operations []*astOperation, rec eacl.Record, target eacl.Target) []*astOperation { - var ( - found *astOperation - groupTarget = target.Role() == eacl.RoleOthers - ) +func policyToAst(bktPolicy *bucketPolicy) (*Ast, error) { + res := &Ast{} - for _, astOp := range operations { - if astOp.Op == rec.Operation() && astOp.IsGroupGrantee() == groupTarget { - found = astOp - } - } - - if found != nil { - if !groupTarget { - for _, key := range target.BinaryKeys() { - found.Users = append(found.Users, hex.EncodeToString(key)) - } - } - } else { - astOperation := &astOperation{ - Op: rec.Operation(), - Action: rec.Action(), - } - if !groupTarget { - for _, key := range target.BinaryKeys() { - astOperation.Users = append(astOperation.Users, hex.EncodeToString(key)) - } - } - - operations = append(operations, astOperation) - } - - return operations -} - -func policyToAst(bktPolicy *bucketPolicy) (*ast, error) { - res := &ast{} - - rr := make(map[string]*astResource) + rr := make(map[string]*AstResource) for _, state := range bktPolicy.Statement { if state.Principal.AWS != "" && state.Principal.AWS != allUsersWildcard || @@ -1001,8 +758,8 @@ func policyToAst(bktPolicy *bucketPolicy) (*ast, error) { trimmedResource := strings.TrimPrefix(resource, arnAwsPrefix) r, ok := rr[trimmedResource] if !ok { - r = &astResource{ - resourceInfo: resourceInfoFromName(trimmedResource, bktPolicy.Bucket), + r = &AstResource{ + ResourceInfo: ResourceInfoFromName(trimmedResource, bktPolicy.Bucket), } } for _, action := range state.Action { @@ -1023,8 +780,8 @@ func policyToAst(bktPolicy *bucketPolicy) (*ast, error) { return res, nil } -func resourceInfoFromName(name, bucketName string) resourceInfo { - resInfo := resourceInfo{Bucket: bucketName} +func ResourceInfoFromName(name, bucketName string) ResourceInfo { + resInfo := ResourceInfo{Bucket: bucketName} if name != bucketName { versionedObject := strings.TrimPrefix(name, bucketName+"/") objVersion := strings.Split(versionedObject, ":") @@ -1042,7 +799,7 @@ func resourceInfoFromName(name, bucketName string) resourceInfo { return resInfo } -func astToPolicy(ast *ast) *bucketPolicy { +func astToPolicy(ast *Ast) *bucketPolicy { bktPolicy := &bucketPolicy{} for _, resource := range ast.Resources { @@ -1057,7 +814,7 @@ func astToPolicy(ast *ast) *bucketPolicy { return bktPolicy } -func handleResourceOperations(bktPolicy *bucketPolicy, list []*astOperation, eaclAction eacl.Action, resourceName string) { +func handleResourceOperations(bktPolicy *bucketPolicy, list []*AstOperation, eaclAction eacl.Action, resourceName string) { userOpsMap := make(map[string][]eacl.Operation) for _, op := range list { @@ -1068,9 +825,9 @@ func handleResourceOperations(bktPolicy *bucketPolicy, list []*astOperation, eac userOpsMap[user] = userOps } } else { - userOps := userOpsMap[allUsersGroup] + userOps := userOpsMap[AllUsersGroup] userOps = append(userOps, op.Op) - userOpsMap[allUsersGroup] = userOps + userOpsMap[AllUsersGroup] = userOps } } @@ -1092,7 +849,7 @@ func handleResourceOperations(bktPolicy *bucketPolicy, list []*astOperation, eac Action: actions, Resource: []string{arnAwsPrefix + resourceName}, } - if user == allUsersGroup { + if user == AllUsersGroup { state.Principal = principal{AWS: allUsersWildcard} } bktPolicy.Statement = append(bktPolicy.Statement, state) @@ -1100,8 +857,8 @@ func handleResourceOperations(bktPolicy *bucketPolicy, list []*astOperation, eac } } -func triageOperations(operations []*astOperation) ([]*astOperation, []*astOperation) { - var allowed, denied []*astOperation +func triageOperations(operations []*AstOperation) ([]*AstOperation, []*AstOperation) { + var allowed, denied []*AstOperation for _, op := range operations { if op.Action == eacl.ActionAllow { allowed = append(allowed, op) @@ -1112,8 +869,8 @@ func triageOperations(operations []*astOperation) ([]*astOperation, []*astOperat return allowed, denied } -func addTo(list []*astOperation, userID string, op eacl.Operation, groupGrantee bool, action eacl.Action) []*astOperation { - var found *astOperation +func addTo(list []*AstOperation, userID string, op eacl.Operation, groupGrantee bool, action eacl.Action) []*AstOperation { + var found *AstOperation for _, astop := range list { if astop.Op == op && astop.IsGroupGrantee() == groupGrantee { found = astop @@ -1125,28 +882,28 @@ func addTo(list []*astOperation, userID string, op eacl.Operation, groupGrantee found.Users = append(found.Users, userID) } } else { - astoperation := &astOperation{ + AstOperation := &AstOperation{ Op: op, Action: action, } if !groupGrantee { - astoperation.Users = append(astoperation.Users, userID) + AstOperation.Users = append(AstOperation.Users, userID) } - list = append(list, astoperation) + list = append(list, AstOperation) } return list } -func aclToAst(acl *AccessControlPolicy, resInfo *resourceInfo) (*ast, error) { - res := &ast{} +func aclToAst(acl *AccessControlPolicy, resInfo *ResourceInfo) (*Ast, error) { + res := &Ast{} - resource := &astResource{resourceInfo: *resInfo} + resource := &AstResource{ResourceInfo: *resInfo} - ops := readOps + ops := ReadOps if resInfo.IsBucket() { - ops = append(ops, writeOps...) + ops = append(ops, WriteOps...) } // Expect to have at least 1 full control grant for owner which is set in @@ -1154,7 +911,7 @@ func aclToAst(acl *AccessControlPolicy, resInfo *resourceInfo) (*ast, error) { // canned ACL, which is processed in this branch. if len(acl.AccessControlList) < 2 { for _, op := range ops { - operation := &astOperation{ + operation := &AstOperation{ Op: op, Action: eacl.ActionDeny, } @@ -1163,7 +920,7 @@ func aclToAst(acl *AccessControlPolicy, resInfo *resourceInfo) (*ast, error) { } for _, op := range ops { - operation := &astOperation{ + operation := &AstOperation{ Users: []string{acl.Owner.ID}, Op: op, Action: eacl.ActionAllow, @@ -1172,12 +929,12 @@ func aclToAst(acl *AccessControlPolicy, resInfo *resourceInfo) (*ast, error) { } for _, grant := range acl.AccessControlList { - if grant.Grantee.Type == acpAmazonCustomerByEmail || (grant.Grantee.Type == acpGroup && grant.Grantee.URI != allUsersGroup) { + if grant.Grantee.Type == AcpAmazonCustomerByEmail || (grant.Grantee.Type == AcpGroup && grant.Grantee.URI != AllUsersGroup) { return nil, stderrors.New("unsupported grantee type") } var groupGrantee bool - if grant.Grantee.Type == acpGroup { + if grant.Grantee.Type == AcpGroup { groupGrantee = true } else if grant.Grantee.ID == acl.Owner.ID { continue @@ -1190,33 +947,33 @@ func aclToAst(acl *AccessControlPolicy, resInfo *resourceInfo) (*ast, error) { } } - res.Resources = []*astResource{resource} + res.Resources = []*AstResource{resource} return res, nil } -func aclToPolicy(acl *AccessControlPolicy, resInfo *resourceInfo) (*bucketPolicy, error) { +func aclToPolicy(acl *AccessControlPolicy, resInfo *ResourceInfo) (*bucketPolicy, error) { if resInfo.Bucket == "" { return nil, fmt.Errorf("resource bucket must not be empty") } results := []statement{ - getAllowStatement(resInfo, acl.Owner.ID, aclFullControl), + getAllowStatement(resInfo, acl.Owner.ID, ACLFullControl), } // Expect to have at least 1 full control grant for owner which is set in // parseACLHeaders(). If there is no other grants, then user sets private // canned ACL, which is processed in this branch. if len(acl.AccessControlList) < 2 { - results = append([]statement{getDenyStatement(resInfo, allUsersWildcard, aclFullControl)}, results...) + results = append([]statement{getDenyStatement(resInfo, allUsersWildcard, ACLFullControl)}, results...) } for _, grant := range acl.AccessControlList { - if grant.Grantee.Type == acpAmazonCustomerByEmail || (grant.Grantee.Type == acpGroup && grant.Grantee.URI != allUsersGroup) { + if grant.Grantee.Type == AcpAmazonCustomerByEmail || (grant.Grantee.Type == AcpGroup && grant.Grantee.URI != AllUsersGroup) { return nil, stderrors.New("unsupported grantee type") } user := grant.Grantee.ID - if grant.Grantee.Type == acpGroup { + if grant.Grantee.Type == AcpGroup { user = allUsersWildcard } else if user == acl.Owner.ID { continue @@ -1230,7 +987,7 @@ func aclToPolicy(acl *AccessControlPolicy, resInfo *resourceInfo) (*bucketPolicy }, nil } -func getAllowStatement(resInfo *resourceInfo, id string, permission AWSACL) statement { +func getAllowStatement(resInfo *ResourceInfo, id string, permission AWSACL) statement { state := statement{ Effect: "Allow", Principal: principal{ @@ -1247,7 +1004,7 @@ func getAllowStatement(resInfo *resourceInfo, id string, permission AWSACL) stat return state } -func getDenyStatement(resInfo *resourceInfo, id string, permission AWSACL) statement { +func getDenyStatement(resInfo *ResourceInfo, id string, permission AWSACL) statement { state := statement{ Effect: "Deny", Principal: principal{ @@ -1267,17 +1024,17 @@ func getDenyStatement(resInfo *resourceInfo, id string, permission AWSACL) state func getActions(permission AWSACL, isBucket bool) []string { var res []string switch permission { - case aclRead: + case ACLRead: if isBucket { res = []string{s3ListBucket, s3ListBucketVersions, s3ListBucketMultipartUploads} } else { res = []string{s3GetObject, s3GetObjectVersion} } - case aclWrite: + case ACLWrite: if isBucket { res = []string{s3PutObject, s3DeleteObject} } - case aclFullControl: + case ACLFullControl: if isBucket { res = []string{s3ListBucket, s3ListBucketVersions, s3ListBucketMultipartUploads, s3PutObject, s3DeleteObject} } else { @@ -1309,18 +1066,6 @@ func actionToEffect(action eacl.Action) string { } } -func permissionToOperations(permission AWSACL) []eacl.Operation { - switch permission { - case aclFullControl: - return fullOps - case aclRead: - return readOps - case aclWrite: - return writeOps - } - return nil -} - func isWriteOperation(op eacl.Operation) bool { return op == eacl.OperationDelete || op == eacl.OperationPut } @@ -1335,7 +1080,7 @@ func (h *handler) encodeObjectACL(bucketACL *layer.BucketACL, bucketName, object m := make(map[string][]eacl.Operation) - astList := tableToAst(bucketACL.EACL, bucketName) + astList := h.NeoFS.TableToAst(bucketACL.EACL, bucketName) for _, resource := range astList.Resources { if resource.Version != objectVersion { @@ -1348,8 +1093,8 @@ func (h *handler) encodeObjectACL(bucketACL *layer.BucketACL, bucketName, object } if len(op.Users) == 0 { - list := append(m[allUsersGroup], op.Op) - m[allUsersGroup] = list + list := append(m[AllUsersGroup], op.Op) + m[AllUsersGroup] = list } else { for _, user := range op.Users { list := append(m[user], op.Op) @@ -1360,7 +1105,7 @@ func (h *handler) encodeObjectACL(bucketACL *layer.BucketACL, bucketName, object } for key, val := range m { - permission := aclFullControl + permission := ACLFullControl read, write := true, true for op := eacl.OperationGet; op <= eacl.OperationRangeHash; op++ { if !contains(val, op) { @@ -1377,17 +1122,17 @@ func (h *handler) encodeObjectACL(bucketACL *layer.BucketACL, bucketName, object continue } if !read { - permission = aclWrite + permission = ACLWrite } else if !write { - permission = aclRead + permission = ACLRead } var grantee *Grantee - if key == allUsersGroup { - grantee = NewGrantee(acpGroup) - grantee.URI = allUsersGroup + if key == AllUsersGroup { + grantee = NewGrantee(AcpGroup) + grantee.URI = AllUsersGroup } else { - grantee = NewGrantee(acpCanonicalUser) + grantee = NewGrantee(AcpCanonicalUser) grantee.ID = key } @@ -1413,89 +1158,3 @@ func contains(list []eacl.Operation, op eacl.Operation) bool { } return false } - -type getRecordFunc func(op eacl.Operation) *eacl.Record - -func bucketACLToTable(acp *AccessControlPolicy, resInfo *resourceInfo) (*eacl.Table, error) { - if !resInfo.IsBucket() { - return nil, fmt.Errorf("allowed only bucket acl") - } - - var found bool - table := eacl.NewTable() - - ownerKey, err := keys.NewPublicKeyFromString(acp.Owner.ID) - if err != nil { - return nil, fmt.Errorf("public key from string: %w", err) - } - - for _, grant := range acp.AccessControlList { - if !isValidGrant(grant) { - return nil, stderrors.New("unsupported grantee") - } - if grant.Grantee.ID == acp.Owner.ID { - found = true - } - - getRecord, err := getRecordFunction(grant.Grantee) - if err != nil { - return nil, fmt.Errorf("record func from grantee: %w", err) - } - for _, op := range permissionToOperations(grant.Permission) { - table.AddRecord(getRecord(op)) - } - } - - if !found { - for _, op := range fullOps { - table.AddRecord(getAllowRecord(op, ownerKey)) - } - } - - for _, op := range fullOps { - table.AddRecord(getOthersRecord(op, eacl.ActionDeny)) - } - - return table, nil -} - -func getRecordFunction(grantee *Grantee) (getRecordFunc, error) { - switch grantee.Type { - case acpAmazonCustomerByEmail: - case acpCanonicalUser: - pk, err := keys.NewPublicKeyFromString(grantee.ID) - if err != nil { - return nil, fmt.Errorf("couldn't parse canonical ID %s: %w", grantee.ID, err) - } - return func(op eacl.Operation) *eacl.Record { - return getAllowRecord(op, pk) - }, nil - case acpGroup: - return func(op eacl.Operation) *eacl.Record { - return getOthersRecord(op, eacl.ActionAllow) - }, nil - } - return nil, fmt.Errorf("unknown type: %s", grantee.Type) -} - -func isValidGrant(grant *Grant) bool { - return (grant.Permission == aclFullControl || grant.Permission == aclRead || grant.Permission == aclWrite) && - (grant.Grantee.Type == acpCanonicalUser || (grant.Grantee.Type == acpGroup && grant.Grantee.URI == allUsersGroup)) -} - -func getAllowRecord(op eacl.Operation, pk *keys.PublicKey) *eacl.Record { - record := eacl.NewRecord() - record.SetOperation(op) - record.SetAction(eacl.ActionAllow) - // Unknown role is used, because it is ignored when keys are set - eacl.AddFormedTarget(record, eacl.RoleUnknown, (ecdsa.PublicKey)(*pk)) - return record -} - -func getOthersRecord(op eacl.Operation, action eacl.Action) *eacl.Record { - record := eacl.NewRecord() - record.SetOperation(op) - record.SetAction(action) - eacl.AddFormedTarget(record, eacl.RoleOthers) - return record -} diff --git a/api/handler/acl_test.go b/api/handler/acl_test.go index 84b2df35..483411fa 100644 --- a/api/handler/acl_test.go +++ b/api/handler/acl_test.go @@ -1,11 +1,9 @@ package handler import ( - "crypto/ecdsa" "crypto/rand" "crypto/sha256" "encoding/hex" - "fmt" "io" "net/http" "testing" @@ -13,74 +11,10 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neofs-s3-gw/api" "github.com/nspcc-dev/neofs-sdk-go/eacl" - "github.com/nspcc-dev/neofs-sdk-go/object" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" "github.com/stretchr/testify/require" ) -func TestTableToAst(t *testing.T) { - b := make([]byte, 32) - _, err := io.ReadFull(rand.Reader, b) - require.NoError(t, err) - var id oid.ID - id.SetSHA256(sha256.Sum256(b)) - - key, err := keys.NewPrivateKey() - require.NoError(t, err) - key2, err := keys.NewPrivateKey() - require.NoError(t, err) - - table := new(eacl.Table) - record := eacl.NewRecord() - record.SetAction(eacl.ActionAllow) - record.SetOperation(eacl.OperationGet) - eacl.AddFormedTarget(record, eacl.RoleOthers) - table.AddRecord(record) - record2 := eacl.NewRecord() - record2.SetAction(eacl.ActionDeny) - record2.SetOperation(eacl.OperationPut) - // Unknown role is used, because it is ignored when keys are set - eacl.AddFormedTarget(record2, eacl.RoleUnknown, *(*ecdsa.PublicKey)(key.PublicKey()), *((*ecdsa.PublicKey)(key2.PublicKey()))) - record2.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, "objectName") - record2.AddObjectIDFilter(eacl.MatchStringEqual, id) - table.AddRecord(record2) - - expectedAst := &ast{ - Resources: []*astResource{ - { - resourceInfo: resourceInfo{Bucket: "bucketName"}, - Operations: []*astOperation{{ - Op: eacl.OperationGet, - Action: eacl.ActionAllow, - }}}, - { - resourceInfo: resourceInfo{ - Bucket: "bucketName", - Object: "objectName", - Version: id.EncodeToString(), - }, - Operations: []*astOperation{{ - Users: []string{ - hex.EncodeToString(key.PublicKey().Bytes()), - hex.EncodeToString(key2.PublicKey().Bytes()), - }, - Op: eacl.OperationPut, - Action: eacl.ActionDeny, - }}}, - }, - } - - actualAst := tableToAst(table, expectedAst.Resources[0].Bucket) - - if actualAst.Resources[0].Name() == expectedAst.Resources[0].Name() { - require.Equal(t, expectedAst, actualAst) - } else { - require.Equal(t, len(expectedAst.Resources), len(actualAst.Resources)) - require.Equal(t, expectedAst.Resources[0], actualAst.Resources[1]) - require.Equal(t, expectedAst.Resources[1], actualAst.Resources[0]) - } -} - func TestPolicyToAst(t *testing.T) { key, err := keys.NewPrivateKey() require.NoError(t, err) @@ -104,19 +38,19 @@ func TestPolicyToAst(t *testing.T) { } policy.Bucket = "bucketName" - expectedAst := &ast{ - Resources: []*astResource{ + expectedAst := &Ast{ + Resources: []*AstResource{ { - resourceInfo: resourceInfo{ + ResourceInfo: ResourceInfo{ Bucket: "bucketName", }, - Operations: []*astOperation{{ + Operations: []*AstOperation{{ Op: eacl.OperationPut, Action: eacl.ActionAllow, }}, }, { - resourceInfo: resourceInfo{ + ResourceInfo: ResourceInfo{ Bucket: "bucketName", Object: "object", }, @@ -137,17 +71,17 @@ func TestPolicyToAst(t *testing.T) { } } -func getReadOps(key *keys.PrivateKey, groupGrantee bool, action eacl.Action) []*astOperation { +func getReadOps(key *keys.PrivateKey, groupGrantee bool, action eacl.Action) []*AstOperation { var ( - result []*astOperation + result []*AstOperation users []string ) if !groupGrantee { users = append(users, hex.EncodeToString(key.PublicKey().Bytes())) } - for _, op := range readOps { - result = append(result, &astOperation{ + for _, op := range ReadOps { + result = append(result, &AstOperation{ Users: users, Op: op, Action: action, @@ -161,14 +95,14 @@ func TestMergeAstUnModified(t *testing.T) { key, err := keys.NewPrivateKey() require.NoError(t, err) - child := &ast{ - Resources: []*astResource{ + child := &Ast{ + Resources: []*AstResource{ { - resourceInfo: resourceInfo{ + ResourceInfo: ResourceInfo{ Bucket: "bucket", Object: "objectName", }, - Operations: []*astOperation{{ + Operations: []*AstOperation{{ Users: []string{hex.EncodeToString(key.PublicKey().Bytes())}, Op: eacl.OperationPut, Action: eacl.ActionDeny, @@ -177,13 +111,13 @@ func TestMergeAstUnModified(t *testing.T) { }, } - parent := &ast{ - Resources: []*astResource{ + parent := &Ast{ + Resources: []*AstResource{ { - resourceInfo: resourceInfo{ + ResourceInfo: ResourceInfo{ Bucket: "bucket", }, - Operations: []*astOperation{{ + Operations: []*AstOperation{{ Op: eacl.OperationGet, Action: eacl.ActionAllow, }}, @@ -192,20 +126,20 @@ func TestMergeAstUnModified(t *testing.T) { }, } - result, updated := mergeAst(parent, child) + result, updated := MergeAst(parent, child) require.False(t, updated) require.Equal(t, parent, result) } func TestMergeAstModified(t *testing.T) { - child := &ast{ - Resources: []*astResource{ + child := &Ast{ + Resources: []*AstResource{ { - resourceInfo: resourceInfo{ + ResourceInfo: ResourceInfo{ Bucket: "bucket", Object: "objectName", }, - Operations: []*astOperation{{ + Operations: []*AstOperation{{ Op: eacl.OperationPut, Action: eacl.ActionDeny, }, { @@ -217,14 +151,14 @@ func TestMergeAstModified(t *testing.T) { }, } - parent := &ast{ - Resources: []*astResource{ + parent := &Ast{ + Resources: []*AstResource{ { - resourceInfo: resourceInfo{ + ResourceInfo: ResourceInfo{ Bucket: "bucket", Object: "objectName", }, - Operations: []*astOperation{{ + Operations: []*AstOperation{{ Users: []string{"user1"}, Op: eacl.OperationGet, Action: eacl.ActionDeny, @@ -233,14 +167,14 @@ func TestMergeAstModified(t *testing.T) { }, } - expected := &ast{ - Resources: []*astResource{ + expected := &Ast{ + Resources: []*AstResource{ { - resourceInfo: resourceInfo{ + ResourceInfo: ResourceInfo{ Bucket: "bucket", Object: "objectName", }, - Operations: []*astOperation{ + Operations: []*AstOperation{ child.Resources[0].Operations[0], { Users: []string{"user1", "user2"}, @@ -252,7 +186,7 @@ func TestMergeAstModified(t *testing.T) { }, } - actual, updated := mergeAst(parent, child) + actual, updated := MergeAst(parent, child) require.True(t, updated) require.Equal(t, expected, actual) } @@ -262,13 +196,13 @@ func TestMergeAppended(t *testing.T) { require.NoError(t, err) users := []string{hex.EncodeToString(key.PublicKey().Bytes())} - parent := &ast{ - Resources: []*astResource{ + parent := &Ast{ + Resources: []*AstResource{ { - resourceInfo: resourceInfo{ + ResourceInfo: ResourceInfo{ Bucket: "bucket", }, - Operations: []*astOperation{ + Operations: []*AstOperation{ { Users: users, Op: eacl.OperationGet, @@ -301,14 +235,14 @@ func TestMergeAppended(t *testing.T) { }, } - child := &ast{ - Resources: []*astResource{ + child := &Ast{ + Resources: []*AstResource{ { - resourceInfo: resourceInfo{ + ResourceInfo: ResourceInfo{ Bucket: "bucket", Object: "objectName", }, - Operations: []*astOperation{ + Operations: []*AstOperation{ { Users: users, Op: eacl.OperationGet, @@ -341,13 +275,13 @@ func TestMergeAppended(t *testing.T) { }, } - expected := &ast{ - Resources: []*astResource{ + expected := &Ast{ + Resources: []*AstResource{ { - resourceInfo: resourceInfo{ + ResourceInfo: ResourceInfo{ Bucket: "bucket", }, - Operations: []*astOperation{ + Operations: []*AstOperation{ { Users: users, Op: eacl.OperationGet, @@ -378,11 +312,11 @@ func TestMergeAppended(t *testing.T) { }, }, { - resourceInfo: resourceInfo{ + ResourceInfo: ResourceInfo{ Bucket: "bucket", Object: "objectName", }, - Operations: []*astOperation{ + Operations: []*AstOperation{ { Users: users, Op: eacl.OperationGet, @@ -414,141 +348,20 @@ func TestMergeAppended(t *testing.T) { }, }, } - actual, updated := mergeAst(parent, child) + actual, updated := MergeAst(parent, child) require.True(t, updated) require.Equal(t, expected, actual) } -func TestOrder(t *testing.T) { - key, err := keys.NewPrivateKey() - require.NoError(t, err) - users := []string{hex.EncodeToString(key.PublicKey().Bytes())} - targetUser := eacl.NewTarget() - targetUser.SetBinaryKeys([][]byte{key.PublicKey().Bytes()}) - targetOther := eacl.NewTarget() - targetOther.SetRole(eacl.RoleOthers) - bucketName := "bucket" - objectName := "objectName" - - expectedAst := &ast{ - Resources: []*astResource{ - { - resourceInfo: resourceInfo{ - Bucket: bucketName, - }, - Operations: []*astOperation{ - { - Users: users, - Op: eacl.OperationGet, - Action: eacl.ActionAllow, - }, - { - Op: eacl.OperationGet, - Action: eacl.ActionDeny, - }, - }, - }, - { - resourceInfo: resourceInfo{ - Bucket: bucketName, - Object: objectName, - }, - Operations: []*astOperation{ - { - Users: users, - Op: eacl.OperationPut, - Action: eacl.ActionAllow, - }, - { - Op: eacl.OperationPut, - Action: eacl.ActionDeny, - }, - }, - }, - }, - } - bucketServiceRec := &ServiceRecord{Resource: expectedAst.Resources[0].Name(), GroupRecordsLength: 2} - bucketUsersGetRec := eacl.NewRecord() - bucketUsersGetRec.SetOperation(eacl.OperationGet) - bucketUsersGetRec.SetAction(eacl.ActionAllow) - bucketUsersGetRec.SetTargets(*targetUser) - bucketOtherGetRec := eacl.NewRecord() - bucketOtherGetRec.SetOperation(eacl.OperationGet) - bucketOtherGetRec.SetAction(eacl.ActionDeny) - bucketOtherGetRec.SetTargets(*targetOther) - objectServiceRec := &ServiceRecord{Resource: expectedAst.Resources[1].Name(), GroupRecordsLength: 2} - objectUsersPutRec := eacl.NewRecord() - objectUsersPutRec.SetOperation(eacl.OperationPut) - objectUsersPutRec.SetAction(eacl.ActionAllow) - objectUsersPutRec.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, objectName) - objectUsersPutRec.SetTargets(*targetUser) - objectOtherPutRec := eacl.NewRecord() - objectOtherPutRec.SetOperation(eacl.OperationPut) - objectOtherPutRec.SetAction(eacl.ActionDeny) - objectOtherPutRec.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, objectName) - objectOtherPutRec.SetTargets(*targetOther) - - expectedEacl := eacl.NewTable() - expectedEacl.AddRecord(objectServiceRec.ToEACLRecord()) - expectedEacl.AddRecord(objectOtherPutRec) - expectedEacl.AddRecord(objectUsersPutRec) - expectedEacl.AddRecord(bucketServiceRec.ToEACLRecord()) - expectedEacl.AddRecord(bucketOtherGetRec) - expectedEacl.AddRecord(bucketUsersGetRec) - - t.Run("astToTable order and vice versa", func(t *testing.T) { - actualEacl, err := astToTable(expectedAst) - require.NoError(t, err) - require.Equal(t, expectedEacl, actualEacl) - - actualAst := tableToAst(actualEacl, bucketName) - require.Equal(t, expectedAst, actualAst) - }) - - t.Run("tableToAst order and vice versa", func(t *testing.T) { - actualAst := tableToAst(expectedEacl, bucketName) - require.Equal(t, expectedAst, actualAst) - - actualEacl, err := astToTable(actualAst) - require.NoError(t, err) - require.Equal(t, expectedEacl, actualEacl) - }) - - t.Run("append a resource", func(t *testing.T) { - childName := "child" - child := &ast{Resources: []*astResource{{ - resourceInfo: resourceInfo{ - Bucket: bucketName, - Object: childName, - }, - Operations: []*astOperation{{Op: eacl.OperationDelete, Action: eacl.ActionDeny}}}}, - } - - childRecord := eacl.NewRecord() - childRecord.SetOperation(eacl.OperationDelete) - childRecord.SetAction(eacl.ActionDeny) - childRecord.SetTargets(*targetOther) - childRecord.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, childName) - - mergedAst, updated := mergeAst(expectedAst, child) - require.True(t, updated) - - mergedEacl, err := astToTable(mergedAst) - require.NoError(t, err) - - require.Equal(t, *childRecord, mergedEacl.Records()[1]) - }) -} - func TestMergeAstModifiedConflict(t *testing.T) { - child := &ast{ - Resources: []*astResource{ + child := &Ast{ + Resources: []*AstResource{ { - resourceInfo: resourceInfo{ + ResourceInfo: ResourceInfo{ Bucket: "bucket", Object: "objectName", }, - Operations: []*astOperation{{ + Operations: []*AstOperation{{ Users: []string{"user1"}, Op: eacl.OperationPut, Action: eacl.ActionDeny, @@ -561,14 +374,14 @@ func TestMergeAstModifiedConflict(t *testing.T) { }, } - parent := &ast{ - Resources: []*astResource{ + parent := &Ast{ + Resources: []*AstResource{ { - resourceInfo: resourceInfo{ + ResourceInfo: ResourceInfo{ Bucket: "bucket", Object: "objectName", }, - Operations: []*astOperation{{ + Operations: []*AstOperation{{ Users: []string{"user1"}, Op: eacl.OperationPut, Action: eacl.ActionAllow, @@ -585,14 +398,14 @@ func TestMergeAstModifiedConflict(t *testing.T) { }, } - expected := &ast{ - Resources: []*astResource{ + expected := &Ast{ + Resources: []*AstResource{ { - resourceInfo: resourceInfo{ + ResourceInfo: ResourceInfo{ Bucket: "bucket", Object: "objectName", }, - Operations: []*astOperation{ + Operations: []*AstOperation{ { Users: []string{"user2", "user1"}, Op: eacl.OperationPut, @@ -607,71 +420,17 @@ func TestMergeAstModifiedConflict(t *testing.T) { }, } - actual, updated := mergeAst(parent, child) + actual, updated := MergeAst(parent, child) require.True(t, updated) require.Equal(t, expected, actual) } -func TestAstToTable(t *testing.T) { - key, err := keys.NewPrivateKey() - require.NoError(t, err) - - ast := &ast{ - Resources: []*astResource{ - { - resourceInfo: resourceInfo{ - Bucket: "bucketName", - }, - Operations: []*astOperation{{ - Users: []string{hex.EncodeToString(key.PublicKey().Bytes())}, - Op: eacl.OperationPut, - Action: eacl.ActionAllow, - }}, - }, - { - resourceInfo: resourceInfo{ - Bucket: "bucketName", - Object: "objectName", - }, - Operations: []*astOperation{{ - Op: eacl.OperationGet, - Action: eacl.ActionDeny, - }}, - }, - }, - } - - expectedTable := eacl.NewTable() - serviceRec1 := &ServiceRecord{Resource: ast.Resources[0].Name(), GroupRecordsLength: 1} - record1 := eacl.NewRecord() - record1.SetAction(eacl.ActionAllow) - record1.SetOperation(eacl.OperationPut) - // Unknown role is used, because it is ignored when keys are set - eacl.AddFormedTarget(record1, eacl.RoleUnknown, *(*ecdsa.PublicKey)(key.PublicKey())) - - serviceRec2 := &ServiceRecord{Resource: ast.Resources[1].Name(), GroupRecordsLength: 1} - record2 := eacl.NewRecord() - record2.SetAction(eacl.ActionDeny) - record2.SetOperation(eacl.OperationGet) - eacl.AddFormedTarget(record2, eacl.RoleOthers) - record2.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, "objectName") - - expectedTable.AddRecord(serviceRec2.ToEACLRecord()) - expectedTable.AddRecord(record2) - expectedTable.AddRecord(serviceRec1.ToEACLRecord()) - expectedTable.AddRecord(record1) - - actualTable, err := astToTable(ast) - require.NoError(t, err) - require.Equal(t, expectedTable, actualTable) -} - func TestRemoveUsers(t *testing.T) { - resource := &astResource{ - resourceInfo: resourceInfo{ + resource := &AstResource{ + ResourceInfo: ResourceInfo{ Bucket: "bucket", }, - Operations: []*astOperation{{ + Operations: []*AstOperation{{ Users: []string{"user1", "user3", "user4"}, Op: eacl.OperationPut, Action: eacl.ActionAllow, @@ -684,17 +443,17 @@ func TestRemoveUsers(t *testing.T) { }, } - op1 := &astOperation{ + op1 := &AstOperation{ Op: eacl.OperationPut, Action: eacl.ActionAllow, } - op2 := &astOperation{ + op2 := &AstOperation{ Op: eacl.OperationGet, Action: eacl.ActionDeny, } - removeUsers(resource, op1, []string{"user1", "user2", "user4"}) // modify astOperation - removeUsers(resource, op2, []string{"user5"}) // remove astOperation + removeUsers(resource, op1, []string{"user1", "user2", "user4"}) // modify AstOperation + removeUsers(resource, op2, []string{"user5"}) // remove AstOperation require.Equal(t, len(resource.Operations), 1) require.Equal(t, []string{"user3"}, resource.Operations[0].Users) @@ -716,20 +475,20 @@ func TestBucketAclToPolicy(t *testing.T) { }, AccessControlList: []*Grant{{ Grantee: &Grantee{ - URI: allUsersGroup, - Type: acpGroup, + URI: AllUsersGroup, + Type: AcpGroup, }, - Permission: aclRead, + Permission: ACLRead, }, { Grantee: &Grantee{ ID: id2, - Type: acpCanonicalUser, + Type: AcpCanonicalUser, }, - Permission: aclWrite, + Permission: ACLWrite, }}, } - resInfo := &resourceInfo{ + resInfo := &ResourceInfo{ Bucket: "bucketName", } @@ -783,25 +542,25 @@ func TestObjectAclToPolicy(t *testing.T) { AccessControlList: []*Grant{{ Grantee: &Grantee{ ID: id, - Type: acpCanonicalUser, + Type: AcpCanonicalUser, }, - Permission: aclFullControl, + Permission: ACLFullControl, }, { Grantee: &Grantee{ ID: id2, - Type: acpCanonicalUser, + Type: AcpCanonicalUser, }, - Permission: aclFullControl, + Permission: ACLFullControl, }, { Grantee: &Grantee{ - URI: allUsersGroup, - Type: acpGroup, + URI: AllUsersGroup, + Type: AcpGroup, }, - Permission: aclRead, + Permission: ACLRead, }}, } - resInfo := &resourceInfo{ + resInfo := &ResourceInfo{ Bucket: "bucketName", Object: "object", } @@ -839,118 +598,6 @@ func TestObjectAclToPolicy(t *testing.T) { require.Equal(t, expectedPolicy, actualPolicy) } -func TestObjectWithVersionAclToTable(t *testing.T) { - key, err := keys.NewPrivateKey() - require.NoError(t, err) - id := hex.EncodeToString(key.PublicKey().Bytes()) - - acl := &AccessControlPolicy{ - Owner: Owner{ - ID: id, - DisplayName: "user1", - }, - AccessControlList: []*Grant{{ - Grantee: &Grantee{ - ID: id, - Type: acpCanonicalUser, - }, - Permission: aclFullControl, - }}, - } - - resInfoObject := &resourceInfo{ - Bucket: "bucketName", - Object: "object", - } - expectedTable := allowedTableForPrivateObject(t, key, resInfoObject) - actualTable := tableFromACL(t, acl, resInfoObject) - checkTables(t, expectedTable, actualTable) - - resInfoObjectVersion := &resourceInfo{ - Bucket: "bucketName", - Object: "objectVersion", - Version: "Gfrct4Afhio8pCGCCKVNTf1kyexQjMBeaUfvDtQCkAvg", - } - expectedTable = allowedTableForPrivateObject(t, key, resInfoObjectVersion) - actualTable = tableFromACL(t, acl, resInfoObjectVersion) - checkTables(t, expectedTable, actualTable) -} - -func allowedTableForPrivateObject(t *testing.T, key *keys.PrivateKey, resInfo *resourceInfo) *eacl.Table { - var isVersion bool - var objID oid.ID - if resInfo.Version != "" { - isVersion = true - err := objID.DecodeString(resInfo.Version) - require.NoError(t, err) - } - - expectedTable := eacl.NewTable() - serviceRec := &ServiceRecord{Resource: resInfo.Name(), GroupRecordsLength: len(readOps) * 2} - expectedTable.AddRecord(serviceRec.ToEACLRecord()) - - for i := len(readOps) - 1; i >= 0; i-- { - op := readOps[i] - record := getAllowRecord(op, key.PublicKey()) - if isVersion { - record.AddObjectIDFilter(eacl.MatchStringEqual, objID) - } else { - record.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, resInfo.Object) - } - expectedTable.AddRecord(record) - } - for i := len(readOps) - 1; i >= 0; i-- { - op := readOps[i] - record := getOthersRecord(op, eacl.ActionDeny) - if isVersion { - record.AddObjectIDFilter(eacl.MatchStringEqual, objID) - } else { - record.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, resInfo.Object) - } - expectedTable.AddRecord(record) - } - - return expectedTable -} - -func tableFromACL(t *testing.T, acl *AccessControlPolicy, resInfo *resourceInfo) *eacl.Table { - actualPolicy, err := aclToPolicy(acl, resInfo) - require.NoError(t, err) - actualAst, err := policyToAst(actualPolicy) - require.NoError(t, err) - actualTable, err := astToTable(actualAst) - require.NoError(t, err) - return actualTable -} - -func checkTables(t *testing.T, expectedTable, actualTable *eacl.Table) { - require.Equal(t, len(expectedTable.Records()), len(actualTable.Records()), "different number of records") - for i, record := range expectedTable.Records() { - actRecord := actualTable.Records()[i] - - require.Equal(t, len(record.Targets()), len(actRecord.Targets()), "different number of targets") - for j, target := range record.Targets() { - actTarget := actRecord.Targets()[j] - - expected := fmt.Sprintf("%s %v", target.Role().String(), target.BinaryKeys()) - actual := fmt.Sprintf("%s %v", actTarget.Role().String(), actTarget.BinaryKeys()) - require.Equalf(t, target, actTarget, "want: '%s'\ngot: '%s'", expected, actual) - } - - require.Equal(t, len(record.Filters()), len(actRecord.Filters()), "different number of filters") - for j, filter := range record.Filters() { - actFilter := actRecord.Filters()[j] - - expected := fmt.Sprintf("%s:%s %s %s", filter.From().String(), filter.Key(), filter.Matcher().String(), filter.Value()) - actual := fmt.Sprintf("%s:%s %s %s", actFilter.From().String(), actFilter.Key(), actFilter.Matcher().String(), actFilter.Value()) - require.Equalf(t, filter, actFilter, "want: '%s'\ngot: '%s'", expected, actual) - } - - require.Equal(t, record.Action().String(), actRecord.Action().String()) - require.Equal(t, record.Operation().String(), actRecord.Operation().String()) - } -} - func TestParseCannedACLHeaders(t *testing.T) { key, err := keys.NewPrivateKey() require.NoError(t, err) @@ -973,15 +620,15 @@ func TestParseCannedACLHeaders(t *testing.T) { Grantee: &Grantee{ ID: id, DisplayName: address, - Type: acpCanonicalUser, + Type: AcpCanonicalUser, }, - Permission: aclFullControl, + Permission: ACLFullControl, }, { Grantee: &Grantee{ - URI: allUsersGroup, - Type: acpGroup, + URI: AllUsersGroup, + Type: AcpGroup, }, - Permission: aclRead, + Permission: ACLRead, }}, } @@ -1000,7 +647,7 @@ func TestParseACLHeaders(t *testing.T) { req := &http.Request{ Header: map[string][]string{ api.AmzGrantFullControl: {"id=\"user1\""}, - api.AmzGrantRead: {"uri=\"" + allUsersGroup + "\", id=\"user2\""}, + api.AmzGrantRead: {"uri=\"" + AllUsersGroup + "\", id=\"user2\""}, api.AmzGrantWrite: {"id=\"user2\", id=\"user3\""}, }, } @@ -1014,39 +661,39 @@ func TestParseACLHeaders(t *testing.T) { Grantee: &Grantee{ ID: id, DisplayName: address, - Type: acpCanonicalUser, + Type: AcpCanonicalUser, }, - Permission: aclFullControl, + Permission: ACLFullControl, }, { Grantee: &Grantee{ ID: "user1", - Type: acpCanonicalUser, + Type: AcpCanonicalUser, }, - Permission: aclFullControl, + Permission: ACLFullControl, }, { Grantee: &Grantee{ - URI: allUsersGroup, - Type: acpGroup, + URI: AllUsersGroup, + Type: AcpGroup, }, - Permission: aclRead, + Permission: ACLRead, }, { Grantee: &Grantee{ ID: "user2", - Type: acpCanonicalUser, + Type: AcpCanonicalUser, }, - Permission: aclRead, + Permission: ACLRead, }, { Grantee: &Grantee{ ID: "user2", - Type: acpCanonicalUser, + Type: AcpCanonicalUser, }, - Permission: aclWrite, + Permission: ACLWrite, }, { Grantee: &Grantee{ ID: "user3", - Type: acpCanonicalUser, + Type: AcpCanonicalUser, }, - Permission: aclWrite, + Permission: ACLWrite, }}, } @@ -1088,57 +735,6 @@ func TestAddGranteeError(t *testing.T) { require.Nil(t, actualList) } -func TestBucketAclToTable(t *testing.T) { - 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{ - URI: allUsersGroup, - Type: acpGroup, - }, - Permission: aclRead, - }, { - Grantee: &Grantee{ - ID: id2, - Type: acpCanonicalUser, - }, - Permission: aclWrite, - }}, - } - - expectedTable := new(eacl.Table) - for _, op := range readOps { - expectedTable.AddRecord(getOthersRecord(op, eacl.ActionAllow)) - } - for _, op := range writeOps { - expectedTable.AddRecord(getAllowRecord(op, key2.PublicKey())) - } - for _, op := range fullOps { - expectedTable.AddRecord(getAllowRecord(op, key.PublicKey())) - } - for _, op := range fullOps { - expectedTable.AddRecord(getOthersRecord(op, eacl.ActionDeny)) - } - resInfo := &resourceInfo{ - Bucket: "bucketName", - } - - 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) @@ -1162,28 +758,28 @@ func TestObjectAclToAst(t *testing.T) { AccessControlList: []*Grant{{ Grantee: &Grantee{ ID: id, - Type: acpCanonicalUser, + Type: AcpCanonicalUser, }, - Permission: aclFullControl, + Permission: ACLFullControl, }, { Grantee: &Grantee{ ID: id2, - Type: acpCanonicalUser, + Type: AcpCanonicalUser, }, - Permission: aclRead, + Permission: ACLRead, }, }, } - resInfo := &resourceInfo{ + resInfo := &ResourceInfo{ Bucket: "bucketName", Object: "object", Version: objID.EncodeToString(), } - var operations []*astOperation - for _, op := range readOps { - astOp := &astOperation{Users: []string{ + var operations []*AstOperation + for _, op := range ReadOps { + astOp := &AstOperation{Users: []string{ hex.EncodeToString(key.PublicKey().Bytes()), hex.EncodeToString(key2.PublicKey().Bytes()), }, @@ -1193,10 +789,10 @@ func TestObjectAclToAst(t *testing.T) { operations = append(operations, astOp) } - expectedAst := &ast{ - Resources: []*astResource{ + expectedAst := &Ast{ + Resources: []*AstResource{ { - resourceInfo: *resInfo, + ResourceInfo: *resInfo, Operations: operations, }, }, @@ -1231,22 +827,22 @@ func TestBucketAclToAst(t *testing.T) { { Grantee: &Grantee{ ID: id2, - Type: acpCanonicalUser, + Type: AcpCanonicalUser, }, - Permission: aclWrite, + Permission: ACLWrite, }, { Grantee: &Grantee{ - URI: allUsersGroup, - Type: acpGroup, + URI: AllUsersGroup, + Type: AcpGroup, }, - Permission: aclRead, + Permission: ACLRead, }, }, } - var operations []*astOperation - for _, op := range readOps { - astOp := &astOperation{Users: []string{ + var operations []*AstOperation + for _, op := range ReadOps { + astOp := &AstOperation{Users: []string{ hex.EncodeToString(key.PublicKey().Bytes()), }, Op: op, @@ -1254,8 +850,8 @@ func TestBucketAclToAst(t *testing.T) { } operations = append(operations, astOp) } - for _, op := range writeOps { - astOp := &astOperation{Users: []string{ + for _, op := range WriteOps { + astOp := &AstOperation{Users: []string{ hex.EncodeToString(key.PublicKey().Bytes()), hex.EncodeToString(key2.PublicKey().Bytes()), }, @@ -1264,20 +860,20 @@ func TestBucketAclToAst(t *testing.T) { } operations = append(operations, astOp) } - for _, op := range readOps { - astOp := &astOperation{ + for _, op := range ReadOps { + astOp := &AstOperation{ Op: op, Action: eacl.ActionAllow, } operations = append(operations, astOp) } - resInfo := &resourceInfo{Bucket: "bucketName"} + resInfo := &ResourceInfo{Bucket: "bucketName"} - expectedAst := &ast{ - Resources: []*astResource{ + expectedAst := &Ast{ + Resources: []*AstResource{ { - resourceInfo: *resInfo, + ResourceInfo: *resInfo, Operations: operations, }, }, diff --git a/api/handler/api.go b/api/handler/api.go index c1e759b3..0465896a 100644 --- a/api/handler/api.go +++ b/api/handler/api.go @@ -15,6 +15,7 @@ type ( obj layer.Client notificator Notificator cfg *Config + NeoFS NeoFS } Notificator interface { diff --git a/api/handler/multipart_upload.go b/api/handler/multipart_upload.go index a7ac0f13..59883e01 100644 --- a/api/handler/multipart_upload.go +++ b/api/handler/multipart_upload.go @@ -383,7 +383,7 @@ func (h *handler) CompleteMultipartUploadHandler(w http.ResponseWriter, r *http. return } - resInfo := &resourceInfo{ + resInfo := &ResourceInfo{ Bucket: objInfo.Bucket, Object: objInfo.Name, } diff --git a/api/handler/neofs.go b/api/handler/neofs.go new file mode 100644 index 00000000..88ca69f9 --- /dev/null +++ b/api/handler/neofs.go @@ -0,0 +1,13 @@ +package handler + +import "github.com/nspcc-dev/neofs-sdk-go/eacl" + +type Ast struct { + Resources []*AstResource +} + +type NeoFS interface { + AstToTable(ast *Ast) (*eacl.Table, error) + TableToAst(table *eacl.Table, bktName string) *Ast + BucketACLToTable(acp *AccessControlPolicy, resInfo *ResourceInfo) (*eacl.Table, error) +} diff --git a/api/handler/put.go b/api/handler/put.go index 22318925..d91859f2 100644 --- a/api/handler/put.go +++ b/api/handler/put.go @@ -505,7 +505,7 @@ func (h *handler) getNewEAclTable(r *http.Request, bktInfo *data.BucketInfo, obj return nil, fmt.Errorf("could not parse object acl: %w", err) } - resInfo := &resourceInfo{ + resInfo := &ResourceInfo{ Bucket: objInfo.Bucket, Object: objInfo.Name, Version: objInfo.VersionID(), @@ -526,7 +526,7 @@ func (h *handler) getNewEAclTable(r *http.Request, bktInfo *data.BucketInfo, obj return nil, fmt.Errorf("could not get bucket eacl: %w", err) } - parentAst := tableToAst(bacl.EACL, objInfo.Bucket) + parentAst := h.NeoFS.TableToAst(bacl.EACL, objInfo.Bucket) strCID := bacl.Info.CID.EncodeToString() for _, resource := range parentAst.Resources { @@ -535,8 +535,8 @@ func (h *handler) getNewEAclTable(r *http.Request, bktInfo *data.BucketInfo, obj } } - if resAst, updated := mergeAst(parentAst, astChild); updated { - if newEaclTable, err = astToTable(resAst); err != nil { + if resAst, updated := MergeAst(parentAst, astChild); updated { + if newEaclTable, err = h.NeoFS.AstToTable(resAst); err != nil { return nil, fmt.Errorf("could not translate ast to table: %w", err) } } @@ -601,9 +601,9 @@ func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) { h.logAndSendError(w, "could not parse bucket acl", reqInfo, err) return } - resInfo := &resourceInfo{Bucket: reqInfo.BucketName} + resInfo := &ResourceInfo{Bucket: reqInfo.BucketName} - p.EACL, err = bucketACLToTable(bktACL, resInfo) + p.EACL, err = h.NeoFS.BucketACLToTable(bktACL, resInfo) if err != nil { h.logAndSendError(w, "could translate bucket acl to eacl", reqInfo, err) return diff --git a/internal/neofs/acl.go b/internal/neofs/acl.go new file mode 100644 index 00000000..d7bc1c79 --- /dev/null +++ b/internal/neofs/acl.go @@ -0,0 +1,255 @@ +package neofs + +import ( + "crypto/ecdsa" + "encoding/hex" + "fmt" + "sort" + "strconv" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" + "github.com/nspcc-dev/neofs-s3-gw/api/handler" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/nspcc-dev/neofs-sdk-go/object" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" +) + +type orderedAstResource struct { + Index int + Resource *handler.AstResource +} + +const ( + serviceRecordResourceKey = "Resource" + serviceRecordGroupLengthKey = "GroupLength" +) + +type serviceRecord struct { + Resource string + GroupRecordsLength int +} + +func (s serviceRecord) ToEACLRecord() *eacl.Record { + serviceRecord := eacl.NewRecord() + serviceRecord.SetAction(eacl.ActionAllow) + serviceRecord.SetOperation(eacl.OperationGet) + serviceRecord.AddFilter(eacl.HeaderFromService, eacl.MatchUnknown, serviceRecordResourceKey, s.Resource) + serviceRecord.AddFilter(eacl.HeaderFromService, eacl.MatchUnknown, serviceRecordGroupLengthKey, strconv.Itoa(s.GroupRecordsLength)) + eacl.AddFormedTarget(serviceRecord, eacl.RoleSystem) + return serviceRecord +} + +func formRecords(resource *handler.AstResource) ([]*eacl.Record, error) { + var res []*eacl.Record + + for i := len(resource.Operations) - 1; i >= 0; i-- { + astOp := resource.Operations[i] + record := eacl.NewRecord() + record.SetOperation(astOp.Op) + record.SetAction(astOp.Action) + if astOp.IsGroupGrantee() { + eacl.AddFormedTarget(record, eacl.RoleOthers) + } else { + targetKeys := make([]ecdsa.PublicKey, 0, len(astOp.Users)) + for _, user := range astOp.Users { + pk, err := keys.NewPublicKeyFromString(user) + if err != nil { + return nil, fmt.Errorf("public key from string: %w", err) + } + targetKeys = append(targetKeys, (ecdsa.PublicKey)(*pk)) + } + // Unknown role is used, because it is ignored when keys are set + eacl.AddFormedTarget(record, eacl.RoleUnknown, targetKeys...) + } + if len(resource.Object) != 0 { + if len(resource.Version) != 0 { + var id oid.ID + if err := id.DecodeString(resource.Version); err != nil { + return nil, fmt.Errorf("parse object version (oid): %w", err) + } + record.AddObjectIDFilter(eacl.MatchStringEqual, id) + } else { + record.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, resource.Object) + } + } + res = append(res, record) + } + + return res, nil +} + +func formReverseOrderResources(resourceMap map[string]orderedAstResource) []*handler.AstResource { + orderedResources := make([]orderedAstResource, 0, len(resourceMap)) + for _, resource := range resourceMap { + orderedResources = append(orderedResources, resource) + } + sort.Slice(orderedResources, func(i, j int) bool { + return orderedResources[i].Index >= orderedResources[j].Index // reverse order + }) + + result := make([]*handler.AstResource, len(orderedResources)) + for i, ordered := range orderedResources { + res := ordered.Resource + for j, k := 0, len(res.Operations)-1; j < k; j, k = j+1, k-1 { + res.Operations[j], res.Operations[k] = res.Operations[k], res.Operations[j] + } + + result[i] = res + } + + return result +} + +func addOperationsAndUpdateMap(orderedRes orderedAstResource, record eacl.Record, resMap map[string]orderedAstResource) { + for _, target := range record.Targets() { + orderedRes.Resource.Operations = addToList(orderedRes.Resource.Operations, record, target) + } + resMap[orderedRes.Resource.Name()] = orderedRes +} + +func getResourceOrCreate(resMap map[string]orderedAstResource, index int, resInfo handler.ResourceInfo) orderedAstResource { + resource, ok := resMap[resInfo.Name()] + if !ok { + resource = orderedAstResource{ + Index: index, + Resource: &handler.AstResource{ResourceInfo: resInfo}, + } + } + return resource +} + +func resInfoFromFilters(bucketName string, filters []eacl.Filter) handler.ResourceInfo { + resInfo := handler.ResourceInfo{Bucket: bucketName} + for _, filter := range filters { + if filter.Matcher() == eacl.MatchStringEqual { + if filter.Key() == object.AttributeFileName { + resInfo.Object = filter.Value() + } else if filter.Key() == v2acl.FilterObjectID { + resInfo.Version = filter.Value() + } + } + } + + return resInfo +} + +func tryServiceRecord(record eacl.Record) *serviceRecord { + if record.Action() != eacl.ActionAllow || record.Operation() != eacl.OperationGet || + len(record.Targets()) != 1 || len(record.Filters()) != 2 { + return nil + } + + target := record.Targets()[0] + if target.Role() != eacl.RoleSystem { + return nil + } + + resourceFilter := record.Filters()[0] + recordsFilter := record.Filters()[1] + if resourceFilter.From() != eacl.HeaderFromService || recordsFilter.From() != eacl.HeaderFromService || + resourceFilter.Matcher() != eacl.MatchUnknown || recordsFilter.Matcher() != eacl.MatchUnknown || + resourceFilter.Key() != serviceRecordResourceKey || recordsFilter.Key() != serviceRecordGroupLengthKey { + return nil + } + + groupLength, err := strconv.Atoi(recordsFilter.Value()) + if err != nil { + return nil + } + + return &serviceRecord{ + Resource: resourceFilter.Value(), + GroupRecordsLength: groupLength, + } +} + +func addToList(operations []*handler.AstOperation, rec eacl.Record, target eacl.Target) []*handler.AstOperation { + var ( + found *handler.AstOperation + groupTarget = target.Role() == eacl.RoleOthers + ) + + for _, astOp := range operations { + if astOp.Op == rec.Operation() && astOp.IsGroupGrantee() == groupTarget { + found = astOp + } + } + + if found != nil { + if !groupTarget { + for _, key := range target.BinaryKeys() { + found.Users = append(found.Users, hex.EncodeToString(key)) + } + } + } else { + astOperation := &handler.AstOperation{ + Op: rec.Operation(), + Action: rec.Action(), + } + if !groupTarget { + for _, key := range target.BinaryKeys() { + astOperation.Users = append(astOperation.Users, hex.EncodeToString(key)) + } + } + + operations = append(operations, astOperation) + } + + return operations +} + +type getRecordFunc func(op eacl.Operation) *eacl.Record + +func getRecordFunction(grantee *handler.Grantee) (getRecordFunc, error) { + switch grantee.Type { + case handler.AcpAmazonCustomerByEmail: + case handler.AcpCanonicalUser: + pk, err := keys.NewPublicKeyFromString(grantee.ID) + if err != nil { + return nil, fmt.Errorf("couldn't parse canonical ID %s: %w", grantee.ID, err) + } + return func(op eacl.Operation) *eacl.Record { + return getAllowRecord(op, pk) + }, nil + case handler.AcpGroup: + return func(op eacl.Operation) *eacl.Record { + return getOthersRecord(op, eacl.ActionAllow) + }, nil + } + return nil, fmt.Errorf("unknown type: %s", grantee.Type) +} + +func isValidGrant(grant *handler.Grant) bool { + return (grant.Permission == handler.ACLFullControl || grant.Permission == handler.ACLRead || grant.Permission == handler.ACLWrite) && + (grant.Grantee.Type == handler.AcpCanonicalUser || (grant.Grantee.Type == handler.AcpGroup && grant.Grantee.URI == handler.AllUsersGroup)) +} + +func permissionToOperations(permission handler.AWSACL) []eacl.Operation { + switch permission { + case handler.ACLFullControl: + return handler.FullOps + case handler.ACLRead: + return handler.ReadOps + case handler.ACLWrite: + return handler.WriteOps + } + return nil +} + +func getAllowRecord(op eacl.Operation, pk *keys.PublicKey) *eacl.Record { + record := eacl.NewRecord() + record.SetOperation(op) + record.SetAction(eacl.ActionAllow) + // Unknown role is used, because it is ignored when keys are set + eacl.AddFormedTarget(record, eacl.RoleUnknown, (ecdsa.PublicKey)(*pk)) + return record +} + +func getOthersRecord(op eacl.Operation, action eacl.Action) *eacl.Record { + record := eacl.NewRecord() + record.SetOperation(op) + record.SetAction(action) + eacl.AddFormedTarget(record, eacl.RoleOthers) + return record +} diff --git a/internal/neofs/acl_test.go b/internal/neofs/acl_test.go new file mode 100644 index 00000000..89b0e5f2 --- /dev/null +++ b/internal/neofs/acl_test.go @@ -0,0 +1,310 @@ +package neofs + +import ( + "crypto/ecdsa" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "io" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neofs-s3-gw/api/handler" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/nspcc-dev/neofs-sdk-go/object" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/stretchr/testify/require" +) + +func TestTableToAst(t *testing.T) { + neofs := NewNeoFS(nil) + b := make([]byte, 32) + _, err := io.ReadFull(rand.Reader, b) + require.NoError(t, err) + var id oid.ID + id.SetSHA256(sha256.Sum256(b)) + + key, err := keys.NewPrivateKey() + require.NoError(t, err) + key2, err := keys.NewPrivateKey() + require.NoError(t, err) + + table := new(eacl.Table) + record := eacl.NewRecord() + record.SetAction(eacl.ActionAllow) + record.SetOperation(eacl.OperationGet) + eacl.AddFormedTarget(record, eacl.RoleOthers) + table.AddRecord(record) + record2 := eacl.NewRecord() + record2.SetAction(eacl.ActionDeny) + record2.SetOperation(eacl.OperationPut) + // Unknown role is used, because it is ignored when keys are set + eacl.AddFormedTarget(record2, eacl.RoleUnknown, *(*ecdsa.PublicKey)(key.PublicKey()), *((*ecdsa.PublicKey)(key2.PublicKey()))) + record2.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, "objectName") + record2.AddObjectIDFilter(eacl.MatchStringEqual, id) + table.AddRecord(record2) + + expectedAst := &handler.Ast{ + Resources: []*handler.AstResource{ + { + ResourceInfo: handler.ResourceInfo{Bucket: "bucketName"}, + Operations: []*handler.AstOperation{{ + Op: eacl.OperationGet, + Action: eacl.ActionAllow, + }}}, + { + ResourceInfo: handler.ResourceInfo{ + Bucket: "bucketName", + Object: "objectName", + Version: id.EncodeToString(), + }, + Operations: []*handler.AstOperation{{ + Users: []string{ + hex.EncodeToString(key.PublicKey().Bytes()), + hex.EncodeToString(key2.PublicKey().Bytes()), + }, + Op: eacl.OperationPut, + Action: eacl.ActionDeny, + }}}, + }, + } + + actualAst := neofs.TableToAst(table, expectedAst.Resources[0].Bucket) + + if actualAst.Resources[0].Name() == expectedAst.Resources[0].Name() { + require.Equal(t, expectedAst, actualAst) + } else { + require.Equal(t, len(expectedAst.Resources), len(actualAst.Resources)) + require.Equal(t, expectedAst.Resources[0], actualAst.Resources[1]) + require.Equal(t, expectedAst.Resources[1], actualAst.Resources[0]) + } +} + +func TestAstToTable(t *testing.T) { + neofs := NewNeoFS(nil) + key, err := keys.NewPrivateKey() + require.NoError(t, err) + + ast := &handler.Ast{ + Resources: []*handler.AstResource{ + { + ResourceInfo: handler.ResourceInfo{ + Bucket: "bucketName", + }, + Operations: []*handler.AstOperation{{ + Users: []string{hex.EncodeToString(key.PublicKey().Bytes())}, + Op: eacl.OperationPut, + Action: eacl.ActionAllow, + }}, + }, + { + ResourceInfo: handler.ResourceInfo{ + Bucket: "bucketName", + Object: "objectName", + }, + Operations: []*handler.AstOperation{{ + Op: eacl.OperationGet, + Action: eacl.ActionDeny, + }}, + }, + }, + } + + expectedTable := eacl.NewTable() + serviceRec1 := &serviceRecord{Resource: ast.Resources[0].Name(), GroupRecordsLength: 1} + record1 := eacl.NewRecord() + record1.SetAction(eacl.ActionAllow) + record1.SetOperation(eacl.OperationPut) + // Unknown role is used, because it is ignored when keys are set + eacl.AddFormedTarget(record1, eacl.RoleUnknown, *(*ecdsa.PublicKey)(key.PublicKey())) + + serviceRec2 := &serviceRecord{Resource: ast.Resources[1].Name(), GroupRecordsLength: 1} + record2 := eacl.NewRecord() + record2.SetAction(eacl.ActionDeny) + record2.SetOperation(eacl.OperationGet) + eacl.AddFormedTarget(record2, eacl.RoleOthers) + record2.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, "objectName") + + expectedTable.AddRecord(serviceRec2.ToEACLRecord()) + expectedTable.AddRecord(record2) + expectedTable.AddRecord(serviceRec1.ToEACLRecord()) + expectedTable.AddRecord(record1) + + actualTable, err := neofs.AstToTable(ast) + require.NoError(t, err) + require.Equal(t, expectedTable, actualTable) +} + +func TestOrder(t *testing.T) { + neofs := NewNeoFS(nil) + key, err := keys.NewPrivateKey() + require.NoError(t, err) + users := []string{hex.EncodeToString(key.PublicKey().Bytes())} + targetUser := eacl.NewTarget() + targetUser.SetBinaryKeys([][]byte{key.PublicKey().Bytes()}) + targetOther := eacl.NewTarget() + targetOther.SetRole(eacl.RoleOthers) + bucketName := "bucket" + objectName := "objectName" + + expectedAst := &handler.Ast{ + Resources: []*handler.AstResource{ + { + ResourceInfo: handler.ResourceInfo{ + Bucket: bucketName, + }, + Operations: []*handler.AstOperation{ + { + Users: users, + Op: eacl.OperationGet, + Action: eacl.ActionAllow, + }, + { + Op: eacl.OperationGet, + Action: eacl.ActionDeny, + }, + }, + }, + { + ResourceInfo: handler.ResourceInfo{ + Bucket: bucketName, + Object: objectName, + }, + Operations: []*handler.AstOperation{ + { + Users: users, + Op: eacl.OperationPut, + Action: eacl.ActionAllow, + }, + { + Op: eacl.OperationPut, + Action: eacl.ActionDeny, + }, + }, + }, + }, + } + bucketServiceRec := &serviceRecord{Resource: expectedAst.Resources[0].Name(), GroupRecordsLength: 2} + bucketUsersGetRec := eacl.NewRecord() + bucketUsersGetRec.SetOperation(eacl.OperationGet) + bucketUsersGetRec.SetAction(eacl.ActionAllow) + bucketUsersGetRec.SetTargets(*targetUser) + bucketOtherGetRec := eacl.NewRecord() + bucketOtherGetRec.SetOperation(eacl.OperationGet) + bucketOtherGetRec.SetAction(eacl.ActionDeny) + bucketOtherGetRec.SetTargets(*targetOther) + objectServiceRec := &serviceRecord{Resource: expectedAst.Resources[1].Name(), GroupRecordsLength: 2} + objectUsersPutRec := eacl.NewRecord() + objectUsersPutRec.SetOperation(eacl.OperationPut) + objectUsersPutRec.SetAction(eacl.ActionAllow) + objectUsersPutRec.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, objectName) + objectUsersPutRec.SetTargets(*targetUser) + objectOtherPutRec := eacl.NewRecord() + objectOtherPutRec.SetOperation(eacl.OperationPut) + objectOtherPutRec.SetAction(eacl.ActionDeny) + objectOtherPutRec.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, objectName) + objectOtherPutRec.SetTargets(*targetOther) + + expectedEacl := eacl.NewTable() + expectedEacl.AddRecord(objectServiceRec.ToEACLRecord()) + expectedEacl.AddRecord(objectOtherPutRec) + expectedEacl.AddRecord(objectUsersPutRec) + expectedEacl.AddRecord(bucketServiceRec.ToEACLRecord()) + expectedEacl.AddRecord(bucketOtherGetRec) + expectedEacl.AddRecord(bucketUsersGetRec) + + t.Run("astToTable order and vice versa", func(t *testing.T) { + actualEacl, err := neofs.AstToTable(expectedAst) + require.NoError(t, err) + require.Equal(t, expectedEacl, actualEacl) + + actualAst := neofs.TableToAst(actualEacl, bucketName) + require.Equal(t, expectedAst, actualAst) + }) + + t.Run("tableToAst order and vice versa", func(t *testing.T) { + actualAst := neofs.TableToAst(expectedEacl, bucketName) + require.Equal(t, expectedAst, actualAst) + + actualEacl, err := neofs.AstToTable(actualAst) + require.NoError(t, err) + require.Equal(t, expectedEacl, actualEacl) + }) + + t.Run("append a resource", func(t *testing.T) { + childName := "child" + child := &handler.Ast{Resources: []*handler.AstResource{{ + ResourceInfo: handler.ResourceInfo{ + Bucket: bucketName, + Object: childName, + }, + Operations: []*handler.AstOperation{{Op: eacl.OperationDelete, Action: eacl.ActionDeny}}}}, + } + + childRecord := eacl.NewRecord() + childRecord.SetOperation(eacl.OperationDelete) + childRecord.SetAction(eacl.ActionDeny) + childRecord.SetTargets(*targetOther) + childRecord.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, childName) + + mergedAst, updated := handler.MergeAst(expectedAst, child) + require.True(t, updated) + + mergedEacl, err := neofs.AstToTable(mergedAst) + require.NoError(t, err) + + require.Equal(t, *childRecord, mergedEacl.Records()[1]) + }) +} + +func TestBucketAclToTable(t *testing.T) { + neofs := NewNeoFS(nil) + 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 := &handler.AccessControlPolicy{ + Owner: handler.Owner{ + ID: id, + DisplayName: "user1", + }, + AccessControlList: []*handler.Grant{{ + Grantee: &handler.Grantee{ + URI: handler.AllUsersGroup, + Type: handler.AcpGroup, + }, + Permission: handler.ACLRead, + }, { + Grantee: &handler.Grantee{ + ID: id2, + Type: handler.AcpCanonicalUser, + }, + Permission: handler.ACLWrite, + }}, + } + + expectedTable := new(eacl.Table) + for _, op := range handler.ReadOps { + expectedTable.AddRecord(getOthersRecord(op, eacl.ActionAllow)) + } + for _, op := range handler.WriteOps { + expectedTable.AddRecord(getAllowRecord(op, key2.PublicKey())) + } + for _, op := range handler.FullOps { + expectedTable.AddRecord(getAllowRecord(op, key.PublicKey())) + } + for _, op := range handler.FullOps { + expectedTable.AddRecord(getOthersRecord(op, eacl.ActionDeny)) + } + resInfo := &handler.ResourceInfo{ + Bucket: "bucketName", + } + + actualTable, err := neofs.BucketACLToTable(acl, resInfo) + require.NoError(t, err) + require.Equal(t, expectedTable.Records(), actualTable.Records()) +} diff --git a/internal/neofs/neofs.go b/internal/neofs/neofs.go index f74210d7..ba8cad36 100644 --- a/internal/neofs/neofs.go +++ b/internal/neofs/neofs.go @@ -4,13 +4,16 @@ import ( "bytes" "context" "errors" + stderrors "errors" "fmt" "io" "math" "strconv" "time" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" objectv2 "github.com/nspcc-dev/neofs-api-go/v2/object" + "github.com/nspcc-dev/neofs-s3-gw/api/handler" "github.com/nspcc-dev/neofs-s3-gw/api/layer" "github.com/nspcc-dev/neofs-s3-gw/authmate" "github.com/nspcc-dev/neofs-s3-gw/creds/tokens" @@ -470,6 +473,99 @@ func (x *NeoFS) DeleteObject(ctx context.Context, prm layer.PrmObjectDelete) err return nil } +func (x *NeoFS) AstToTable(ast *handler.Ast) (*eacl.Table, error) { + table := eacl.NewTable() + + for i := len(ast.Resources) - 1; i >= 0; i-- { + records, err := formRecords(ast.Resources[i]) + if err != nil { + return nil, fmt.Errorf("form records: %w", err) + } + + serviceRecord := serviceRecord{ + Resource: ast.Resources[i].Name(), + GroupRecordsLength: len(records), + } + table.AddRecord(serviceRecord.ToEACLRecord()) + + for _, rec := range records { + table.AddRecord(rec) + } + } + + return table, nil +} + +func (x *NeoFS) TableToAst(table *eacl.Table, bktName string) *handler.Ast { + resourceMap := make(map[string]orderedAstResource) + + var groupRecordsLeft int + var currentResource orderedAstResource + for i, record := range table.Records() { + if serviceRec := tryServiceRecord(record); serviceRec != nil { + resInfo := handler.ResourceInfoFromName(serviceRec.Resource, bktName) + groupRecordsLeft = serviceRec.GroupRecordsLength + + currentResource = getResourceOrCreate(resourceMap, i, resInfo) + resourceMap[resInfo.Name()] = currentResource + } else if groupRecordsLeft != 0 { + groupRecordsLeft-- + addOperationsAndUpdateMap(currentResource, record, resourceMap) + } else { + resInfo := resInfoFromFilters(bktName, record.Filters()) + resource := getResourceOrCreate(resourceMap, i, resInfo) + addOperationsAndUpdateMap(resource, record, resourceMap) + } + } + + return &handler.Ast{ + Resources: formReverseOrderResources(resourceMap), + } +} + +func (x *NeoFS) BucketACLToTable(acp *handler.AccessControlPolicy, resInfo *handler.ResourceInfo) (*eacl.Table, error) { + if !resInfo.IsBucket() { + return nil, fmt.Errorf("allowed only bucket acl") + } + + var found bool + table := eacl.NewTable() + + ownerKey, err := keys.NewPublicKeyFromString(acp.Owner.ID) + if err != nil { + return nil, fmt.Errorf("public key from string: %w", err) + } + + for _, grant := range acp.AccessControlList { + if !isValidGrant(grant) { + return nil, stderrors.New("unsupported grantee") + } + if grant.Grantee.ID == acp.Owner.ID { + found = true + } + + getRecord, err := getRecordFunction(grant.Grantee) + if err != nil { + return nil, fmt.Errorf("record func from grantee: %w", err) + } + for _, op := range permissionToOperations(grant.Permission) { + table.AddRecord(getRecord(op)) + } + } + + if !found { + for _, op := range handler.FullOps { + table.AddRecord(getAllowRecord(op, ownerKey)) + } + } + + for _, op := range handler.FullOps { + table.AddRecord(getOthersRecord(op, eacl.ActionDeny)) + } + + return table, nil +} + func isErrAccessDenied(err error) (string, bool) { unwrappedErr := errors.Unwrap(err) for unwrappedErr != nil {