diff --git a/cmd/frostfs-cli/modules/util/ape.go b/cmd/frostfs-cli/modules/util/ape.go index 88154645..47ce37bb 100644 --- a/cmd/frostfs-cli/modules/util/ape.go +++ b/cmd/frostfs-cli/modules/util/ape.go @@ -6,6 +6,7 @@ import ( "strings" policyengine "git.frostfs.info/TrueCloudLab/policy-engine" + nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" "github.com/flynn-archive/go-shlex" ) @@ -65,7 +66,7 @@ func parseRuleLexemes(r *policyengine.Rule, lexemes []string) error { return err } - r.Action, err = parseAction(lexemes[1]) + r.Actions, err = parseAction(lexemes[1]) if err != nil { return err } @@ -75,7 +76,7 @@ func parseRuleLexemes(r *policyengine.Rule, lexemes []string) error { return err } - r.Resource, err = parseResource(lexemes[len(lexemes)-1]) + r.Resources, err = parseResource(lexemes[len(lexemes)-1]) return err } @@ -100,41 +101,42 @@ func parseStatus(lexeme string) (policyengine.Status, error) { } } -func parseAction(lexeme string) ([]string, error) { +func parseAction(lexeme string) (policyengine.Actions, error) { switch strings.ToLower(lexeme) { case "object.put": - return []string{"native:PutObject"}, nil + return policyengine.Actions{Names: []string{nativeschema.MethodPutObject}}, nil case "object.get": - return []string{"native:GetObject"}, nil + return policyengine.Actions{Names: []string{nativeschema.MethodGetObject}}, nil case "object.head": - return []string{"native:HeadObject"}, nil + return policyengine.Actions{Names: []string{nativeschema.MethodHeadObject}}, nil case "object.delete": - return []string{"native:DeleteObject"}, nil + return policyengine.Actions{Names: []string{nativeschema.MethodDeleteObject}}, nil case "object.search": - return []string{"native:SearchObject"}, nil + return policyengine.Actions{Names: []string{nativeschema.MethodSearchObject}}, nil case "object.range": - return []string{"native:RangeObject"}, nil + return policyengine.Actions{Names: []string{nativeschema.MethodRangeObject}}, nil case "object.hash": - return []string{"native:HashObject"}, nil + return policyengine.Actions{Names: []string{nativeschema.MethodHashObject}}, nil default: } - return nil, fmt.Errorf("%w: %s", errUnknownOperation, lexeme) + return policyengine.Actions{}, fmt.Errorf("%w: %s", errUnknownOperation, lexeme) } -func parseResource(lexeme string) ([]string, error) { - return []string{fmt.Sprintf("native:::object/%s", lexeme)}, nil +func parseResource(lexeme string) (policyengine.Resources, error) { + if lexeme == "*" { + return policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, nil + } + return policyengine.Resources{Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, lexeme)}}, nil } const ( ObjectResource = "object.resource" ObjectRequest = "object.request" - ObjectActor = "object.actor" ) var typeToCondObject = map[string]policyengine.ObjectType{ ObjectResource: policyengine.ObjectResource, ObjectRequest: policyengine.ObjectRequest, - ObjectActor: policyengine.ObjectActor, } func parseConditions(lexemes []string) ([]policyengine.Condition, error) { diff --git a/cmd/frostfs-cli/modules/util/ape_test.go b/cmd/frostfs-cli/modules/util/ape_test.go index e71544a9..b2e9d1d8 100644 --- a/cmd/frostfs-cli/modules/util/ape_test.go +++ b/cmd/frostfs-cli/modules/util/ape_test.go @@ -4,6 +4,7 @@ import ( "testing" policyengine "git.frostfs.info/TrueCloudLab/policy-engine" + nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" "github.com/stretchr/testify/require" ) @@ -19,8 +20,8 @@ func TestParseAPERule(t *testing.T) { rule: "allow Object.Put *", expectRule: policyengine.Rule{ Status: policyengine.Allow, - Action: []string{"native:PutObject"}, - Resource: []string{"native:::object/*"}, + Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}}, + Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, Condition: []policyengine.Condition{}, }, }, @@ -29,8 +30,8 @@ func TestParseAPERule(t *testing.T) { rule: "deny Object.Put *", expectRule: policyengine.Rule{ Status: policyengine.AccessDenied, - Action: []string{"native:PutObject"}, - Resource: []string{"native:::object/*"}, + Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}}, + Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, Condition: []policyengine.Condition{}, }, }, @@ -39,8 +40,8 @@ func TestParseAPERule(t *testing.T) { rule: "deny:QuotaLimitReached Object.Put *", expectRule: policyengine.Rule{ Status: policyengine.QuotaLimitReached, - Action: []string{"native:PutObject"}, - Resource: []string{"native:::object/*"}, + Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}}, + Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, Condition: []policyengine.Condition{}, }, }, @@ -48,9 +49,9 @@ func TestParseAPERule(t *testing.T) { name: "Valid allow rule with conditions", rule: "allow Object.Get Object.Resource:Department=HR Object.Request:Actor!=ownerA *", expectRule: policyengine.Rule{ - Status: policyengine.Allow, - Action: []string{"native:GetObject"}, - Resource: []string{"native:::object/*"}, + Status: policyengine.Allow, + Actions: policyengine.Actions{Names: []string{nativeschema.MethodGetObject}}, + Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, Condition: []policyengine.Condition{ { Op: policyengine.CondStringEquals, @@ -71,9 +72,9 @@ func TestParseAPERule(t *testing.T) { name: "Valid rule with conditions with action detail", rule: "deny:QuotaLimitReached Object.Get Object.Resource:Department=HR Object.Request:Actor!=ownerA *", expectRule: policyengine.Rule{ - Status: policyengine.QuotaLimitReached, - Action: []string{"native:GetObject"}, - Resource: []string{"native:::object/*"}, + Status: policyengine.QuotaLimitReached, + Actions: policyengine.Actions{Names: []string{nativeschema.MethodGetObject}}, + Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, Condition: []policyengine.Condition{ { Op: policyengine.CondStringEquals, diff --git a/go.mod b/go.mod index 5c22fdfc..5def2fad 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20231101111734-b3ad3335ff65 git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231101144515-6fbe1595cb3d git.frostfs.info/TrueCloudLab/hrw v1.2.1 - git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20231101082425-5eee1a733432 + git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20231114100951-38985e4ec86b git.frostfs.info/TrueCloudLab/tzhash v1.8.0 github.com/cheggaaa/pb v1.0.29 github.com/chzyer/readline v1.5.1 diff --git a/go.sum b/go.sum index 80664825..5c30a75e 100644 --- a/go.sum +++ b/go.sum @@ -736,8 +736,8 @@ git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231101144515-6fbe1595cb3d git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231101144515-6fbe1595cb3d/go.mod h1:t1akKcUH7iBrFHX8rSXScYMP17k2kYQXMbZooiL5Juw= git.frostfs.info/TrueCloudLab/hrw v1.2.1 h1:ccBRK21rFvY5R1WotI6LNoPlizk7qSvdfD8lNIRudVc= git.frostfs.info/TrueCloudLab/hrw v1.2.1/go.mod h1:C1Ygde2n843yTZEQ0FP69jYiuaYV0kriLvP4zm8JuvM= -git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20231101082425-5eee1a733432 h1:z0PqdiEIHXK2qC83e6pmxUZ5peP9CIL0Bh5mP/d+4Xc= -git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20231101082425-5eee1a733432/go.mod h1:qf3B9hSz6gCMfcfvqkhTu5ak+Gx2R+wo4Hc87LnKxPg= +git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20231114100951-38985e4ec86b h1:io+LMCjNfP1IA7Jku7QVzAnlBjJXDNBNrHw1fpbsoeI= +git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20231114100951-38985e4ec86b/go.mod h1:qf3B9hSz6gCMfcfvqkhTu5ak+Gx2R+wo4Hc87LnKxPg= git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 h1:M2KR3iBj7WpY3hP10IevfIB9MURr4O9mwVfJ+SjT3HA= git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0/go.mod h1:okpbKfVYf/BpejtfFTfhZqFP+sZ8rsHrP8Rr/jYPNRc= git.frostfs.info/TrueCloudLab/tzhash v1.8.0 h1:UFMnUIk0Zh17m8rjGHJMqku2hCgaXDqjqZzS4gsb4UA= diff --git a/internal/ape/converter.go b/internal/ape/converter.go new file mode 100644 index 00000000..e268626e --- /dev/null +++ b/internal/ape/converter.go @@ -0,0 +1,246 @@ +package ape + +import ( + "encoding/hex" + "fmt" + + v2acl "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl" + policyengine "git.frostfs.info/TrueCloudLab/policy-engine" + nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" +) + +type ConvertEACLError struct { + nested error +} + +func (e *ConvertEACLError) Error() string { + if e == nil { + return "" + } + return fmt.Sprintf("failed to convert eACL table to policy engine chain: %s", e.nested.Error()) +} + +func (e *ConvertEACLError) Unwrap() error { + if e == nil { + return nil + } + return e.nested +} + +// ConvertEACLToAPE converts eacl.Table to policyengine.Chain. +func ConvertEACLToAPE(eaclTable *eacl.Table) (*policyengine.Chain, error) { + if eaclTable == nil { + return nil, nil + } + res := &policyengine.Chain{} + + resource := getResource(eaclTable) + + for _, eaclRecord := range eaclTable.Records() { + if len(eaclRecord.Targets()) == 0 { + // see https://git.frostfs.info/TrueCloudLab/frostfs-sdk-go/src/commit/ab75edd70939564421936d207ef80d6c1398b51b/eacl/validator.go#L101 + // and https://git.frostfs.info/TrueCloudLab/frostfs-sdk-go/src/commit/ab75edd70939564421936d207ef80d6c1398b51b/eacl/validator.go#L36 + // such record doesn't have any effect + continue + } + + st, err := actionToStatus(eaclRecord.Action()) + if err != nil { + return nil, err + } + act, err := operationToAction(eaclRecord.Operation()) + if err != nil { + return nil, err + } + + if len(eaclRecord.Filters()) == 0 { + res.Rules = appendTargetsOnly(res.Rules, st, act, resource, eaclRecord.Targets()) + } else { + res.Rules, err = appendTargetsAndFilters(res.Rules, st, act, resource, eaclRecord.Targets(), eaclRecord.Filters()) + if err != nil { + return nil, err + } + } + } + + return res, nil +} + +func appendTargetsOnly(source []policyengine.Rule, st policyengine.Status, act policyengine.Actions, res policyengine.Resources, targets []eacl.Target) []policyengine.Rule { + // see https://git.frostfs.info/TrueCloudLab/frostfs-sdk-go/src/commit/ab75edd70939564421936d207ef80d6c1398b51b/eacl/validator.go#L101 + // role OR public key must be equal + rule := policyengine.Rule{ + Status: st, + Actions: act, + Resources: res, + Any: true, + } + for _, target := range targets { + var roleCondition policyengine.Condition + roleCondition.Object = policyengine.ObjectRequest + roleCondition.Key = nativeschema.PropertyKeyActorRole + roleCondition.Value = target.Role().String() + roleCondition.Op = policyengine.CondStringEquals + rule.Condition = append(rule.Condition, roleCondition) + + for _, binKey := range target.BinaryKeys() { + var pubKeyCondition policyengine.Condition + pubKeyCondition.Object = policyengine.ObjectRequest + pubKeyCondition.Key = nativeschema.PropertyKeyActorPublicKey + pubKeyCondition.Value = hex.EncodeToString(binKey) + pubKeyCondition.Op = policyengine.CondStringEquals + rule.Condition = append(rule.Condition, pubKeyCondition) + } + } + return append(source, rule) +} + +func appendTargetsAndFilters(source []policyengine.Rule, st policyengine.Status, act policyengine.Actions, res policyengine.Resources, + targets []eacl.Target, filters []eacl.Filter, +) ([]policyengine.Rule, error) { + // see https://git.frostfs.info/TrueCloudLab/frostfs-sdk-go/src/commit/ab75edd70939564421936d207ef80d6c1398b51b/eacl/validator.go#L101 + // role OR public key must be equal + // so filters are repeated for each role and public key + var err error + for _, target := range targets { + rule := policyengine.Rule{ + Status: st, + Actions: act, + Resources: res, + } + var roleCondition policyengine.Condition + roleCondition.Object = policyengine.ObjectRequest + roleCondition.Key = nativeschema.PropertyKeyActorRole + roleCondition.Value = target.Role().String() + roleCondition.Op = policyengine.CondStringEquals + + rule.Condition = append(rule.Condition, roleCondition) + rule.Condition, err = appendFilters(rule.Condition, filters) + if err != nil { + return nil, err + } + + source = append(source, rule) + + for _, binKey := range target.BinaryKeys() { + rule := policyengine.Rule{ + Status: st, + Actions: act, + Resources: res, + } + var pubKeyCondition policyengine.Condition + pubKeyCondition.Object = policyengine.ObjectRequest + pubKeyCondition.Key = nativeschema.PropertyKeyActorPublicKey + pubKeyCondition.Value = hex.EncodeToString(binKey) + pubKeyCondition.Op = policyengine.CondStringEquals + + rule.Condition = append(rule.Condition, pubKeyCondition) + rule.Condition, err = appendFilters(rule.Condition, filters) + if err != nil { + return nil, err + } + + source = append(source, rule) + } + } + + return source, nil +} + +func appendFilters(source []policyengine.Condition, filters []eacl.Filter) ([]policyengine.Condition, error) { + for _, filter := range filters { + var cond policyengine.Condition + var isObject bool + if filter.From() == eacl.HeaderFromObject { + cond.Object = policyengine.ObjectResource + isObject = true + } else if filter.From() == eacl.HeaderFromRequest { + cond.Object = policyengine.ObjectRequest + } else { + return nil, &ConvertEACLError{nested: fmt.Errorf("unknown filter from: %d", filter.From())} + } + + if filter.Matcher() == eacl.MatchStringEqual { + cond.Op = policyengine.CondStringEquals + } else if filter.Matcher() == eacl.MatchStringNotEqual { + cond.Op = policyengine.CondStringNotEquals + } else { + return nil, &ConvertEACLError{nested: fmt.Errorf("unknown filter matcher: %d", filter.Matcher())} + } + + cond.Key = eaclKeyToAPEKey(filter.Key(), isObject) + cond.Value = filter.Value() + + source = append(source, cond) + } + return source, nil +} + +func eaclKeyToAPEKey(key string, isObject bool) string { + if !isObject { + return key + } + switch key { + default: + return key + case v2acl.FilterObjectVersion: + return nativeschema.PropertyKeyObjectVersion + case v2acl.FilterObjectID: + return nativeschema.PropertyKeyObjectID + case v2acl.FilterObjectContainerID: + return nativeschema.PropertyKeyObjectContainerID + case v2acl.FilterObjectOwnerID: + return nativeschema.PropertyKeyObjectOwnerID + case v2acl.FilterObjectCreationEpoch: + return nativeschema.PropertyKeyObjectCreationEpoch + case v2acl.FilterObjectPayloadLength: + return nativeschema.PropertyKeyObjectPayloadLength + case v2acl.FilterObjectPayloadHash: + return nativeschema.PropertyKeyObjectPayloadHash + case v2acl.FilterObjectType: + return nativeschema.PropertyKeyObjectType + case v2acl.FilterObjectHomomorphicHash: + return nativeschema.PropertyKeyObjectHomomorphicHash + } +} + +func getResource(eaclTable *eacl.Table) policyengine.Resources { + cnrID, isSet := eaclTable.CID() + if isSet { + return policyengine.Resources{ + Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString())}, + } + } + return policyengine.Resources{ + Names: []string{nativeschema.ResourceFormatRootObjects}, + } +} + +func actionToStatus(a eacl.Action) (policyengine.Status, error) { + switch a { + case eacl.ActionAllow: + return policyengine.Allow, nil + case eacl.ActionDeny: + return policyengine.AccessDenied, nil + default: + return policyengine.NoRuleFound, &ConvertEACLError{nested: fmt.Errorf("unknown action: %d", a)} + } +} + +var eaclOperationToEngineAction = map[eacl.Operation]policyengine.Actions{ + eacl.OperationGet: {Names: []string{nativeschema.MethodGetObject}}, + eacl.OperationHead: {Names: []string{nativeschema.MethodHeadObject}}, + eacl.OperationPut: {Names: []string{nativeschema.MethodPutObject}}, + eacl.OperationDelete: {Names: []string{nativeschema.MethodDeleteObject}}, + eacl.OperationSearch: {Names: []string{nativeschema.MethodSearchObject}}, + eacl.OperationRange: {Names: []string{nativeschema.MethodRangeObject}}, + eacl.OperationRangeHash: {Names: []string{nativeschema.MethodHashObject}}, +} + +func operationToAction(op eacl.Operation) (policyengine.Actions, error) { + if v, ok := eaclOperationToEngineAction[op]; ok { + return v, nil + } + return policyengine.Actions{}, &ConvertEACLError{nested: fmt.Errorf("unknown operation: %d", op)} +} diff --git a/internal/ape/converter_test.go b/internal/ape/converter_test.go new file mode 100644 index 00000000..bf6205ec --- /dev/null +++ b/internal/ape/converter_test.go @@ -0,0 +1,470 @@ +package ape + +import ( + "encoding/hex" + "fmt" + "testing" + + cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl" + policyengine "git.frostfs.info/TrueCloudLab/policy-engine" + nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/stretchr/testify/require" +) + +func TestEACLTableWithoutRecords(t *testing.T) { + t.Parallel() + + tb := eacl.NewTable() + ch, err := ConvertEACLToAPE(tb) + require.NoError(t, err) + + vu := &eacl.ValidationUnit{} + vu.WithEACLTable(tb) + req := &testRequest{ + res: &testResource{name: nativeschema.ResourceFormatRootObjects}, + } + + compare(t, tb, vu, ch, req) + + cnrID := cidtest.ID() + tb.SetCID(cnrID) + vu.WithContainerID(&cnrID) + req.res.name = fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString()) + + ch, err = ConvertEACLToAPE(tb) + require.NoError(t, err) + + compare(t, tb, vu, ch, req) +} + +func TestNoTargets(t *testing.T) { + t.Parallel() + for _, act := range []eacl.Action{eacl.ActionAllow, eacl.ActionDeny} { + cnrID := cidtest.ID() + tb := eacl.NewTable() + tb.SetCID(cnrID) + + vu := &eacl.ValidationUnit{} + vu.WithEACLTable(tb) + vu.WithContainerID(&cnrID) + vu.WithRole(eacl.RoleOthers) + + // deny delete without role or key specified + record := eacl.NewRecord() + record.SetAction(act) + record.SetOperation(eacl.OperationDelete) + record.AddObjectContainerIDFilter(eacl.MatchStringEqual, cnrID) + + tb.AddRecord(record) + + ch, err := ConvertEACLToAPE(tb) + require.NoError(t, err) + + req := &testRequest{ + props: map[string]string{ + nativeschema.PropertyKeyActorRole: eacl.RoleOthers.String(), + }, + res: &testResource{name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString())}, + } + compare(t, tb, vu, ch, req) + } +} + +func TestNoFilters(t *testing.T) { + t.Parallel() + + t.Run("target match by role only", func(t *testing.T) { + t.Parallel() + + for _, act := range []eacl.Action{eacl.ActionAllow, eacl.ActionDeny} { + cnrID := cidtest.ID() + tb := eacl.NewTable() + tb.SetCID(cnrID) + + vu := &eacl.ValidationUnit{} + vu.WithEACLTable(tb) + vu.WithContainerID(&cnrID) + vu.WithRole(eacl.RoleOthers) + + // allow/deny for OTHERS + record := eacl.NewRecord() + record.SetAction(act) + record.SetOperation(eacl.OperationDelete) + + target := eacl.NewTarget() + target.SetRole(eacl.RoleOthers) + record.SetTargets(*target) + + tb.AddRecord(record) + + ch, err := ConvertEACLToAPE(tb) + require.NoError(t, err) + + req := &testRequest{ + props: map[string]string{ + nativeschema.PropertyKeyActorRole: eacl.RoleOthers.String(), + }, + res: &testResource{name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString())}, + } + compare(t, tb, vu, ch, req) + } + }) + + t.Run("target match by role and public key", func(t *testing.T) { + t.Parallel() + + for _, act := range []eacl.Action{eacl.ActionAllow, eacl.ActionDeny} { + cnrID := cidtest.ID() + tb := eacl.NewTable() + tb.SetCID(cnrID) + + vu := &eacl.ValidationUnit{} + vu.WithEACLTable(tb) + vu.WithContainerID(&cnrID) + vu.WithRole(eacl.RoleOthers) + + // allow/deny for OTHERS + record := eacl.NewRecord() + record.SetAction(act) + record.SetOperation(eacl.OperationDelete) + + p1, err := keys.NewPrivateKey() + require.NoError(t, err) + p2, err := keys.NewPrivateKey() + require.NoError(t, err) + + vu.WithSenderKey(p2.PublicKey().Bytes()) + + target := eacl.NewTarget() + target.SetRole(eacl.RoleOthers) + target.SetBinaryKeys([][]byte{p1.PublicKey().Bytes(), p2.PublicKey().Bytes()}) + record.SetTargets(*target) + + tb.AddRecord(record) + + ch, err := ConvertEACLToAPE(tb) + require.NoError(t, err) + + req := &testRequest{ + props: map[string]string{ + nativeschema.PropertyKeyActorRole: eacl.RoleOthers.String(), + nativeschema.PropertyKeyActorPublicKey: string(p2.PublicKey().Bytes()), + }, + res: &testResource{name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString())}, + } + compare(t, tb, vu, ch, req) + } + }) + + t.Run("target match by public key only", func(t *testing.T) { + t.Parallel() + + for _, act := range []eacl.Action{eacl.ActionAllow, eacl.ActionDeny} { + cnrID := cidtest.ID() + tb := eacl.NewTable() + tb.SetCID(cnrID) + + vu := &eacl.ValidationUnit{} + vu.WithEACLTable(tb) + vu.WithContainerID(&cnrID) + + // allow/deny for OTHERS + record := eacl.NewRecord() + record.SetAction(act) + record.SetOperation(eacl.OperationDelete) + + p1, err := keys.NewPrivateKey() + require.NoError(t, err) + p2, err := keys.NewPrivateKey() + require.NoError(t, err) + + vu.WithSenderKey(p2.PublicKey().Bytes()) + + target := eacl.NewTarget() + target.SetRole(eacl.RoleOthers) + target.SetBinaryKeys([][]byte{p1.PublicKey().Bytes(), p2.PublicKey().Bytes()}) + record.SetTargets(*target) + + tb.AddRecord(record) + + ch, err := ConvertEACLToAPE(tb) + require.NoError(t, err) + + req := &testRequest{ + props: map[string]string{ + nativeschema.PropertyKeyActorPublicKey: hex.EncodeToString(p2.PublicKey().Bytes()), + }, + res: &testResource{name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString())}, + } + compare(t, tb, vu, ch, req) + } + }) + + t.Run("target doesn't match", func(t *testing.T) { + t.Parallel() + + for _, act := range []eacl.Action{eacl.ActionAllow, eacl.ActionDeny} { + cnrID := cidtest.ID() + tb := eacl.NewTable() + tb.SetCID(cnrID) + + vu := &eacl.ValidationUnit{} + vu.WithEACLTable(tb) + vu.WithContainerID(&cnrID) + vu.WithRole(eacl.RoleSystem) + + // allow/deny for OTHERS + record := eacl.NewRecord() + record.SetAction(act) + record.SetOperation(eacl.OperationDelete) + + target := eacl.NewTarget() + target.SetRole(eacl.RoleOthers) + record.SetTargets(*target) + + tb.AddRecord(record) + + ch, err := ConvertEACLToAPE(tb) + require.NoError(t, err) + + req := &testRequest{ + props: map[string]string{ + nativeschema.PropertyKeyActorRole: eacl.RoleSystem.String(), + }, + res: &testResource{name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString())}, + } + compare(t, tb, vu, ch, req) + } + }) +} + +func TestWithFilters(t *testing.T) { + t.Parallel() + + t.Run("object attributes", func(t *testing.T) { + t.Parallel() + + const attrKey = "attribute_1" + const attrValue = "attribute_1_value" + + for _, act := range []eacl.Action{eacl.ActionAllow, eacl.ActionDeny} { + cnrID := cidtest.ID() + tb := eacl.NewTable() + tb.SetCID(cnrID) + + vu := &eacl.ValidationUnit{} + vu.WithEACLTable(tb) + vu.WithContainerID(&cnrID) + vu.WithRole(eacl.RoleOthers) + vu.WithHeaderSource(&testHeaderSource{ + headers: map[eacl.FilterHeaderType][]eacl.Header{ + eacl.HeaderFromObject: {&testHeader{key: attrKey, value: attrValue}}, + }, + }) + + // allow/deny for OTHERS + record := eacl.NewRecord() + record.SetAction(act) + record.SetOperation(eacl.OperationDelete) + + target := eacl.NewTarget() + target.SetRole(eacl.RoleOthers) + record.SetTargets(*target) + + record.AddObjectAttributeFilter(eacl.MatchStringEqual, attrKey, attrValue) + + tb.AddRecord(record) + + ch, err := ConvertEACLToAPE(tb) + require.NoError(t, err) + + req := &testRequest{ + props: map[string]string{ + nativeschema.PropertyKeyActorRole: eacl.RoleOthers.String(), + }, + res: &testResource{ + name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString()), + props: map[string]string{ + attrKey: attrValue, + }, + }, + } + compare(t, tb, vu, ch, req) + } + }) + + t.Run("request attributes", func(t *testing.T) { + t.Parallel() + + const attrKey = "attribute_1" + const attrValue = "attribute_1_value" + + for _, act := range []eacl.Action{eacl.ActionAllow, eacl.ActionDeny} { + cnrID := cidtest.ID() + tb := eacl.NewTable() + tb.SetCID(cnrID) + + vu := &eacl.ValidationUnit{} + vu.WithEACLTable(tb) + vu.WithContainerID(&cnrID) + vu.WithRole(eacl.RoleOthers) + vu.WithHeaderSource(&testHeaderSource{ + headers: map[eacl.FilterHeaderType][]eacl.Header{ + eacl.HeaderFromRequest: {&testHeader{key: attrKey, value: attrValue}}, + }, + }) + + // allow/deny for OTHERS + record := eacl.NewRecord() + record.SetAction(act) + record.SetOperation(eacl.OperationDelete) + + target := eacl.NewTarget() + target.SetRole(eacl.RoleOthers) + record.SetTargets(*target) + + record.AddFilter(eacl.HeaderFromRequest, eacl.MatchStringEqual, attrKey, attrValue) + + tb.AddRecord(record) + + ch, err := ConvertEACLToAPE(tb) + require.NoError(t, err) + + req := &testRequest{ + props: map[string]string{ + nativeschema.PropertyKeyActorRole: eacl.RoleOthers.String(), + attrKey: attrValue, + }, + res: &testResource{ + name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString()), + }, + } + compare(t, tb, vu, ch, req) + } + }) +} + +func TestNoHeader(t *testing.T) { + t.Skip("Should pass after https://git.frostfs.info/TrueCloudLab/policy-engine/issues/8#issuecomment-26126") + + t.Parallel() + + const attrKey = "attribute_1" + cnrID := cidtest.ID() + tb := eacl.NewTable() + tb.SetCID(cnrID) + + vu := &eacl.ValidationUnit{} + vu.WithEACLTable(tb) + vu.WithContainerID(&cnrID) + vu.WithRole(eacl.RoleOthers) + vu.WithHeaderSource(&testHeaderSource{ + headers: map[eacl.FilterHeaderType][]eacl.Header{ + eacl.HeaderFromRequest: {}, + }, + }) + + // allow/deny for OTHERS + record := eacl.NewRecord() + record.SetAction(eacl.ActionDeny) + record.SetOperation(eacl.OperationDelete) + + target := eacl.NewTarget() + target.SetRole(eacl.RoleOthers) + record.SetTargets(*target) + + record.AddFilter(eacl.HeaderFromRequest, eacl.MatchStringEqual, attrKey, "") + + tb.AddRecord(record) + + ch, err := ConvertEACLToAPE(tb) + require.NoError(t, err) + + req := &testRequest{ + props: map[string]string{ + nativeschema.PropertyKeyActorRole: eacl.RoleOthers.String(), + }, + res: &testResource{ + name: fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString()), + }, + } + compare(t, tb, vu, ch, req) +} + +func compare(t *testing.T, eaclTable *eacl.Table, vu *eacl.ValidationUnit, ch *policyengine.Chain, req *testRequest) { + validator := eacl.NewValidator() + for eaclOp, apeOp := range eaclOperationToEngineAction { + vu.WithOperation(eaclOp) + req.op = apeOp.Names[0] + + eaclAct, recordFound := validator.CalculateAction(vu) + apeSt, ruleFound := ch.Match(req) + + require.Equal(t, recordFound, ruleFound) + require.NotEqual(t, eacl.ActionUnknown, eaclAct) + if eaclAct == eacl.ActionAllow { + if recordFound { + require.Equal(t, policyengine.Allow, apeSt) + } else { + require.Equal(t, policyengine.NoRuleFound, apeSt) + } + } else { + require.Equal(t, policyengine.AccessDenied, apeSt) + } + } +} + +type testRequest struct { + op string + props map[string]string + res *testResource +} + +func (r *testRequest) Operation() string { + return r.op +} + +func (r *testRequest) Property(key string) string { + if v, ok := r.props[key]; ok { + return v + } + return "" +} + +func (r *testRequest) Resource() policyengine.Resource { + return r.res +} + +type testResource struct { + name string + props map[string]string +} + +func (r *testResource) Name() string { + return r.name +} + +func (r *testResource) Property(key string) string { + if v, ok := r.props[key]; ok { + return v + } + return "" +} + +type testHeaderSource struct { + headers map[eacl.FilterHeaderType][]eacl.Header +} + +func (s *testHeaderSource) HeadersOfType(t eacl.FilterHeaderType) ([]eacl.Header, bool) { + v, ok := s.headers[t] + return v, ok +} + +type testHeader struct { + key, value string +} + +func (h *testHeader) Key() string { return h.key } +func (h *testHeader) Value() string { return h.value }