forked from TrueCloudLab/frostfs-node
180 lines
4.6 KiB
Go
180 lines
4.6 KiB
Go
|
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
|
||
|
}
|