diff --git a/cmd/frostfs-adm/internal/modules/morph/ape/ape.go b/cmd/frostfs-adm/internal/modules/morph/ape/ape.go index c08dc512e..957c7a15f 100644 --- a/cmd/frostfs-adm/internal/modules/morph/ape/ape.go +++ b/cmd/frostfs-adm/internal/modules/morph/ape/ape.go @@ -22,8 +22,8 @@ const ( chainIDDesc = "Rule chain ID" ruleFlag = "rule" ruleFlagDesc = "Rule chain in text format" - ruleJSONFlag = "rule-json" - ruleJSONFlagDesc = "Chain rule in JSON format or path to the file" + pathFlag = "path" + pathFlagDesc = "path to encoded chain in JSON or binary format" targetNameFlag = "target-name" targetNameDesc = "Resource name in APE resource name format" targetTypeFlag = "target-type" @@ -99,10 +99,10 @@ func initAddRuleChainCmd() { addRuleChainCmd.Flags().String(chainIDFlag, "", chainIDDesc) _ = addRuleChainCmd.MarkFlagRequired(chainIDFlag) - addRuleChainCmd.Flags().String(ruleFlag, "", ruleFlagDesc) - addRuleChainCmd.Flags().String(ruleJSONFlag, "", ruleJSONFlagDesc) + addRuleChainCmd.Flags().StringArray(ruleFlag, []string{}, ruleFlagDesc) + addRuleChainCmd.Flags().String(pathFlag, "", pathFlagDesc) addRuleChainCmd.Flags().String(chainNameFlag, ingress, chainNameFlagDesc) - addRuleChainCmd.MarkFlagsMutuallyExclusive(ruleFlag, ruleJSONFlag) + addRuleChainCmd.MarkFlagsMutuallyExclusive(ruleFlag, pathFlag) } func initRemoveRuleChainCmd() { diff --git a/cmd/frostfs-adm/internal/modules/morph/ape/ape_util.go b/cmd/frostfs-adm/internal/modules/morph/ape/ape_util.go index 7f23e3754..fe09776ed 100644 --- a/cmd/frostfs-adm/internal/modules/morph/ape/ape_util.go +++ b/cmd/frostfs-adm/internal/modules/morph/ape/ape_util.go @@ -1,9 +1,7 @@ package ape import ( - "encoding/json" "fmt" - "os" "strings" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags" @@ -63,29 +61,19 @@ func parseChainID(cmd *cobra.Command) apechain.ID { func parseChain(cmd *cobra.Command) *apechain.Chain { chain := new(apechain.Chain) - if ruleStmt, _ := cmd.Flags().GetString(ruleFlag); ruleStmt != "" { - parseErr := parseutil.ParseAPEChain(chain, []string{ruleStmt}) - commonCmd.ExitOnErr(cmd, "ape chain parser error: %w", parseErr) - } else if ruleJSON, _ := cmd.Flags().GetString(ruleJSONFlag); ruleJSON != "" { - var rule []byte - if _, err := os.Stat(ruleJSON); err == nil { - rule, err = os.ReadFile(ruleJSON) - commonCmd.ExitOnErr(cmd, "read file error: %w", err) - } else { - rule = []byte(ruleJSON) - if !json.Valid(rule) { - commonCmd.ExitOnErr(cmd, "read raw rule error: %w", - fmt.Errorf("invalid JSON")) - } - } - err := chain.DecodeBytes(rule) - commonCmd.ExitOnErr(cmd, "chain decode error: %w", err) + if rules, _ := cmd.Flags().GetStringArray(ruleFlag); len(rules) > 0 { + commonCmd.ExitOnErr(cmd, "parser error: %w", parseutil.ParseAPEChain(chain, rules)) + } else if encPath, _ := cmd.Flags().GetString(pathFlag); encPath != "" { + commonCmd.ExitOnErr(cmd, "decode binary or json error: %w", parseutil.ParseAPEChainBinaryOrJSON(chain, encPath)) } else { - commonCmd.ExitOnErr(cmd, "", fmt.Errorf("rule is not passed")) + commonCmd.ExitOnErr(cmd, "parser error: %w", fmt.Errorf("rule is not passed")) } chain.ID = parseChainID(cmd) + cmd.Println("Parsed chain:") + parseutil.PrintHumanReadableAPEChain(cmd, chain) + return chain } diff --git a/cmd/frostfs-cli/docs/policy.md b/cmd/frostfs-cli/docs/policy.md index 06c325c62..1d51818ac 100644 --- a/cmd/frostfs-cli/docs/policy.md +++ b/cmd/frostfs-cli/docs/policy.md @@ -27,9 +27,13 @@ For container it can be represented as: - `/*` all containers in the `root` namespace Actions is a regular operations upon FrostFS containers/objects. Like `Object.Put`, `Container.Get` etc. +You can use `Object.*`, `Container.*` that implies all actions. In status section it is possible to use `allow`, `deny` or `deny:QuotaLimitReached` actions. +If a statement does not contain lexeme `any`, field `Any` is set to `false` by default. Otherwise, it is set +to `true`. Optionally, `all` can be used - it also sets `Any=false`. + It is prohibited to mix operation under FrostFS container and object in one rule. The same statement is equal for conditions and resources - one rule is for one type of items. diff --git a/cmd/frostfs-cli/modules/control/add_rule.go b/cmd/frostfs-cli/modules/control/add_rule.go index 0f5ac11f3..45b36bfb1 100644 --- a/cmd/frostfs-cli/modules/control/add_rule.go +++ b/cmd/frostfs-cli/modules/control/add_rule.go @@ -2,6 +2,7 @@ package control import ( "encoding/hex" + "errors" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key" @@ -14,6 +15,7 @@ import ( const ( ruleFlag = "rule" + pathFlag = "path" ) var addRuleCmd = &cobra.Command{ @@ -23,15 +25,13 @@ var addRuleCmd = &cobra.Command{ Example: `control add-rule --endpoint ... -w ... --address ... --chain-id ChainID --cid ... --rule "allow Object.Get *" --rule "deny Object.Get EbxzAdz5LB4uqxuz6crWKAumBNtZyK2rKsqQP7TdZvwr/*" --rule "deny:QuotaLimitReached Object.Put Object.Resource:Department=HR *" + +control add-rule --endpoint ... -w ... --address ... --chain-id ChainID --cid ... --path some_chain.json `, Run: addRule, } -func addRule(cmd *cobra.Command, _ []string) { - pk := key.Get(cmd) - - target := parseTarget(cmd) - +func parseChain(cmd *cobra.Command) *apechain.Chain { chainID, _ := cmd.Flags().GetString(chainIDFlag) hexEncoded, _ := cmd.Flags().GetBool(chainIDHexFlag) @@ -43,20 +43,34 @@ func addRule(cmd *cobra.Command, _ []string) { commonCmd.ExitOnErr(cmd, "can't decode chain ID as hex: %w", err) } - rule, _ := cmd.Flags().GetStringArray(ruleFlag) - chain := new(apechain.Chain) - commonCmd.ExitOnErr(cmd, "parser error: %w", util.ParseAPEChain(chain, rule)) chain.ID = apechain.ID(chainIDRaw) - serializedChain := chain.Bytes() + + if rules, _ := cmd.Flags().GetStringArray(ruleFlag); len(rules) > 0 { + commonCmd.ExitOnErr(cmd, "parser error: %w", util.ParseAPEChain(chain, rules)) + } else if encPath, _ := cmd.Flags().GetString(pathFlag); encPath != "" { + commonCmd.ExitOnErr(cmd, "decode binary or json error: %w", util.ParseAPEChainBinaryOrJSON(chain, encPath)) + } else { + commonCmd.ExitOnErr(cmd, "parser error", errors.New("rule is not passed")) + } cmd.Println("Parsed chain:") util.PrintHumanReadableAPEChain(cmd, chain) + return chain +} + +func addRule(cmd *cobra.Command, _ []string) { + pk := key.Get(cmd) + + target := parseTarget(cmd) + + parsed := parseChain(cmd) + req := &control.AddChainLocalOverrideRequest{ Body: &control.AddChainLocalOverrideRequest_Body{ Target: target, - Chain: serializedChain, + Chain: parsed.Bytes(), }, } @@ -81,9 +95,12 @@ func initControlAddRuleCmd() { ff := addRuleCmd.Flags() ff.StringArray(ruleFlag, []string{}, "Rule statement") + ff.String(pathFlag, "", "Path to encoded chain in JSON or binary format") ff.String(chainIDFlag, "", "Assign ID to the parsed chain") ff.String(targetNameFlag, "", targetNameDesc) ff.String(targetTypeFlag, "", targetTypeDesc) _ = addRuleCmd.MarkFlagRequired(targetTypeFlag) ff.Bool(chainIDHexFlag, false, "Flag to parse chain ID as hex") + + addRuleCmd.MarkFlagsMutuallyExclusive(pathFlag, ruleFlag) } diff --git a/cmd/frostfs-cli/modules/util/ape.go b/cmd/frostfs-cli/modules/util/ape.go index 6a7eca948..1500f0c5c 100644 --- a/cmd/frostfs-cli/modules/util/ape.go +++ b/cmd/frostfs-cli/modules/util/ape.go @@ -3,6 +3,7 @@ package util import ( "errors" "fmt" + "os" "strconv" "strings" @@ -57,6 +58,23 @@ func PrintHumanReadableAPEChain(cmd *cobra.Command, chain *apechain.Chain) { } } +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 { @@ -82,6 +100,10 @@ func ParseAPEChain(chain *apechain.Chain, rules []string) error { // deny:QuotaLimitReached Object.Put * // allow Object.Put * // allow Object.Get Object.Resource:Department=HR Object.Request:Actor=ownerA * +// allow Object.Get any Object.Resource:Department=HR Object.Request:Actor=ownerA * +// allow Object.Get all Object.Resource:Department=HR Object.Request:Actor=ownerA * +// allow Object.* * +// allow Container.* * // //nolint:godot func ParseAPERule(r *apechain.Rule, rule string) error { @@ -92,6 +114,18 @@ func ParseAPERule(r *apechain.Rule, rule string) error { 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 @@ -105,9 +139,15 @@ func parseRuleLexemes(r *apechain.Rule, lexemes []string) error { var isObject *bool for i, lexeme := range lexemes[1:] { - var name string + anyExpr, anyErr := parseAnyAll(lexeme) + if anyErr == nil { + r.Any = anyExpr + continue + } + + var names []string var actionType bool - name, actionType, err = parseAction(lexeme) + names, actionType, err = parseAction(lexeme) if err != nil { condition, errCond := parseCondition(lexeme) if errCond != nil { @@ -118,7 +158,7 @@ func parseRuleLexemes(r *apechain.Rule, lexemes []string) error { actionType = condition.Object == apechain.ObjectResource || condition.Object == apechain.ObjectRequest r.Condition = append(r.Condition, *condition) } else { - r.Actions.Names = append(r.Actions.Names, name) + r.Actions.Names = append(r.Actions.Names, names...) } if isObject == nil { isObject = &actionType @@ -126,6 +166,7 @@ func parseRuleLexemes(r *apechain.Rule, lexemes []string) error { return errMixedTypesInRule } } + r.Actions.Names = unique(r.Actions.Names) if len(r.Actions.Names) == 0 { return fmt.Errorf("%w:%w", err, errNoActionsInRule) } @@ -140,6 +181,17 @@ func parseRuleLexemes(r *apechain.Rule, lexemes []string) error { 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, fmt.Errorf("any/all is not parsed") + } +} + func parseStatus(lexeme string) (apechain.Status, error) { action, expression, found := strings.Cut(lexeme, ":") switch strings.ToLower(action) { @@ -161,35 +213,55 @@ func parseStatus(lexeme string) (apechain.Status, error) { } } -func parseAction(lexeme string) (string, bool, error) { +func parseAction(lexeme string) ([]string, bool, error) { switch strings.ToLower(lexeme) { case "object.put": - return nativeschema.MethodPutObject, true, nil + return []string{nativeschema.MethodPutObject}, true, nil case "object.get": - return nativeschema.MethodGetObject, true, nil + return []string{nativeschema.MethodGetObject}, true, nil case "object.head": - return nativeschema.MethodHeadObject, true, nil + return []string{nativeschema.MethodHeadObject}, true, nil case "object.delete": - return nativeschema.MethodDeleteObject, true, nil + return []string{nativeschema.MethodDeleteObject}, true, nil case "object.search": - return nativeschema.MethodSearchObject, true, nil + return []string{nativeschema.MethodSearchObject}, true, nil case "object.range": - return nativeschema.MethodRangeObject, true, nil + return []string{nativeschema.MethodRangeObject}, true, nil case "object.hash": - return nativeschema.MethodHashObject, true, nil + return []string{nativeschema.MethodHashObject}, true, nil + case "object.*": + return []string{ + nativeschema.MethodPutObject, + nativeschema.MethodGetObject, + nativeschema.MethodHeadObject, + nativeschema.MethodDeleteObject, + nativeschema.MethodSearchObject, + nativeschema.MethodHashObject, + }, true, nil case "container.put": - return nativeschema.MethodPutContainer, false, nil + return []string{nativeschema.MethodPutContainer}, false, nil case "container.delete": - return nativeschema.MethodDeleteContainer, false, nil + return []string{nativeschema.MethodDeleteContainer}, false, nil case "container.get": - return nativeschema.MethodGetContainer, false, nil + return []string{nativeschema.MethodGetContainer}, false, nil case "container.setcontainereacl": - return nativeschema.MethodSetContainerEACL, false, nil + return []string{nativeschema.MethodSetContainerEACL}, false, nil case "container.getcontainereacl": - return nativeschema.MethodGetContainerEACL, false, nil + return []string{nativeschema.MethodGetContainerEACL}, false, nil + case "container.list": + return []string{nativeschema.MethodListContainers}, false, nil + case "container.*": + return []string{ + nativeschema.MethodPutContainer, + nativeschema.MethodDeleteContainer, + nativeschema.MethodGetContainer, + nativeschema.MethodSetContainerEACL, + nativeschema.MethodGetContainerEACL, + nativeschema.MethodListContainers, + }, false, nil default: } - return "", false, fmt.Errorf("%w: %s", errUnknownAction, lexeme) + return nil, false, fmt.Errorf("%w: %s", errUnknownAction, lexeme) } func parseResource(lexeme string, isObj bool) (string, error) {