package util import ( "errors" "fmt" "os" "strconv" "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" "github.com/spf13/cobra" ) 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") ) // PrintHumanReadableAPEChain print APE chain rules. func PrintHumanReadableAPEChain(cmd *cobra.Command, chain *apechain.Chain) { cmd.Println("Chain ID: " + string(chain.ID)) cmd.Printf(" HEX: %x\n", chain.ID) cmd.Println("Rules:") for _, rule := range chain.Rules { cmd.Println("\n\tStatus: " + rule.Status.String()) cmd.Println("\tAny: " + strconv.FormatBool(rule.Any)) cmd.Println("\tConditions:") for _, c := range rule.Condition { var ot string switch c.Kind { case apechain.KindResource: ot = "Resource" case apechain.KindRequest: ot = "Request" default: panic("unknown object type") } cmd.Println(fmt.Sprintf("\t\t%s %s %s %s", ot, c.Key, c.Op, c.Value)) } cmd.Println("\tActions:\tInverted:" + strconv.FormatBool(rule.Actions.Inverted)) for _, name := range rule.Actions.Names { cmd.Println("\t\t" + name) } cmd.Println("\tResources:\tInverted:" + strconv.FormatBool(rule.Resources.Inverted)) for _, name := range rule.Resources.Names { cmd.Println("\t\t" + name) } } } 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_detail] ... [...] ... // // 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) }