forked from TrueCloudLab/frostfs-node
[#800] node: eACL -> APE converter
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
This commit is contained in:
parent
364f835b7e
commit
fd9128d051
6 changed files with 747 additions and 28 deletions
|
@ -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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
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
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
BIN
go.sum
Binary file not shown.
246
internal/ape/converter.go
Normal file
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
|
||||||
|
pubKeyCondition.Value = hex.EncodeToString(binKey)
|
||||||
|
pubKeyCondition.Op = policyengine.CondStringEquals
|
||||||
|
rule.Condition = append(rule.Condition, pubKeyCondition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(source, rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendTargetsAndFilters(source []policyengine.Rule, st policyengine.Status, act policyengine.Actions, res policyengine.Resources,
|
||||||
|
targets []eacl.Target, filters []eacl.Filter,
|
||||||
|
) ([]policyengine.Rule, error) {
|
||||||
|
// see https://git.frostfs.info/TrueCloudLab/frostfs-sdk-go/src/commit/ab75edd70939564421936d207ef80d6c1398b51b/eacl/validator.go#L101
|
||||||
|
// role OR public key must be equal
|
||||||
|
// so filters are repeated for each role and public key
|
||||||
|
var err error
|
||||||
|
for _, target := range targets {
|
||||||
|
rule := policyengine.Rule{
|
||||||
|
Status: st,
|
||||||
|
Actions: act,
|
||||||
|
Resources: res,
|
||||||
|
}
|
||||||
|
var roleCondition policyengine.Condition
|
||||||
|
roleCondition.Object = policyengine.ObjectRequest
|
||||||
|
roleCondition.Key = nativeschema.PropertyKeyActorRole
|
||||||
|
roleCondition.Value = target.Role().String()
|
||||||
|
roleCondition.Op = policyengine.CondStringEquals
|
||||||
|
|
||||||
|
rule.Condition = append(rule.Condition, roleCondition)
|
||||||
|
rule.Condition, err = appendFilters(rule.Condition, filters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
source = append(source, rule)
|
||||||
|
|
||||||
|
for _, binKey := range target.BinaryKeys() {
|
||||||
|
rule := policyengine.Rule{
|
||||||
|
Status: st,
|
||||||
|
Actions: act,
|
||||||
|
Resources: res,
|
||||||
|
}
|
||||||
|
var pubKeyCondition policyengine.Condition
|
||||||
|
pubKeyCondition.Object = policyengine.ObjectRequest
|
||||||
|
pubKeyCondition.Key = nativeschema.PropertyKeyActorPublicKey
|
||||||
|
pubKeyCondition.Value = hex.EncodeToString(binKey)
|
||||||
|
pubKeyCondition.Op = policyengine.CondStringEquals
|
||||||
|
|
||||||
|
rule.Condition = append(rule.Condition, pubKeyCondition)
|
||||||
|
rule.Condition, err = appendFilters(rule.Condition, filters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
source = append(source, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return source, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendFilters(source []policyengine.Condition, filters []eacl.Filter) ([]policyengine.Condition, error) {
|
||||||
|
for _, filter := range filters {
|
||||||
|
var cond policyengine.Condition
|
||||||
|
var isObject bool
|
||||||
|
if filter.From() == eacl.HeaderFromObject {
|
||||||
|
cond.Object = policyengine.ObjectResource
|
||||||
|
isObject = true
|
||||||
|
} else if filter.From() == eacl.HeaderFromRequest {
|
||||||
|
cond.Object = policyengine.ObjectRequest
|
||||||
|
} else {
|
||||||
|
return nil, &ConvertEACLError{nested: fmt.Errorf("unknown filter from: %d", filter.From())}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Matcher() == eacl.MatchStringEqual {
|
||||||
|
cond.Op = policyengine.CondStringEquals
|
||||||
|
} else if filter.Matcher() == eacl.MatchStringNotEqual {
|
||||||
|
cond.Op = policyengine.CondStringNotEquals
|
||||||
|
} else {
|
||||||
|
return nil, &ConvertEACLError{nested: fmt.Errorf("unknown filter matcher: %d", filter.Matcher())}
|
||||||
|
}
|
||||||
|
|
||||||
|
cond.Key = eaclKeyToAPEKey(filter.Key(), isObject)
|
||||||
|
cond.Value = filter.Value()
|
||||||
|
|
||||||
|
source = append(source, cond)
|
||||||
|
}
|
||||||
|
return source, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func eaclKeyToAPEKey(key string, isObject bool) string {
|
||||||
|
if !isObject {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
default:
|
||||||
|
return key
|
||||||
|
case v2acl.FilterObjectVersion:
|
||||||
|
return nativeschema.PropertyKeyObjectVersion
|
||||||
|
case v2acl.FilterObjectID:
|
||||||
|
return nativeschema.PropertyKeyObjectID
|
||||||
|
case v2acl.FilterObjectContainerID:
|
||||||
|
return nativeschema.PropertyKeyObjectContainerID
|
||||||
|
case v2acl.FilterObjectOwnerID:
|
||||||
|
return nativeschema.PropertyKeyObjectOwnerID
|
||||||
|
case v2acl.FilterObjectCreationEpoch:
|
||||||
|
return nativeschema.PropertyKeyObjectCreationEpoch
|
||||||
|
case v2acl.FilterObjectPayloadLength:
|
||||||
|
return nativeschema.PropertyKeyObjectPayloadLength
|
||||||
|
case v2acl.FilterObjectPayloadHash:
|
||||||
|
return nativeschema.PropertyKeyObjectPayloadHash
|
||||||
|
case v2acl.FilterObjectType:
|
||||||
|
return nativeschema.PropertyKeyObjectType
|
||||||
|
case v2acl.FilterObjectHomomorphicHash:
|
||||||
|
return nativeschema.PropertyKeyObjectHomomorphicHash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResource(eaclTable *eacl.Table) policyengine.Resources {
|
||||||
|
cnrID, isSet := eaclTable.CID()
|
||||||
|
if isSet {
|
||||||
|
return policyengine.Resources{
|
||||||
|
Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString())},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return policyengine.Resources{
|
||||||
|
Names: []string{nativeschema.ResourceFormatRootObjects},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func actionToStatus(a eacl.Action) (policyengine.Status, error) {
|
||||||
|
switch a {
|
||||||
|
case eacl.ActionAllow:
|
||||||
|
return policyengine.Allow, nil
|
||||||
|
case eacl.ActionDeny:
|
||||||
|
return policyengine.AccessDenied, nil
|
||||||
|
default:
|
||||||
|
return policyengine.NoRuleFound, &ConvertEACLError{nested: fmt.Errorf("unknown action: %d", a)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var eaclOperationToEngineAction = map[eacl.Operation]policyengine.Actions{
|
||||||
|
eacl.OperationGet: {Names: []string{nativeschema.MethodGetObject}},
|
||||||
|
eacl.OperationHead: {Names: []string{nativeschema.MethodHeadObject}},
|
||||||
|
eacl.OperationPut: {Names: []string{nativeschema.MethodPutObject}},
|
||||||
|
eacl.OperationDelete: {Names: []string{nativeschema.MethodDeleteObject}},
|
||||||
|
eacl.OperationSearch: {Names: []string{nativeschema.MethodSearchObject}},
|
||||||
|
eacl.OperationRange: {Names: []string{nativeschema.MethodRangeObject}},
|
||||||
|
eacl.OperationRangeHash: {Names: []string{nativeschema.MethodHashObject}},
|
||||||
|
}
|
||||||
|
|
||||||
|
func operationToAction(op eacl.Operation) (policyengine.Actions, error) {
|
||||||
|
if v, ok := eaclOperationToEngineAction[op]; ok {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
return policyengine.Actions{}, &ConvertEACLError{nested: fmt.Errorf("unknown operation: %d", op)}
|
||||||
|
}
|
470
internal/ape/converter_test.go
Normal file
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 }
|
Loading…
Reference in a new issue