Compare commits

...
Sign in to create a new pull request.

4 commits

Author SHA1 Message Date
d3a6e289be [#986] cli: Allow add-rule command to parse new actions
All checks were successful
Vulncheck / Vulncheck (pull_request) Successful in 1m52s
DCO action / DCO (pull_request) Successful in 1m49s
Build / Build Components (1.21) (pull_request) Successful in 4m16s
Build / Build Components (1.22) (pull_request) Successful in 4m13s
Tests and linters / Lint (pull_request) Successful in 4m21s
Tests and linters / Staticcheck (pull_request) Successful in 5m15s
Tests and linters / Tests (1.21) (pull_request) Successful in 7m28s
Tests and linters / Tests with -race (pull_request) Successful in 8m14s
Tests and linters / Tests (1.22) (pull_request) Successful in 8m38s
* Introduce Object.* and Container.* actions that
  span all methods for services.

Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2024-02-19 17:47:17 +03:00
4ed781be17 [#989] util: Introduce any and all statements for ape rule parsing
Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2024-02-19 17:09:35 +03:00
efc978b9e2 [#989] adm: Read and parse chains from file
* Slightly fix the approach to read encoded chain from file
  in frostfs-adm.

Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2024-02-19 17:09:35 +03:00
b2491881bd [#989] cli: Read and parse chains from file
* Introduce path flag to make add-rule command read and parse
  chain from file. File is binary/JSON-encoded chain.

Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2024-02-19 17:09:35 +03:00
5 changed files with 133 additions and 52 deletions

View file

@ -22,8 +22,8 @@ const (
chainIDDesc = "Rule chain ID" chainIDDesc = "Rule chain ID"
ruleFlag = "rule" ruleFlag = "rule"
ruleFlagDesc = "Rule chain in text format" ruleFlagDesc = "Rule chain in text format"
ruleJSONFlag = "rule-json" pathFlag = "path"
ruleJSONFlagDesc = "Chain rule in JSON format or path to the file" pathFlagDesc = "path to encoded chain in JSON or binary format"
targetNameFlag = "target-name" targetNameFlag = "target-name"
targetNameDesc = "Resource name in APE resource name format" targetNameDesc = "Resource name in APE resource name format"
targetTypeFlag = "target-type" targetTypeFlag = "target-type"
@ -99,10 +99,10 @@ func initAddRuleChainCmd() {
addRuleChainCmd.Flags().String(chainIDFlag, "", chainIDDesc) addRuleChainCmd.Flags().String(chainIDFlag, "", chainIDDesc)
_ = addRuleChainCmd.MarkFlagRequired(chainIDFlag) _ = addRuleChainCmd.MarkFlagRequired(chainIDFlag)
addRuleChainCmd.Flags().String(ruleFlag, "", ruleFlagDesc) addRuleChainCmd.Flags().StringArray(ruleFlag, []string{}, ruleFlagDesc)
addRuleChainCmd.Flags().String(ruleJSONFlag, "", ruleJSONFlagDesc) addRuleChainCmd.Flags().String(pathFlag, "", pathFlagDesc)
addRuleChainCmd.Flags().String(chainNameFlag, ingress, chainNameFlagDesc) addRuleChainCmd.Flags().String(chainNameFlag, ingress, chainNameFlagDesc)
addRuleChainCmd.MarkFlagsMutuallyExclusive(ruleFlag, ruleJSONFlag) addRuleChainCmd.MarkFlagsMutuallyExclusive(ruleFlag, pathFlag)
} }
func initRemoveRuleChainCmd() { func initRemoveRuleChainCmd() {

View file

@ -1,9 +1,7 @@
package ape package ape
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"strings" "strings"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags" "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 { func parseChain(cmd *cobra.Command) *apechain.Chain {
chain := new(apechain.Chain) chain := new(apechain.Chain)
if ruleStmt, _ := cmd.Flags().GetString(ruleFlag); ruleStmt != "" { if rules, _ := cmd.Flags().GetStringArray(ruleFlag); len(rules) > 0 {
parseErr := parseutil.ParseAPEChain(chain, []string{ruleStmt}) commonCmd.ExitOnErr(cmd, "parser error: %w", parseutil.ParseAPEChain(chain, rules))
commonCmd.ExitOnErr(cmd, "ape chain parser error: %w", parseErr) } else if encPath, _ := cmd.Flags().GetString(pathFlag); encPath != "" {
} else if ruleJSON, _ := cmd.Flags().GetString(ruleJSONFlag); ruleJSON != "" { commonCmd.ExitOnErr(cmd, "decode binary or json error: %w", parseutil.ParseAPEChainBinaryOrJSON(chain, encPath))
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)
} else { } 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) chain.ID = parseChainID(cmd)
cmd.Println("Parsed chain:")
parseutil.PrintHumanReadableAPEChain(cmd, chain)
return chain return chain
} }

View file

@ -27,9 +27,13 @@ For container it can be represented as:
- `/*` all containers in the `root` namespace - `/*` all containers in the `root` namespace
Actions is a regular operations upon FrostFS containers/objects. Like `Object.Put`, `Container.Get` etc. 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. 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. 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. 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 ( import (
"encoding/hex" "encoding/hex"
"errors"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
@ -14,6 +15,7 @@ import (
const ( const (
ruleFlag = "rule" ruleFlag = "rule"
pathFlag = "path"
) )
var addRuleCmd = &cobra.Command{ 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 *" Example: `control add-rule --endpoint ... -w ... --address ... --chain-id ChainID --cid ... --rule "allow Object.Get *"
--rule "deny Object.Get EbxzAdz5LB4uqxuz6crWKAumBNtZyK2rKsqQP7TdZvwr/*" --rule "deny Object.Get EbxzAdz5LB4uqxuz6crWKAumBNtZyK2rKsqQP7TdZvwr/*"
--rule "deny:QuotaLimitReached Object.Put Object.Resource:Department=HR *" --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, Run: addRule,
} }
func addRule(cmd *cobra.Command, _ []string) { func parseChain(cmd *cobra.Command) *apechain.Chain {
pk := key.Get(cmd)
target := parseTarget(cmd)
chainID, _ := cmd.Flags().GetString(chainIDFlag) chainID, _ := cmd.Flags().GetString(chainIDFlag)
hexEncoded, _ := cmd.Flags().GetBool(chainIDHexFlag) 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) commonCmd.ExitOnErr(cmd, "can't decode chain ID as hex: %w", err)
} }
rule, _ := cmd.Flags().GetStringArray(ruleFlag)
chain := new(apechain.Chain) chain := new(apechain.Chain)
commonCmd.ExitOnErr(cmd, "parser error: %w", util.ParseAPEChain(chain, rule))
chain.ID = apechain.ID(chainIDRaw) 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:") cmd.Println("Parsed chain:")
util.PrintHumanReadableAPEChain(cmd, 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{ req := &control.AddChainLocalOverrideRequest{
Body: &control.AddChainLocalOverrideRequest_Body{ Body: &control.AddChainLocalOverrideRequest_Body{
Target: target, Target: target,
Chain: serializedChain, Chain: parsed.Bytes(),
}, },
} }
@ -81,9 +95,12 @@ func initControlAddRuleCmd() {
ff := addRuleCmd.Flags() ff := addRuleCmd.Flags()
ff.StringArray(ruleFlag, []string{}, "Rule statement") 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(chainIDFlag, "", "Assign ID to the parsed chain")
ff.String(targetNameFlag, "", targetNameDesc) ff.String(targetNameFlag, "", targetNameDesc)
ff.String(targetTypeFlag, "", targetTypeDesc) ff.String(targetTypeFlag, "", targetTypeDesc)
_ = addRuleCmd.MarkFlagRequired(targetTypeFlag) _ = addRuleCmd.MarkFlagRequired(targetTypeFlag)
ff.Bool(chainIDHexFlag, false, "Flag to parse chain ID as hex") ff.Bool(chainIDHexFlag, false, "Flag to parse chain ID as hex")
addRuleCmd.MarkFlagsMutuallyExclusive(pathFlag, ruleFlag)
} }

View file

@ -3,6 +3,7 @@ package util
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"strconv" "strconv"
"strings" "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. // ParseAPEChain parses APE chain rules.
func ParseAPEChain(chain *apechain.Chain, rules []string) error { func ParseAPEChain(chain *apechain.Chain, rules []string) error {
if len(rules) == 0 { if len(rules) == 0 {
@ -82,6 +100,10 @@ func ParseAPEChain(chain *apechain.Chain, rules []string) error {
// deny:QuotaLimitReached Object.Put * // deny:QuotaLimitReached Object.Put *
// allow Object.Put * // allow Object.Put *
// allow Object.Get Object.Resource:Department=HR Object.Request:Actor=ownerA * // 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 //nolint:godot
func ParseAPERule(r *apechain.Rule, rule string) error { func ParseAPERule(r *apechain.Rule, rule string) error {
@ -92,6 +114,18 @@ func ParseAPERule(r *apechain.Rule, rule string) error {
return parseRuleLexemes(r, lexemes) 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 { func parseRuleLexemes(r *apechain.Rule, lexemes []string) error {
if len(lexemes) < 2 { if len(lexemes) < 2 {
return errInvalidStatementFormat return errInvalidStatementFormat
@ -105,9 +139,15 @@ func parseRuleLexemes(r *apechain.Rule, lexemes []string) error {
var isObject *bool var isObject *bool
for i, lexeme := range lexemes[1:] { 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 var actionType bool
name, actionType, err = parseAction(lexeme) names, actionType, err = parseAction(lexeme)
if err != nil { if err != nil {
condition, errCond := parseCondition(lexeme) condition, errCond := parseCondition(lexeme)
if errCond != nil { if errCond != nil {
@ -118,7 +158,7 @@ func parseRuleLexemes(r *apechain.Rule, lexemes []string) error {
actionType = condition.Object == apechain.ObjectResource || condition.Object == apechain.ObjectRequest actionType = condition.Object == apechain.ObjectResource || condition.Object == apechain.ObjectRequest
r.Condition = append(r.Condition, *condition) r.Condition = append(r.Condition, *condition)
} else { } else {
r.Actions.Names = append(r.Actions.Names, name) r.Actions.Names = append(r.Actions.Names, names...)
} }
if isObject == nil { if isObject == nil {
isObject = &actionType isObject = &actionType
@ -126,6 +166,7 @@ func parseRuleLexemes(r *apechain.Rule, lexemes []string) error {
return errMixedTypesInRule return errMixedTypesInRule
} }
} }
r.Actions.Names = unique(r.Actions.Names)
if len(r.Actions.Names) == 0 { if len(r.Actions.Names) == 0 {
return fmt.Errorf("%w:%w", err, errNoActionsInRule) return fmt.Errorf("%w:%w", err, errNoActionsInRule)
} }
@ -140,6 +181,17 @@ func parseRuleLexemes(r *apechain.Rule, lexemes []string) error {
return nil 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) { func parseStatus(lexeme string) (apechain.Status, error) {
action, expression, found := strings.Cut(lexeme, ":") action, expression, found := strings.Cut(lexeme, ":")
switch strings.ToLower(action) { 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) { switch strings.ToLower(lexeme) {
case "object.put": case "object.put":
return nativeschema.MethodPutObject, true, nil return []string{nativeschema.MethodPutObject}, true, nil
case "object.get": case "object.get":
return nativeschema.MethodGetObject, true, nil return []string{nativeschema.MethodGetObject}, true, nil
case "object.head": case "object.head":
return nativeschema.MethodHeadObject, true, nil return []string{nativeschema.MethodHeadObject}, true, nil
case "object.delete": case "object.delete":
return nativeschema.MethodDeleteObject, true, nil return []string{nativeschema.MethodDeleteObject}, true, nil
case "object.search": case "object.search":
return nativeschema.MethodSearchObject, true, nil return []string{nativeschema.MethodSearchObject}, true, nil
case "object.range": case "object.range":
return nativeschema.MethodRangeObject, true, nil return []string{nativeschema.MethodRangeObject}, true, nil
case "object.hash": 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": case "container.put":
return nativeschema.MethodPutContainer, false, nil return []string{nativeschema.MethodPutContainer}, false, nil
case "container.delete": case "container.delete":
return nativeschema.MethodDeleteContainer, false, nil return []string{nativeschema.MethodDeleteContainer}, false, nil
case "container.get": case "container.get":
return nativeschema.MethodGetContainer, false, nil return []string{nativeschema.MethodGetContainer}, false, nil
case "container.setcontainereacl": case "container.setcontainereacl":
return nativeschema.MethodSetContainerEACL, false, nil return []string{nativeschema.MethodSetContainerEACL}, false, nil
case "container.getcontainereacl": 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: 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) { func parseResource(lexeme string, isObj bool) (string, error) {