cli: Add support for container in local rules #921

Merged
fyrchik merged 3 commits from acid-ant/frostfs-node:feature/876-cli-policy into master 2024-09-04 19:51:05 +00:00
7 changed files with 467 additions and 120 deletions

View file

@ -0,0 +1,115 @@
# How manage local Access Policy Engine (APE) override of the node
## Overview
APE is a replacement for eACL. Each rule can restrict somehow access to the object/container or list of them.
Here is a simple representation for the rule:
`<status>[:status_detail] <action>... <condition>... <resource>...`
Rule start with `status`(with or without details), contains list of actions(which this rule regulate) or conditions
(which can be under resource or request) and ends with list of resources.
Resource is the combination of namespace, identificator of the FrostFS container/object and wildcard `*`.
For object it can be represented as:
- `namespace/cid/oid` object in the container of the namespace
- `namespace/cid/*` all objects in the container of the namespace
- `namespace/*` all objects in the namespace
- `*` all objects
- `/*` all object in the `root` namespace
- `/cid/*` all objects in the container of the `root` namespace
- `/cid/oid` object in the container of the `root` namespace
For container it can be represented as:
- `namespace/cid` container in the namespace
- `namespace/*` all containers in the namespace
- `*` all containers
- `/cid` container 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.
In status section it is possible to use `allow`, `deny` or `deny:QuotaLimitReached` actions.
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.
## Add rule
Local rule can be added with the command `frostfs-cli control add-rule`:
```shell
@:~$ frostfs-cli control add-rule --endpoint s04.frostfs.devenv:8081 -c cnt_create_cfg.yml \
fyrchik marked this conversation as resolved
Review

What is the meaning of @:~$, is it some macro?

What is the meaning of `@:~$`, is it some macro?
Review

Means cosole input like user@USER-123:~$

Means cosole input like `user@USER-123:~$`
--address NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM --cid SeHNpifDH2Fc4scNBphrbmrKi96QXj2HzYJkhSGuytH \
--chain-id TestPolicy \
--rule "allow Object.Get Object.Head /*" --rule "deny Container.Put *"
Parsed chain:
Chain ID: TestPolicy
HEX: 54657374506f6c696379
Rules:
Status: Allowed
Any: false
Conditions:
Actions: Inverted:false
GetObject
HeadObject
Resources: Inverted:false
native:object//*
Status: Access denied
Any: false
Conditions:
Actions: Inverted:false
PutContainer
Resources: Inverted:false
native:container/*
Rule has been added.
@:~$
```
## List rules
Local rules can be listed with command `frostfs-cli control list-rules`:
```shell
@:~$ frostfs-cli control list-rules --endpoint s04.frostfs.devenv:8081 --address NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM \
--cid SeHNpifDH2Fc4scNBphrbmrKi96QXj2HzYJkhSGuytH -w wallets/wallet.json
Enter password >
Chain ID: TestPolicy
HEX: 54657374506f6c696379
Rules:
Status: Allowed
Any: false
...
@:~$
```
## Get rule
Rules can be retrieved with `frostfs-cli control get-rule`:
```shell
@:~$ frostfs-cli control get-rule --endpoint s04.frostfs.devenv:8081 -c cnt_create_cfg.yml \
--address NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM --cid SeHNpifDH2Fc4scNBphrbmrKi96QXj2HzYJkhSGuytH \
--chain-id TestPolicy
Parsed chain (chain id hex: '54657374506f6c696379'):
Chain ID: TestPolicy
HEX: 54657374506f6c696379
Rules:
Status: Allowed
Any: false
...
@:~$
```
## Remove rule
To remove rule need to use command `frostfs-cli control remove-rule`:
```shell
@:~$ frostfs-cli control remove-rule --endpoint s04.frostfs.devenv:8081 -c cnt_create_cfg.yml \
--address NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM --cid SeHNpifDH2Fc4scNBphrbmrKi96QXj2HzYJkhSGuytH --chain-id TestPolicy
Rule has been removed.
@:~$ frostfs-cli control get-rule --endpoint s04.frostfs.devenv:8081 -c cnt_create_cfg.yml \
--address NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM --cid SeHNpifDH2Fc4scNBphrbmrKi96QXj2HzYJkhSGuytH --chain-id TestPolicy
rpc error: rpc error: code = NotFound desc = chain not found
@:~$ frostfs-cli control list-rules --endpoint s04.frostfs.devenv:8081 \
--address NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM --cid SeHNpifDH2Fc4scNBphrbmrKi96QXj2HzYJkhSGuytH -w wallets/wallet.json
Enter password >
Local overrides are not defined for the container.
@:~$
```

View file

@ -1,10 +1,8 @@
package control package control
import ( import (
"bytes"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json"
"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/commonflags" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
@ -25,20 +23,13 @@ var addRuleCmd = &cobra.Command{
Use: "add-rule", Use: "add-rule",
Short: "Add local override", Short: "Add local override",
Long: "Add local APE rule to a node with following format:\n<action>[:action_detail] <operation> [<condition1> ...] <resource>", Long: "Add local APE rule to a node with following format:\n<action>[:action_detail] <operation> [<condition1> ...] <resource>",
Example: `allow Object.Get * Example: `control add-rule --endpoint ... -w ... --address ... --chain-id ChainID --cid ... --rule "allow Object.Get *"
deny Object.Get EbxzAdz5LB4uqxuz6crWKAumBNtZyK2rKsqQP7TdZvwr/* --rule "deny Object.Get EbxzAdz5LB4uqxuz6crWKAumBNtZyK2rKsqQP7TdZvwr/*"
deny:QuotaLimitReached Object.Put Object.Resource:Department=HR * --rule "deny:QuotaLimitReached Object.Put Object.Resource:Department=HR *"
`, `,
Run: addRule, Run: addRule,
dstepanov-yadro marked this conversation as resolved Outdated

I think it is better to drop link from example: in case of docs autogenerating, it will be strange to have external link

I think it is better to drop link from example: in case of docs autogenerating, it will be strange to have external link

Agree, removed.

Agree, removed.
} }
func prettyJSONFormat(cmd *cobra.Command, serializedChain []byte) string {
wr := bytes.NewBufferString("")
err := json.Indent(wr, serializedChain, "", " ")
commonCmd.ExitOnErr(cmd, "%w", err)
return wr.String()
}
func addRule(cmd *cobra.Command, _ []string) { func addRule(cmd *cobra.Command, _ []string) {
pk := key.Get(cmd) pk := key.Get(cmd)
@ -60,15 +51,15 @@ func addRule(cmd *cobra.Command, _ []string) {
rawCID := make([]byte, sha256.Size) rawCID := make([]byte, sha256.Size)
cnr.Encode(rawCID) cnr.Encode(rawCID)
rule, _ := cmd.Flags().GetString(ruleFlag) rule, _ := cmd.Flags().GetStringArray(ruleFlag)
chain := new(apechain.Chain) chain := new(apechain.Chain)
commonCmd.ExitOnErr(cmd, "parser error: %w", util.ParseAPEChain(chain, []string{rule})) commonCmd.ExitOnErr(cmd, "parser error: %w", util.ParseAPEChain(chain, rule))
chain.ID = apechain.ID(chainIDRaw) chain.ID = apechain.ID(chainIDRaw)
serializedChain := chain.Bytes() serializedChain := chain.Bytes()
cmd.Println("CID: " + cidStr) cmd.Println("Parsed chain:")
cmd.Println("Parsed chain:\n" + prettyJSONFormat(cmd, serializedChain)) util.PrintHumanReadableAPEChain(cmd, chain)
req := &control.AddChainLocalOverrideRequest{ req := &control.AddChainLocalOverrideRequest{
Body: &control.AddChainLocalOverrideRequest_Body{ Body: &control.AddChainLocalOverrideRequest_Body{
@ -93,9 +84,7 @@ func addRule(cmd *cobra.Command, _ []string) {
commonCmd.ExitOnErr(cmd, "rpc error: %w", err) commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
verifyResponse(cmd, resp.GetSignature(), resp.GetBody()) verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
cmd.Println("\nRule has been added.")
chainIDRaw = resp.GetBody().GetChainId()
cmd.Printf("Rule has been added.\nChain id: '%s'\nChain id hex: '%x'\n", string(chainIDRaw), chainIDRaw)
} }
func initControlAddRuleCmd() { func initControlAddRuleCmd() {
@ -103,7 +92,7 @@ func initControlAddRuleCmd() {
ff := addRuleCmd.Flags() ff := addRuleCmd.Flags()
ff.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage) ff.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
ff.String(ruleFlag, "", "Rule statement") ff.StringArray(ruleFlag, []string{}, "Rule statement")
ff.String(chainIDFlag, "", "Assign ID to the parsed chain") ff.String(chainIDFlag, "", "Assign ID to the parsed chain")
ff.Bool(chainIDHexFlag, false, "Flag to parse chain ID as hex") ff.Bool(chainIDHexFlag, false, "Flag to parse chain ID as hex")
} }

View file

@ -7,6 +7,7 @@ import (
"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/commonflags" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common" commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
@ -66,9 +67,7 @@ func getRule(cmd *cobra.Command, _ []string) {
var chain apechain.Chain var chain apechain.Chain
commonCmd.ExitOnErr(cmd, "decode error: %w", chain.DecodeBytes(resp.GetBody().GetChain())) commonCmd.ExitOnErr(cmd, "decode error: %w", chain.DecodeBytes(resp.GetBody().GetChain()))
util.PrintHumanReadableAPEChain(cmd, &chain)
// TODO (aarifullin): make pretty-formatted output for chains.
cmd.Printf("Parsed chain (chain id hex: '%x'):\n%s\n", chain.ID, prettyJSONFormat(cmd, chain.Bytes()))
} }
func initControGetRuleCmd() { func initControGetRuleCmd() {

View file

@ -6,6 +6,7 @@ import (
"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/commonflags" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common" commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
@ -60,10 +61,9 @@ func listRules(cmd *cobra.Command, _ []string) {
} }
for _, c := range chains { for _, c := range chains {
// TODO (aarifullin): make pretty-formatted output for chains.
var chain apechain.Chain var chain apechain.Chain
commonCmd.ExitOnErr(cmd, "decode error: %w", chain.DecodeBytes(c)) commonCmd.ExitOnErr(cmd, "decode error: %w", chain.DecodeBytes(c))
cmd.Printf("Parsed chain (chain id hex: '%x'):\n%s\n", chain.ID, prettyJSONFormat(cmd, chain.Bytes())) util.PrintHumanReadableAPEChain(cmd, &chain)
} }
} }

View file

@ -15,19 +15,23 @@ import (
var ( var (
errInvalidStatementFormat = errors.New("invalid statement format") errInvalidStatementFormat = errors.New("invalid statement format")
errInvalidConditionFormat = errors.New("invalid condition 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") 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") errUnknownBinaryOperator = errors.New("binary operator is not recognized")
errUnknownCondObjectType = errors.New("condition object type is not recognized") errUnknownCondObjectType = errors.New("condition object type is not recognized")
errMixedTypesInRule = errors.New("found mixed type of actions and conditions in rule")
errNoActionsInRule = errors.New("there are no actions in rule")
errUnsupportedResourceFormat = errors.New("unsupported resource format")
) )
// PrintHumanReadableAPEChain print APE chain rules. // PrintHumanReadableAPEChain print APE chain rules.
func PrintHumanReadableAPEChain(cmd *cobra.Command, chain *apechain.Chain) { func PrintHumanReadableAPEChain(cmd *cobra.Command, chain *apechain.Chain) {
cmd.Println("Chain ID: " + string(chain.ID)) cmd.Println("Chain ID: " + string(chain.ID))
cmd.Printf(" HEX: %x\n", chain.ID)
cmd.Println("Rules:") cmd.Println("Rules:")
for _, rule := range chain.Rules { for _, rule := range chain.Rules {
cmd.Println("\tStatus: " + rule.Status.String()) cmd.Println("\n\tStatus: " + rule.Status.String())
cmd.Println("\tAny: " + strconv.FormatBool(rule.Any)) cmd.Println("\tAny: " + strconv.FormatBool(rule.Any))
cmd.Println("\tConditions:") cmd.Println("\tConditions:")
for _, c := range rule.Condition { for _, c := range rule.Condition {
@ -71,7 +75,7 @@ func ParseAPEChain(chain *apechain.Chain, rules []string) error {
} }
// ParseAPERule parses access-policy-engine statement from the following form: // ParseAPERule parses access-policy-engine statement from the following form:
// <action>[:action_detail] <operation> [<condition1> ...] <resource> // <status>[:status_detail] <action>... [<condition>...] <resource>...
// //
// Examples: // Examples:
// deny Object.Put * // deny Object.Put *
@ -99,83 +103,153 @@ func parseRuleLexemes(r *apechain.Rule, lexemes []string) error {
return err return err
} }
r.Actions, err = parseAction(lexemes[1]) var isObject *bool
for i, lexeme := range lexemes[1:] {
var name string
var actionType bool
name, actionType, err = parseAction(lexeme)
if err != nil { if err != nil {
return err condition, errCond := parseCondition(lexeme)
if errCond != nil {
err = fmt.Errorf("%w:%w", err, errCond)

Looks like err can be not nil but not returned. Is it ok?

Looks like `err` can be not nil but not returned. Is it ok?

In the rule, it is possible to mix actions and conditions, but resources should be always at the end of the rule. The same is for status - rule should always start with it. So when lexeme neither action, neither condition - it is resource. If it is not a resource - error will contain all parsing errors.

In the `rule`, it is possible to mix `actions` and `conditions`, but `resources` should be always at the end of the `rule`. The same is for `status` - `rule` should always start with it. So when `lexeme` neither `action`, neither `condition` - it is `resource`. If it is not a `resource` - error will contain all parsing errors.
lexemes = lexemes[i+1:]
break
}
actionType = condition.Object == apechain.ObjectResource || condition.Object == apechain.ObjectRequest

Just :)

actionType  = condition.Object == apechain.ObjectResource || condition.Object == apechain.ObjectRequest
Just :) ```go actionType = condition.Object == apechain.ObjectResource || condition.Object == apechain.ObjectRequest ```

OMG! Shame on me! Fixed.

OMG! Shame on me! Fixed.
r.Condition = append(r.Condition, *condition)
} else {
r.Actions.Names = append(r.Actions.Names, name)
}
if isObject == nil {
isObject = &actionType
} else if actionType != *isObject {
return errMixedTypesInRule
}
}
if len(r.Actions.Names) == 0 {
return fmt.Errorf("%w:%w", err, errNoActionsInRule)
}
for _, lexeme := range lexemes {
resource, errRes := parseResource(lexeme, *isObject)
if errRes != nil {
return fmt.Errorf("%w:%w", err, errRes)
}
r.Resources.Names = append(r.Resources.Names, resource)
} }
r.Condition, err = parseConditions(lexemes[2 : len(lexemes)-1]) return nil
if err != nil {
return err
}
r.Resources, err = parseResource(lexemes[len(lexemes)-1])
return err
} }
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 action = strings.ToLower(action); action { switch strings.ToLower(action) {
case "deny": case "deny":
if !found { if !found {
return apechain.AccessDenied, nil return apechain.AccessDenied, nil
} else if strings.EqualFold(expression, "QuotaLimitReached") { } else if strings.EqualFold(expression, "QuotaLimitReached") {
return apechain.QuotaLimitReached, nil return apechain.QuotaLimitReached, nil
} else { } else {
return 0, fmt.Errorf("%w: %s", errUnknownActionDetail, expression) return 0, fmt.Errorf("%w: %s", errUnknownStatusDetail, expression)
} }
case "allow": case "allow":
if found { if found {
return 0, errUnknownActionDetail return 0, errUnknownStatusDetail
} }
return apechain.Allow, nil return apechain.Allow, nil
default: default:
return 0, errUnknownAction return 0, errUnknownStatus
} }
} }
func parseAction(lexeme string) (apechain.Actions, error) { func parseAction(lexeme string) (string, bool, error) {
switch strings.ToLower(lexeme) { switch strings.ToLower(lexeme) {
case "object.put": case "object.put":
return apechain.Actions{Names: []string{nativeschema.MethodPutObject}}, nil return nativeschema.MethodPutObject, true, nil
case "object.get": case "object.get":
return apechain.Actions{Names: []string{nativeschema.MethodGetObject}}, nil return nativeschema.MethodGetObject, true, nil
case "object.head": case "object.head":
return apechain.Actions{Names: []string{nativeschema.MethodHeadObject}}, nil return nativeschema.MethodHeadObject, true, nil
case "object.delete": case "object.delete":
return apechain.Actions{Names: []string{nativeschema.MethodDeleteObject}}, nil return nativeschema.MethodDeleteObject, true, nil
case "object.search": case "object.search":
return apechain.Actions{Names: []string{nativeschema.MethodSearchObject}}, nil return nativeschema.MethodSearchObject, true, nil
case "object.range": case "object.range":
return apechain.Actions{Names: []string{nativeschema.MethodRangeObject}}, nil return nativeschema.MethodRangeObject, true, nil
case "object.hash": case "object.hash":
return apechain.Actions{Names: []string{nativeschema.MethodHashObject}}, nil return nativeschema.MethodHashObject, true, nil
case "container.put":
return nativeschema.MethodPutContainer, false, nil
case "container.delete":
return nativeschema.MethodDeleteContainer, false, nil
case "container.get":
return nativeschema.MethodGetContainer, false, nil
case "container.setcontainereacl":
return nativeschema.MethodSetContainerEACL, false, nil
case "container.getcontainereacl":
return nativeschema.MethodGetContainerEACL, false, nil
default: default:
} }
return apechain.Actions{}, fmt.Errorf("%w: %s", errUnknownOperation, lexeme) return "", false, fmt.Errorf("%w: %s", errUnknownAction, lexeme)
} }
func parseResource(lexeme string) (apechain.Resources, error) { func parseResource(lexeme string, isObj bool) (string, error) {
if len(lexeme) > 0 && !strings.HasSuffix(lexeme, "/") {
if isObj {
if lexeme == "*" { if lexeme == "*" {
return apechain.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, nil return nativeschema.ResourceFormatAllObjects, nil
} else if lexeme == "/*" {
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
} }
return apechain.Resources{Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, 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 ( const (
ObjectResource = "object.resource" ObjectResource = "object.resource"
ObjectRequest = "object.request" ObjectRequest = "object.request"
ContainerResource = "container.resource"
ContainerRequest = "container.request"
) )
var typeToCondObject = map[string]apechain.ObjectType{ var typeToCondObject = map[string]apechain.ObjectType{
ObjectResource: apechain.ObjectResource, ObjectResource: apechain.ObjectResource,
ObjectRequest: apechain.ObjectRequest, ObjectRequest: apechain.ObjectRequest,
ContainerResource: apechain.ContainerResource,
ContainerRequest: apechain.ContainerRequest,
} }
func parseConditions(lexemes []string) ([]apechain.Condition, error) { func parseCondition(lexeme string) (*apechain.Condition, error) {
conds := make([]apechain.Condition, 0)
for _, lexeme := range lexemes {
typ, expression, found := strings.Cut(lexeme, ":") typ, expression, found := strings.Cut(lexeme, ":")
typ = strings.ToLower(typ) typ = strings.ToLower(typ)
@ -185,13 +259,10 @@ func parseConditions(lexemes []string) ([]apechain.Condition, error) {
return nil, fmt.Errorf("%w: %s", errInvalidConditionFormat, lexeme) return nil, fmt.Errorf("%w: %s", errInvalidConditionFormat, lexeme)
} }
var lhs, rhs string
var binExpFound bool
var cond apechain.Condition var cond apechain.Condition
cond.Object = objType cond.Object = objType
lhs, rhs, binExpFound = strings.Cut(expression, "!=") lhs, rhs, binExpFound := strings.Cut(expression, "!=")
if !binExpFound { if !binExpFound {
lhs, rhs, binExpFound = strings.Cut(expression, "=") lhs, rhs, binExpFound = strings.Cut(expression, "=")
if !binExpFound { if !binExpFound {
@ -203,12 +274,7 @@ func parseConditions(lexemes []string) ([]apechain.Condition, error) {
} }
cond.Key, cond.Value = lhs, rhs cond.Key, cond.Value = lhs, rhs
return &cond, nil
conds = append(conds, cond) }
} else {
return nil, fmt.Errorf("%w: %s", errUnknownCondObjectType, typ) return nil, fmt.Errorf("%w: %s", errUnknownCondObjectType, typ)
} }
}
return conds, nil
}

View file

@ -1,6 +1,7 @@
package util package util
import ( import (
"fmt"
"testing" "testing"
policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
@ -16,13 +17,76 @@ func TestParseAPERule(t *testing.T) {
expectRule policyengine.Rule expectRule policyengine.Rule
}{ }{
{ {
name: "Valid allow rule", name: "Valid allow rule for all objects",
rule: "allow Object.Put *", rule: "allow Object.Put *",
expectRule: policyengine.Rule{
Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllObjects}},
},
},
{
name: "Valid rule for all objects in root namespace",
rule: "allow Object.Put /*",
expectRule: policyengine.Rule{ expectRule: policyengine.Rule{
Status: policyengine.Allow, Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}}, Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}},
Condition: []policyengine.Condition{}, },
},
{
name: "Valid rule for all objects in root namespace and container",
rule: "allow Object.Put /cid/*",
expectRule: policyengine.Rule{
Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
Resources: policyengine.Resources{Names: []string{
fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, "cid"),
}},
},
},
{
name: "Valid rule for object in root namespace and container",
rule: "allow Object.Put /cid/oid",
expectRule: policyengine.Rule{
Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
Resources: policyengine.Resources{Names: []string{
fmt.Sprintf(nativeschema.ResourceFormatRootContainerObject, "cid", "oid"),
}},
},
},
{
name: "Valid rule for all objects in namespace",
rule: "allow Object.Put ns/*",
expectRule: policyengine.Rule{
Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
Resources: policyengine.Resources{Names: []string{
fmt.Sprintf(nativeschema.ResourceFormatNamespaceObjects, "ns"),
}},
},
},
{
name: "Valid rule for all objects in namespace and container",
rule: "allow Object.Put ns/cid/*",
expectRule: policyengine.Rule{
Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
Resources: policyengine.Resources{Names: []string{
fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainerObjects, "ns", "cid"),
}},
},
},
{
name: "Valid rule for object in namespace and container",
rule: "allow Object.Put ns/cid/oid",
expectRule: policyengine.Rule{
Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
Resources: policyengine.Resources{Names: []string{
fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainerObject, "ns", "cid", "oid"),
}},
}, },
}, },
{ {
@ -31,8 +95,7 @@ func TestParseAPERule(t *testing.T) {
expectRule: policyengine.Rule{ expectRule: policyengine.Rule{
Status: policyengine.AccessDenied, Status: policyengine.AccessDenied,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}}, Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllObjects}},
Condition: []policyengine.Condition{},
}, },
}, },
{ {
@ -41,8 +104,7 @@ func TestParseAPERule(t *testing.T) {
expectRule: policyengine.Rule{ expectRule: policyengine.Rule{
Status: policyengine.QuotaLimitReached, Status: policyengine.QuotaLimitReached,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}}, Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllObjects}},
Condition: []policyengine.Condition{},
}, },
}, },
{ {
@ -51,7 +113,7 @@ func TestParseAPERule(t *testing.T) {
expectRule: policyengine.Rule{ expectRule: policyengine.Rule{
Status: policyengine.Allow, Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodGetObject}}, Actions: policyengine.Actions{Names: []string{nativeschema.MethodGetObject}},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllObjects}},
Condition: []policyengine.Condition{ Condition: []policyengine.Condition{
{ {
Op: policyengine.CondStringEquals, Op: policyengine.CondStringEquals,
@ -69,12 +131,12 @@ func TestParseAPERule(t *testing.T) {
}, },
}, },
{ {
name: "Valid rule with conditions with action detail", name: "Valid rule for object with conditions with action detail",
rule: "deny:QuotaLimitReached Object.Get Object.Resource:Department=HR Object.Request:Actor!=ownerA *", rule: "deny:QuotaLimitReached Object.Get Object.Resource:Department=HR Object.Request:Actor!=ownerA *",
expectRule: policyengine.Rule{ expectRule: policyengine.Rule{
Status: policyengine.QuotaLimitReached, Status: policyengine.QuotaLimitReached,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodGetObject}}, Actions: policyengine.Actions{Names: []string{nativeschema.MethodGetObject}},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllObjects}},
Condition: []policyengine.Condition{ Condition: []policyengine.Condition{
{ {
Op: policyengine.CondStringEquals, Op: policyengine.CondStringEquals,
@ -92,19 +154,19 @@ func TestParseAPERule(t *testing.T) {
}, },
}, },
{ {
name: "Invalid rule with unknown action", name: "Invalid rule with unknown status",
rule: "permit Object.Put *", rule: "permit Object.Put *",
expectErr: errUnknownStatus,
},
{
name: "Invalid rule with unknown action",
rule: "allow Object.PutOut *",
expectErr: errUnknownAction, expectErr: errUnknownAction,
}, },
{ {
name: "Invalid rule with unknown operation", name: "Invalid rule with unknown status detail",
rule: "allow Object.PutOut *",
expectErr: errUnknownOperation,
},
{
name: "Invalid rule with unknown action detail",
rule: "deny:UnknownActionDetail Object.Put *", rule: "deny:UnknownActionDetail Object.Put *",
expectErr: errUnknownActionDetail, expectErr: errUnknownStatusDetail,
}, },
{ {
name: "Invalid rule with unknown condition binary operator", name: "Invalid rule with unknown condition binary operator",
@ -116,6 +178,124 @@ func TestParseAPERule(t *testing.T) {
rule: "deny Object.Put Object.ResourZe:Department=HR *", rule: "deny Object.Put Object.ResourZe:Department=HR *",
expectErr: errUnknownCondObjectType, expectErr: errUnknownCondObjectType,
}, },
{
name: "Invalid rule with mixed types of actions",
rule: "allow Object.Put Container.Put *",
expectErr: errMixedTypesInRule,
},
{
name: "Invalid rule with no actions",
rule: "allow Container.Resource:A=B *",
expectErr: errNoActionsInRule,
},
{
name: "Invalid rule with invalid resource for object nm/cnt/obj/err",
rule: "allow Object.Put nm/cnt/obj/err",
expectErr: errUnsupportedResourceFormat,
},
{
name: "Invalid rule with invalid resource for container nm/cnt/err",
rule: "allow Container.Put nm/cnt/err",
expectErr: errUnsupportedResourceFormat,
},
{
name: "Invalid rule with invalid resource for container /nm/cnt/err",
rule: "allow Container.Put /nm/cnt/err",
expectErr: errUnsupportedResourceFormat,
},
{
name: "Invalid rule with invalid resource for container /nm/cnt/",
rule: "allow Container.Put /nm/cnt/",
expectErr: errUnsupportedResourceFormat,
},
{
name: "Invalid rule with invalid resource for container /nm/cnt",
rule: "allow Container.Put /nm/cnt",
expectErr: errUnsupportedResourceFormat,
},
{
name: "Invalid rule with invalid resource for container /nm/",
rule: "allow Container.Put /nm/",
expectErr: errUnsupportedResourceFormat,
},
{
name: "Valid rule for all containers",
rule: "allow Container.Put *",
expectRule: policyengine.Rule{
Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutContainer}},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllContainers}},
},
},
{
name: "Valid rule for all containers in root namespace",
rule: "allow Container.Put /*",
expectRule: policyengine.Rule{
Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutContainer}},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootContainers}},
},
},
{
name: "Valid rule for container in root namespace",
rule: "allow Container.Put /cid",
expectRule: policyengine.Rule{
Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutContainer}},
Resources: policyengine.Resources{Names: []string{
fmt.Sprintf(nativeschema.ResourceFormatRootContainer, "cid"),
}},
},
},
{
name: "Valid rule for all container in namespace",
rule: "allow Container.Put ns/*",
expectRule: policyengine.Rule{
Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutContainer}},
Resources: policyengine.Resources{Names: []string{
fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, "ns"),
}},
},
},
{
name: "Valid rule for container in namespace",
rule: "allow Container.Put ns/cid",
expectRule: policyengine.Rule{
Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutContainer}},
Resources: policyengine.Resources{Names: []string{
fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainer, "ns", "cid"),
}},
},
},
{
name: "Valid rule for container with conditions with action detail",
rule: "allow Container.Get Container.Resource:A=B Container.Put Container.Request:C!=D " +
"* /cnt_id",
expectRule: policyengine.Rule{
Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodGetContainer, nativeschema.MethodPutContainer}},
Resources: policyengine.Resources{Names: []string{
nativeschema.ResourceFormatAllContainers,
fmt.Sprintf(nativeschema.ResourceFormatRootContainer, "cnt_id"),
}},
Condition: []policyengine.Condition{
{
Op: policyengine.CondStringEquals,
Object: policyengine.ContainerResource,
Key: "A",
Value: "B",
},
{
Op: policyengine.CondStringNotEquals,
Object: policyengine.ContainerRequest,
Key: "C",
Value: "D",
},
},
},
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {

View file

@ -47,9 +47,7 @@ type Prm struct {
SenderKey string SenderKey string
} }
var ( var errMissingOID = fmt.Errorf("object ID is not set")
errMissingOID = fmt.Errorf("object ID is not set")
)
// CheckAPE checks if a request or a response is permitted creating an ape request and passing // CheckAPE checks if a request or a response is permitted creating an ape request and passing
// it to chain router. // it to chain router.