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_detail] [ ...] // // 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 }