eACL -> APE converter #800

Merged
fyrchik merged 1 commits from dstepanov-yadro/frostfs-node:feat/eacl_ape_converter into master 2023-11-15 15:12:31 +00:00
6 changed files with 749 additions and 30 deletions

View File

@ -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,
fyrchik marked this conversation as resolved Outdated

Also remove the constant ObjectActor?

Also remove the constant `ObjectActor`?
}
func parseConditions(lexemes []string) ([]policyengine.Condition, error) {

View File

@ -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,
fyrchik marked this conversation as resolved Outdated

Why not use constants here?

Why not use constants here?
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,

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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
fyrchik marked this conversation as resolved Outdated

Will we store binary non-UTF-8 data in Value field here?

Will we store binary non-UTF-8 data in `Value` field here?

Replaced with hex

Replaced with hex

Wanted some discussion here -- matching binary data is a valid usecase (all ids are in base58 now, public key in hex, but could be in binary).
There is another problem with public key -- it can have 2 formats in hex (short = 0x02|0x03 + 32 bytes or long = 0x04 + 64 bytes), both denote the same entity.

Looks ok now (in practice 0x04 encoding is rare), but maybe we could extend operations in policy engine, having this in mind.

Wanted some discussion here -- matching binary data is a valid usecase (all ids are in base58 now, public key in hex, but could be in binary). There is another problem with public key -- it can have 2 formats in hex (short = 0x02|0x03 + 32 bytes or long = 0x04 + 64 bytes), both denote the same entity. Looks ok now (in practice 0x04 encoding is rare), but maybe we could extend operations in policy engine, having this in mind.
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
aarifullin marked this conversation as resolved Outdated

The converter ignores else { /**/ } for filter.From() and filter.Matcher() and it means that invalid/incorrect filter

filter.from = eacl.HeaderTypeUnknown
filter.matcher = eacl.MatchUnknown

is converted to valid:

cond.Object = policyengine.ObjectResource
cond.Op = policyengine.CondStringEquals

Either we need to reject appending the cond to the result or we need to introduce policyengine.ObjectUnknown/policyengine.CondUnknown

The converter ignores `else { /**/ }` for `filter.From()` and `filter.Matcher()` and it means that invalid/incorrect filter ```golang filter.from = eacl.HeaderTypeUnknown filter.matcher = eacl.MatchUnknown ``` is converted to valid: ```golang cond.Object = policyengine.ObjectResource cond.Op = policyengine.CondStringEquals ``` Either we need to reject appending the `cond` to the result or we need to introduce `policyengine.ObjectUnknown/policyengine.CondUnknown`
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)}
}

View File

@ -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 }