eACL -> APE converter #800

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

View file

@ -6,6 +6,7 @@ import (
"strings" "strings"
policyengine "git.frostfs.info/TrueCloudLab/policy-engine" policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
"github.com/flynn-archive/go-shlex" "github.com/flynn-archive/go-shlex"
) )
@ -65,7 +66,7 @@ func parseRuleLexemes(r *policyengine.Rule, lexemes []string) error {
return err return err
} }
r.Action, err = parseAction(lexemes[1]) r.Actions, err = parseAction(lexemes[1])
if err != nil { if err != nil {
return err return err
} }
@ -75,7 +76,7 @@ func parseRuleLexemes(r *policyengine.Rule, lexemes []string) error {
return err return err
} }
r.Resource, err = parseResource(lexemes[len(lexemes)-1]) r.Resources, err = parseResource(lexemes[len(lexemes)-1])
return err 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) { switch strings.ToLower(lexeme) {
case "object.put": case "object.put":
return []string{"native:PutObject"}, nil return policyengine.Actions{Names: []string{nativeschema.MethodPutObject}}, nil
case "object.get": case "object.get":
return []string{"native:GetObject"}, nil return policyengine.Actions{Names: []string{nativeschema.MethodGetObject}}, nil
case "object.head": case "object.head":
return []string{"native:HeadObject"}, nil return policyengine.Actions{Names: []string{nativeschema.MethodHeadObject}}, nil
case "object.delete": case "object.delete":
return []string{"native:DeleteObject"}, nil return policyengine.Actions{Names: []string{nativeschema.MethodDeleteObject}}, nil
case "object.search": case "object.search":
return []string{"native:SearchObject"}, nil return policyengine.Actions{Names: []string{nativeschema.MethodSearchObject}}, nil
case "object.range": case "object.range":
return []string{"native:RangeObject"}, nil return policyengine.Actions{Names: []string{nativeschema.MethodRangeObject}}, nil
case "object.hash": case "object.hash":
return []string{"native:HashObject"}, nil return policyengine.Actions{Names: []string{nativeschema.MethodHashObject}}, nil
default: default:
} }
return nil, fmt.Errorf("%w: %s", errUnknownOperation, lexeme) return policyengine.Actions{}, fmt.Errorf("%w: %s", errUnknownOperation, lexeme)
} }
func parseResource(lexeme string) ([]string, error) { func parseResource(lexeme string) (policyengine.Resources, error) {
return []string{fmt.Sprintf("native:::object/%s", lexeme)}, nil if lexeme == "*" {
return policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, nil
}
return policyengine.Resources{Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, lexeme)}}, nil
} }
const ( const (
ObjectResource = "object.resource" ObjectResource = "object.resource"
ObjectRequest = "object.request" ObjectRequest = "object.request"
ObjectActor = "object.actor"
) )
var typeToCondObject = map[string]policyengine.ObjectType{ var typeToCondObject = map[string]policyengine.ObjectType{
ObjectResource: policyengine.ObjectResource, ObjectResource: policyengine.ObjectResource,
ObjectRequest: policyengine.ObjectRequest, ObjectRequest: policyengine.ObjectRequest,
ObjectActor: policyengine.ObjectActor,
fyrchik marked this conversation as resolved Outdated

Also remove the constant ObjectActor?

Also remove the constant `ObjectActor`?

Done

Done
} }
func parseConditions(lexemes []string) ([]policyengine.Condition, error) { func parseConditions(lexemes []string) ([]policyengine.Condition, error) {

View file

@ -4,6 +4,7 @@ import (
"testing" "testing"
policyengine "git.frostfs.info/TrueCloudLab/policy-engine" policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -19,8 +20,8 @@ func TestParseAPERule(t *testing.T) {
rule: "allow Object.Put *", rule: "allow Object.Put *",
expectRule: policyengine.Rule{ expectRule: policyengine.Rule{
Status: policyengine.Allow, Status: policyengine.Allow,
fyrchik marked this conversation as resolved Outdated

Why not use constants here?

Why not use constants here?

Fixed

Fixed
Action: []string{"native:PutObject"}, Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
Resource: []string{"native:::object/*"}, Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}},
Condition: []policyengine.Condition{}, Condition: []policyengine.Condition{},
}, },
}, },
@ -29,8 +30,8 @@ func TestParseAPERule(t *testing.T) {
rule: "deny Object.Put *", rule: "deny Object.Put *",
expectRule: policyengine.Rule{ expectRule: policyengine.Rule{
Status: policyengine.AccessDenied, Status: policyengine.AccessDenied,
Action: []string{"native:PutObject"}, Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
Resource: []string{"native:::object/*"}, Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}},
Condition: []policyengine.Condition{}, Condition: []policyengine.Condition{},
}, },
}, },
@ -39,8 +40,8 @@ func TestParseAPERule(t *testing.T) {
rule: "deny:QuotaLimitReached Object.Put *", rule: "deny:QuotaLimitReached Object.Put *",
expectRule: policyengine.Rule{ expectRule: policyengine.Rule{
Status: policyengine.QuotaLimitReached, Status: policyengine.QuotaLimitReached,
Action: []string{"native:PutObject"}, Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
Resource: []string{"native:::object/*"}, Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}},
Condition: []policyengine.Condition{}, Condition: []policyengine.Condition{},
}, },
}, },
@ -48,9 +49,9 @@ func TestParseAPERule(t *testing.T) {
name: "Valid allow rule with conditions", name: "Valid allow rule with conditions",
rule: "allow Object.Get Object.Resource:Department=HR Object.Request:Actor!=ownerA *", rule: "allow Object.Get Object.Resource:Department=HR Object.Request:Actor!=ownerA *",
expectRule: policyengine.Rule{ expectRule: policyengine.Rule{
Status: policyengine.Allow, Status: policyengine.Allow,
Action: []string{"native:GetObject"}, Actions: policyengine.Actions{Names: []string{nativeschema.MethodGetObject}},
Resource: []string{"native:::object/*"}, Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}},
Condition: []policyengine.Condition{ Condition: []policyengine.Condition{
{ {
Op: policyengine.CondStringEquals, Op: policyengine.CondStringEquals,
@ -71,9 +72,9 @@ func TestParseAPERule(t *testing.T) {
name: "Valid rule with conditions with action detail", name: "Valid rule with conditions with action detail",
rule: "deny:QuotaLimitReached Object.Get Object.Resource:Department=HR Object.Request:Actor!=ownerA *", rule: "deny:QuotaLimitReached Object.Get Object.Resource:Department=HR Object.Request:Actor!=ownerA *",
expectRule: policyengine.Rule{ expectRule: policyengine.Rule{
Status: policyengine.QuotaLimitReached, Status: policyengine.QuotaLimitReached,
Action: []string{"native:GetObject"}, Actions: policyengine.Actions{Names: []string{nativeschema.MethodGetObject}},
Resource: []string{"native:::object/*"}, Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}},
Condition: []policyengine.Condition{ Condition: []policyengine.Condition{
{ {
Op: policyengine.CondStringEquals, 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-observability v0.0.0-20231101111734-b3ad3335ff65
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231101144515-6fbe1595cb3d git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231101144515-6fbe1595cb3d
git.frostfs.info/TrueCloudLab/hrw v1.2.1 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 git.frostfs.info/TrueCloudLab/tzhash v1.8.0
github.com/cheggaaa/pb v1.0.29 github.com/cheggaaa/pb v1.0.29
github.com/chzyer/readline v1.5.1 github.com/chzyer/readline v1.5.1

BIN
go.sum

Binary file not shown.

246
internal/ape/converter.go Normal file
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`

fixed

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