Aleksey Savchuk
a4fb7f085b
All checks were successful
Tests and linters / Run gofumpt (pull_request) Successful in 2m36s
Pre-commit hooks / Pre-commit (pull_request) Successful in 3m3s
Vulncheck / Vulncheck (pull_request) Successful in 2m50s
Tests and linters / Tests (1.22) (pull_request) Successful in 3m3s
DCO action / DCO (pull_request) Successful in 2m47s
Tests and linters / Lint (pull_request) Successful in 3m39s
Tests and linters / Staticcheck (pull_request) Successful in 3m37s
Tests and linters / Tests (1.23) (pull_request) Successful in 3m43s
Build / Build Components (1.22) (pull_request) Successful in 3m32s
Build / Build Components (1.23) (pull_request) Successful in 3m32s
Tests and linters / gopls check (pull_request) Successful in 4m10s
Tests and linters / Tests with -race (pull_request) Successful in 4m16s
Signed-off-by: Aleksey Savchuk <a.savchuk@yadro.com>
355 lines
10 KiB
Go
355 lines
10 KiB
Go
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>[: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)
|
|
}
|