frostfs-node/cmd/frostfs-cli/modules/util/ape.go
Dmitrii Stepanov fd9128d051 [#800] node: eACL -> APE converter
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-11-15 11:55:55 +03:00

181 lines
5.1 KiB
Go

package util
import (
"errors"
"fmt"
"strings"
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
"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.Actions, err = parseAction(lexemes[1])
if err != nil {
return err
}
r.Condition, err = parseConditions(lexemes[2 : len(lexemes)-1])
if err != nil {
return err
}
r.Resources, 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) (policyengine.Actions, error) {
switch strings.ToLower(lexeme) {
case "object.put":
return policyengine.Actions{Names: []string{nativeschema.MethodPutObject}}, nil
case "object.get":
return policyengine.Actions{Names: []string{nativeschema.MethodGetObject}}, nil
case "object.head":
return policyengine.Actions{Names: []string{nativeschema.MethodHeadObject}}, nil
case "object.delete":
return policyengine.Actions{Names: []string{nativeschema.MethodDeleteObject}}, nil
case "object.search":
return policyengine.Actions{Names: []string{nativeschema.MethodSearchObject}}, nil
case "object.range":
return policyengine.Actions{Names: []string{nativeschema.MethodRangeObject}}, nil
case "object.hash":
return policyengine.Actions{Names: []string{nativeschema.MethodHashObject}}, nil
default:
}
return policyengine.Actions{}, fmt.Errorf("%w: %s", errUnknownOperation, lexeme)
}
func parseResource(lexeme string) (policyengine.Resources, error) {
if lexeme == "*" {
return policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, nil
}
return policyengine.Resources{Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, lexeme)}}, nil
}
const (
ObjectResource = "object.resource"
ObjectRequest = "object.request"
)
var typeToCondObject = map[string]policyengine.ObjectType{
ObjectResource: policyengine.ObjectResource,
ObjectRequest: policyengine.ObjectRequest,
}
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
}