frostfs-node/cmd/frostfs-cli/modules/util/ape.go
Airat Arifullin ae31ef3602 [#1501] cli: Move PrintHumanReadableAPEChain to a common package
* Both `frostfs-cli` and `frostfs-adm` APE-related subcommands use
  `PrintHumanReadableAPEChain` to print a parsed APE-chain. So, it's
  more correct to have it in a common package over `frostfs-cli` and
  `frostfs-adm` folders.

Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2024-11-20 07:58:32 +00:00

321 lines
9.3 KiB
Go

package util
import (
"errors"
"fmt"
"os"
"strings"
apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
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")
errUnknownStatus = errors.New("status is not recognized")
errUnknownStatusDetail = errors.New("status detail is not recognized")
errUnknownAction = errors.New("action is not recognized")
errUnknownBinaryOperator = errors.New("binary operator is not recognized")
errUnknownCondObjectType = errors.New("condition object type is not recognized")
errMixedTypesInRule = errors.New("found mixed type of actions in rule")
errNoActionsInRule = errors.New("there are no actions in rule")
errUnsupportedResourceFormat = errors.New("unsupported resource format")
errFailedToParseAllAny = errors.New("any/all is not parsed")
)
func ParseAPEChainBinaryOrJSON(chain *apechain.Chain, path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read file <%s>: %w", path, err)
}
err = chain.UnmarshalBinary(data)
if err != nil {
err = chain.UnmarshalJSON(data)
if err != nil {
return fmt.Errorf("invalid format: %w", err)
}
}
return nil
}
// ParseAPEChain parses APE chain rules.
func ParseAPEChain(chain *apechain.Chain, rules []string) error {
if len(rules) == 0 {
return errors.New("no APE rules provided")
}
for _, rule := range rules {
r := new(apechain.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:
// <status>[:status_detail] <action>... [<condition>...] <resource>...
//
// Examples:
// deny Object.Put *
// deny:QuotaLimitReached Object.Put *
// allow Object.Put *
// allow Object.Get ResourceCondition:Department=HR RequestCondition:Actor=ownerA *
// allow Object.Get any ResourceCondition:Department=HR RequestCondition:Actor=ownerA *
// allow Object.Get all ResourceCondition:Department=HR RequestCondition:Actor=ownerA *
// allow Object.* *
// allow Container.* *
//
//nolint:godot
func ParseAPERule(r *apechain.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 unique(inputSlice []string) []string {
uniqueSlice := make([]string, 0, len(inputSlice))
seen := make(map[string]bool, len(inputSlice))
for _, element := range inputSlice {
if !seen[element] {
uniqueSlice = append(uniqueSlice, element)
seen[element] = true
}
}
return uniqueSlice
}
func parseRuleLexemes(r *apechain.Rule, lexemes []string) error {
if len(lexemes) < 2 {
return errInvalidStatementFormat
}
var err error
r.Status, err = parseStatus(lexemes[0])
if err != nil {
return err
}
var objectTargeted bool
var containerTargeted bool
for i, lexeme := range lexemes[1:] {
anyExpr, anyErr := parseAnyAll(lexeme)
if anyErr == nil {
r.Any = anyExpr
continue
}
var names []string
var actionType bool
names, actionType, err = parseAction(lexeme)
if err != nil {
condition, errCond := parseCondition(lexeme)
if errCond != nil {
err = fmt.Errorf("%w:%w", err, errCond)
lexemes = lexemes[i+1:]
break
}
r.Condition = append(r.Condition, *condition)
} else {
if actionType {
objectTargeted = true
} else {
containerTargeted = true
}
if objectTargeted && containerTargeted {
// Actually, APE chain allows to define rules for several resources, for example, if
// chain target is namespace, but the parser primitevly compiles verbs,
// conditions and resources in one rule. So, for the parser, one rule relates only to
// one resource type - object or container.
return errMixedTypesInRule
}
r.Actions.Names = append(r.Actions.Names, names...)
}
}
r.Actions.Names = unique(r.Actions.Names)
if len(r.Actions.Names) == 0 {
return fmt.Errorf("%w:%w", err, errNoActionsInRule)
}
for _, lexeme := range lexemes {
resource, errRes := parseResource(lexeme, objectTargeted)
if errRes != nil {
return fmt.Errorf("%w:%w", err, errRes)
}
r.Resources.Names = append(r.Resources.Names, resource)
}
return nil
}
func parseAnyAll(lexeme string) (bool, error) {
switch strings.ToLower(lexeme) {
case "any":
return true, nil
case "all":
return false, nil
default:
return false, errFailedToParseAllAny
}
}
func parseStatus(lexeme string) (apechain.Status, error) {
action, expression, found := strings.Cut(lexeme, ":")
switch strings.ToLower(action) {
case "deny":
if !found {
return apechain.AccessDenied, nil
} else if strings.EqualFold(expression, "QuotaLimitReached") {
return apechain.QuotaLimitReached, nil
} else {
return 0, fmt.Errorf("%w: %s", errUnknownStatusDetail, expression)
}
case "allow":
if found {
return 0, errUnknownStatusDetail
}
return apechain.Allow, nil
default:
return 0, errUnknownStatus
}
}
func parseAction(lexeme string) ([]string, bool, error) {
switch strings.ToLower(lexeme) {
case "object.put":
return []string{nativeschema.MethodPutObject}, true, nil
case "object.get":
return []string{nativeschema.MethodGetObject}, true, nil
case "object.head":
return []string{nativeschema.MethodHeadObject}, true, nil
case "object.delete":
return []string{nativeschema.MethodDeleteObject}, true, nil
case "object.search":
return []string{nativeschema.MethodSearchObject}, true, nil
case "object.range":
return []string{nativeschema.MethodRangeObject}, true, nil
case "object.hash":
return []string{nativeschema.MethodHashObject}, true, nil
case "object.patch":
return []string{nativeschema.MethodPatchObject}, true, nil
case "object.*":
return []string{
nativeschema.MethodPutObject,
nativeschema.MethodGetObject,
nativeschema.MethodHeadObject,
nativeschema.MethodDeleteObject,
nativeschema.MethodSearchObject,
nativeschema.MethodRangeObject,
nativeschema.MethodHashObject,
nativeschema.MethodPatchObject,
}, true, nil
case "container.put":
return []string{nativeschema.MethodPutContainer}, false, nil
case "container.delete":
return []string{nativeschema.MethodDeleteContainer}, false, nil
case "container.get":
return []string{nativeschema.MethodGetContainer}, false, nil
case "container.list":
return []string{nativeschema.MethodListContainers}, false, nil
case "container.*":
return []string{
nativeschema.MethodPutContainer,
nativeschema.MethodDeleteContainer,
nativeschema.MethodGetContainer,
nativeschema.MethodListContainers,
}, false, nil
default:
}
return nil, false, fmt.Errorf("%w: %s", errUnknownAction, lexeme)
}
func parseResource(lexeme string, isObj bool) (string, error) {
if len(lexeme) > 0 && !strings.HasSuffix(lexeme, "/") {
if isObj {
if lexeme == "*" {
return nativeschema.ResourceFormatAllObjects, nil
} else if lexeme == "/*" || lexeme == "root/*" {
return nativeschema.ResourceFormatRootObjects, nil
} else if strings.HasPrefix(lexeme, "/") {
lexeme = lexeme[1:]
delimCount := strings.Count(lexeme, "/")
if delimCount == 1 && len(lexeme) >= 3 { // container/object
return nativeschema.ObjectPrefix + "//" + lexeme, nil
}
} else {
delimCount := strings.Count(lexeme, "/")
if delimCount == 1 && len(lexeme) >= 3 ||
delimCount == 2 && len(lexeme) >= 5 { // namespace/container/object
return nativeschema.ObjectPrefix + "/" + lexeme, nil
}
}
} else {
if lexeme == "*" {
return nativeschema.ResourceFormatAllContainers, nil
} else if lexeme == "/*" {
return nativeschema.ResourceFormatRootContainers, nil
} else if strings.HasPrefix(lexeme, "/") && len(lexeme) > 1 {
lexeme = lexeme[1:]
delimCount := strings.Count(lexeme, "/")
if delimCount == 0 {
return nativeschema.ContainerPrefix + "//" + lexeme, nil
}
} else {
delimCount := strings.Count(lexeme, "/")
if delimCount == 1 && len(lexeme) > 3 { // namespace/container
return nativeschema.ContainerPrefix + "/" + lexeme, nil
}
}
}
}
return "", errUnsupportedResourceFormat
}
const (
ResourceCondition = "resourcecondition"
RequestCondition = "requestcondition"
)
var typeToCondKindType = map[string]apechain.ConditionKindType{
ResourceCondition: apechain.KindResource,
RequestCondition: apechain.KindRequest,
}
func parseCondition(lexeme string) (*apechain.Condition, error) {
typ, expression, found := strings.Cut(lexeme, ":")
typ = strings.ToLower(typ)
condKindType, ok := typeToCondKindType[typ]
if ok {
if !found {
return nil, fmt.Errorf("%w: %s", errInvalidConditionFormat, lexeme)
}
var cond apechain.Condition
cond.Kind = condKindType
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 = apechain.CondStringEquals
} else {
cond.Op = apechain.CondStringNotEquals
}
cond.Key, cond.Value = lhs, rhs
return &cond, nil
}
return nil, fmt.Errorf("%w: %s", errUnknownCondObjectType, typ)
}