eACL -> APE converter #800
|
@ -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
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseConditions(lexemes []string) ([]policyengine.Condition, error) {
|
func parseConditions(lexemes []string) ([]policyengine.Condition, error) {
|
||||||
|
|
|
@ -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
fyrchik
commented
Why not use constants here? Why not use constants here?
dstepanov-yadro
commented
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
|
@ -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
246
internal/ape/converter.go
Normal 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
fyrchik
commented
Will we store binary non-UTF-8 data in Will we store binary non-UTF-8 data in `Value` field here?
dstepanov-yadro
commented
Replaced with hex Replaced with hex
fyrchik
commented
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). 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
aarifullin
commented
The converter ignores
is converted to valid:
Either we need to reject appending the 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`
dstepanov-yadro
commented
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)}
|
||||||
|
}
|
470
internal/ape/converter_test.go
Normal 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 }
|
Also remove the constant
ObjectActor
?Done