forked from TrueCloudLab/frostfs-node
[#770] cli: Add methods to work with APE rules via control svc
* Add methods to frostfs-cli * Implement rpc in control service Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
This commit is contained in:
parent
8e11ef46b8
commit
66848d3288
11 changed files with 813 additions and 12 deletions
179
cmd/frostfs-cli/modules/util/ape.go
Normal file
179
cmd/frostfs-cli/modules/util/ape.go
Normal file
|
@ -0,0 +1,179 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
|
||||
"github.com/flynn-archive/go-shlex"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidStatementFormat = errors.New("invalid statement format")
|
||||
errInvalidConditionFormat = errors.New("invalid condition format")
|
||||
errUnknownAction = errors.New("action is not recognized")
|
||||
errUnknownOperation = errors.New("operation is not recognized")
|
||||
errUnknownActionDetail = errors.New("action detail is not recognized")
|
||||
errUnknownBinaryOperator = errors.New("binary operator is not recognized")
|
||||
errUnknownCondObjectType = errors.New("condition object type is not recognized")
|
||||
)
|
||||
|
||||
// ParseAPEChain parses APE chain rules.
|
||||
func ParseAPEChain(chain *policyengine.Chain, rules []string) error {
|
||||
if len(rules) == 0 {
|
||||
return errors.New("no APE rules provided")
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
r := new(policyengine.Rule)
|
||||
if err := ParseAPERule(r, rule); err != nil {
|
||||
return err
|
||||
}
|
||||
chain.Rules = append(chain.Rules, *r)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseAPERule parses access-policy-engine statement from the following form:
|
||||
// <action>[:action_detail] <operation> [<condition1> ...] <resource>
|
||||
//
|
||||
// Examples:
|
||||
// deny Object.Put *
|
||||
// deny:QuotaLimitReached Object.Put *
|
||||
// allow Object.Put *
|
||||
// allow Object.Get Object.Resource:Department=HR Object.Request:Actor=ownerA *
|
||||
//
|
||||
//nolint:godot
|
||||
func ParseAPERule(r *policyengine.Rule, rule string) error {
|
||||
lexemes, err := shlex.Split(rule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't parse rule '%s': %v", rule, err)
|
||||
}
|
||||
return parseRuleLexemes(r, lexemes)
|
||||
}
|
||||
|
||||
func parseRuleLexemes(r *policyengine.Rule, lexemes []string) error {
|
||||
if len(lexemes) < 2 {
|
||||
return errInvalidStatementFormat
|
||||
}
|
||||
|
||||
var err error
|
||||
r.Status, err = parseStatus(lexemes[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Action, err = parseAction(lexemes[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Condition, err = parseConditions(lexemes[2 : len(lexemes)-1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Resource, err = parseResource(lexemes[len(lexemes)-1])
|
||||
return err
|
||||
}
|
||||
|
||||
func parseStatus(lexeme string) (policyengine.Status, error) {
|
||||
action, expression, found := strings.Cut(lexeme, ":")
|
||||
switch action = strings.ToLower(action); action {
|
||||
case "deny":
|
||||
if !found {
|
||||
return policyengine.AccessDenied, nil
|
||||
} else if strings.EqualFold(expression, "QuotaLimitReached") {
|
||||
return policyengine.QuotaLimitReached, nil
|
||||
} else {
|
||||
return 0, fmt.Errorf("%w: %s", errUnknownActionDetail, expression)
|
||||
}
|
||||
case "allow":
|
||||
if found {
|
||||
return 0, errUnknownActionDetail
|
||||
}
|
||||
return policyengine.Allow, nil
|
||||
default:
|
||||
return 0, errUnknownAction
|
||||
}
|
||||
}
|
||||
|
||||
func parseAction(lexeme string) ([]string, error) {
|
||||
switch strings.ToLower(lexeme) {
|
||||
case "object.put":
|
||||
return []string{"native:PutObject"}, nil
|
||||
case "object.get":
|
||||
return []string{"native:GetObject"}, nil
|
||||
case "object.head":
|
||||
return []string{"native:HeadObject"}, nil
|
||||
case "object.delete":
|
||||
return []string{"native:DeleteObject"}, nil
|
||||
case "object.search":
|
||||
return []string{"native:SearchObject"}, nil
|
||||
case "object.range":
|
||||
return []string{"native:RangeObject"}, nil
|
||||
case "object.hash":
|
||||
return []string{"native:HashObject"}, nil
|
||||
default:
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", errUnknownOperation, lexeme)
|
||||
}
|
||||
|
||||
func parseResource(lexeme string) ([]string, error) {
|
||||
return []string{fmt.Sprintf("native:::object/%s", lexeme)}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
ObjectResource = "object.resource"
|
||||
ObjectRequest = "object.request"
|
||||
ObjectActor = "object.actor"
|
||||
)
|
||||
|
||||
var typeToCondObject = map[string]policyengine.ObjectType{
|
||||
ObjectResource: policyengine.ObjectResource,
|
||||
ObjectRequest: policyengine.ObjectRequest,
|
||||
ObjectActor: policyengine.ObjectActor,
|
||||
}
|
||||
|
||||
func parseConditions(lexemes []string) ([]policyengine.Condition, error) {
|
||||
conds := make([]policyengine.Condition, 0)
|
||||
|
||||
for _, lexeme := range lexemes {
|
||||
typ, expression, found := strings.Cut(lexeme, ":")
|
||||
typ = strings.ToLower(typ)
|
||||
|
||||
objType, ok := typeToCondObject[typ]
|
||||
if ok {
|
||||
if !found {
|
||||
return nil, fmt.Errorf("%w: %s", errInvalidConditionFormat, lexeme)
|
||||
}
|
||||
|
||||
var lhs, rhs string
|
||||
var binExpFound bool
|
||||
|
||||
var cond policyengine.Condition
|
||||
cond.Object = objType
|
||||
|
||||
lhs, rhs, binExpFound = strings.Cut(expression, "!=")
|
||||
if !binExpFound {
|
||||
lhs, rhs, binExpFound = strings.Cut(expression, "=")
|
||||
if !binExpFound {
|
||||
return nil, fmt.Errorf("%w: %s", errUnknownBinaryOperator, expression)
|
||||
}
|
||||
cond.Op = policyengine.CondStringEquals
|
||||
} else {
|
||||
cond.Op = policyengine.CondStringNotEquals
|
||||
}
|
||||
|
||||
cond.Key, cond.Value = lhs, rhs
|
||||
|
||||
conds = append(conds, cond)
|
||||
} else {
|
||||
return nil, fmt.Errorf("%w: %s", errUnknownCondObjectType, typ)
|
||||
}
|
||||
}
|
||||
|
||||
return conds, nil
|
||||
}
|
129
cmd/frostfs-cli/modules/util/ape_test.go
Normal file
129
cmd/frostfs-cli/modules/util/ape_test.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseAPERule(t *testing.T) {
|
||||
tests := [...]struct {
|
||||
name string
|
||||
rule string
|
||||
expectErr error
|
||||
expectRule policyengine.Rule
|
||||
}{
|
||||
{
|
||||
name: "Valid allow rule",
|
||||
rule: "allow Object.Put *",
|
||||
expectRule: policyengine.Rule{
|
||||
Status: policyengine.Allow,
|
||||
Action: []string{"native:PutObject"},
|
||||
Resource: []string{"native:::object/*"},
|
||||
Condition: []policyengine.Condition{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid deny rule",
|
||||
rule: "deny Object.Put *",
|
||||
expectRule: policyengine.Rule{
|
||||
Status: policyengine.AccessDenied,
|
||||
Action: []string{"native:PutObject"},
|
||||
Resource: []string{"native:::object/*"},
|
||||
Condition: []policyengine.Condition{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid deny rule with action detail",
|
||||
rule: "deny:QuotaLimitReached Object.Put *",
|
||||
expectRule: policyengine.Rule{
|
||||
Status: policyengine.QuotaLimitReached,
|
||||
Action: []string{"native:PutObject"},
|
||||
Resource: []string{"native:::object/*"},
|
||||
Condition: []policyengine.Condition{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid allow rule with conditions",
|
||||
rule: "allow Object.Get Object.Resource:Department=HR Object.Request:Actor!=ownerA *",
|
||||
expectRule: policyengine.Rule{
|
||||
Status: policyengine.Allow,
|
||||
Action: []string{"native:GetObject"},
|
||||
Resource: []string{"native:::object/*"},
|
||||
Condition: []policyengine.Condition{
|
||||
{
|
||||
Op: policyengine.CondStringEquals,
|
||||
Object: policyengine.ObjectResource,
|
||||
Key: "Department",
|
||||
Value: "HR",
|
||||
},
|
||||
{
|
||||
Op: policyengine.CondStringNotEquals,
|
||||
Object: policyengine.ObjectRequest,
|
||||
Key: "Actor",
|
||||
Value: "ownerA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid rule with conditions with action detail",
|
||||
rule: "deny:QuotaLimitReached Object.Get Object.Resource:Department=HR Object.Request:Actor!=ownerA *",
|
||||
expectRule: policyengine.Rule{
|
||||
Status: policyengine.QuotaLimitReached,
|
||||
Action: []string{"native:GetObject"},
|
||||
Resource: []string{"native:::object/*"},
|
||||
Condition: []policyengine.Condition{
|
||||
{
|
||||
Op: policyengine.CondStringEquals,
|
||||
Object: policyengine.ObjectResource,
|
||||
Key: "Department",
|
||||
Value: "HR",
|
||||
},
|
||||
{
|
||||
Op: policyengine.CondStringNotEquals,
|
||||
Object: policyengine.ObjectRequest,
|
||||
Key: "Actor",
|
||||
Value: "ownerA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid rule with unknown action",
|
||||
rule: "permit Object.Put *",
|
||||
expectErr: errUnknownAction,
|
||||
},
|
||||
{
|
||||
name: "Invalid rule with unknown operation",
|
||||
rule: "allow Object.PutOut *",
|
||||
expectErr: errUnknownOperation,
|
||||
},
|
||||
{
|
||||
name: "Invalid rule with unknown action detail",
|
||||
rule: "deny:UnknownActionDetail Object.Put *",
|
||||
expectErr: errUnknownActionDetail,
|
||||
},
|
||||
{
|
||||
name: "Invalid rule with unknown condition binary operator",
|
||||
rule: "deny Object.Put Object.Resource:Department<HR *",
|
||||
expectErr: errUnknownBinaryOperator,
|
||||
},
|
||||
{
|
||||
name: "Invalid rule with unknown condition object type",
|
||||
rule: "deny Object.Put Object.ResourZe:Department=HR *",
|
||||
expectErr: errUnknownCondObjectType,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
r := new(policyengine.Rule)
|
||||
err := ParseAPERule(r, test.rule)
|
||||
require.ErrorIs(t, err, test.expectErr)
|
||||
if test.expectErr == nil {
|
||||
require.Equal(t, test.expectRule, *r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue