cli: Read ape rule from JSON file #989

Merged
fyrchik merged 4 commits from aarifullin/frostfs-node:feat/cli_parse_json into master 2024-02-20 07:42:30 +00:00
5 changed files with 133 additions and 52 deletions

View file

@ -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() {

View file

@ -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
}

View file

@ -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.

View file

@ -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)
}

View file

@ -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) {