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
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
@ -25,20 +23,13 @@ var addRuleCmd = &cobra.Command{
Use: "add-rule",
Short: "Add local override",
Long: "Add local APE rule to a node with following format:\n<action>[:action_detail] <operation> [<condition1> ...] <resource>",
Example: `allow Object.Get *
deny Object.Get EbxzAdz5LB4uqxuz6crWKAumBNtZyK2rKsqQP7TdZvwr/*
deny:QuotaLimitReached Object.Put Object.Resource:Department=HR *
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 *"
`,
Run: addRule,
}
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) {
pk := key.Get(cmd)
@ -60,15 +51,15 @@ func addRule(cmd *cobra.Command, _ []string) {
rawCID := make([]byte, sha256.Size)
cnr.Encode(rawCID)
rule, _ := cmd.Flags().GetString(ruleFlag)
rule, _ := cmd.Flags().GetStringArray(ruleFlag)
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)
serializedChain := chain.Bytes()
cmd.Println("CID: " + cidStr)
cmd.Println("Parsed chain:\n" + prettyJSONFormat(cmd, serializedChain))
cmd.Println("Parsed chain:")
util.PrintHumanReadableAPEChain(cmd, chain)
req := &control.AddChainLocalOverrideRequest{
Body: &control.AddChainLocalOverrideRequest_Body{
@ -93,9 +84,7 @@ func addRule(cmd *cobra.Command, _ []string) {
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
chainIDRaw = resp.GetBody().GetChainId()
cmd.Printf("Rule has been added.\nChain id: '%s'\nChain id hex: '%x'\n", string(chainIDRaw), chainIDRaw)
cmd.Println("\nRule has been added.")
}
func initControlAddRuleCmd() {
@ -103,7 +92,7 @@ func initControlAddRuleCmd() {
ff := addRuleCmd.Flags()
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.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-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/modules/util"
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
@ -66,9 +67,7 @@ func getRule(cmd *cobra.Command, _ []string) {
var chain apechain.Chain
commonCmd.ExitOnErr(cmd, "decode error: %w", chain.DecodeBytes(resp.GetBody().GetChain()))
// 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()))
util.PrintHumanReadableAPEChain(cmd, &chain)
}
func initControGetRuleCmd() {

View file

@ -6,6 +6,7 @@ import (
"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/key"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
@ -60,10 +61,9 @@ func listRules(cmd *cobra.Command, _ []string) {
}
for _, c := range chains {
// TODO (aarifullin): make pretty-formatted output for chains.
var chain apechain.Chain
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

@ -13,21 +13,25 @@ import (
)
var (
errInvalidStatementFormat = errors.New("invalid statement format")
errInvalidConditionFormat = errors.New("invalid condition format")
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")
errUnknownCondObjectType = errors.New("condition object type is not recognized")
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 and conditions in rule")
errNoActionsInRule = errors.New("there are no actions in rule")
errUnsupportedResourceFormat = errors.New("unsupported resource format")
)
// PrintHumanReadableAPEChain print APE chain rules.
func PrintHumanReadableAPEChain(cmd *cobra.Command, chain *apechain.Chain) {
cmd.Println("ChainID: " + string(chain.ID))
cmd.Println("Chain ID: " + string(chain.ID))
cmd.Printf(" HEX: %x\n", chain.ID)
cmd.Println("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("\tConditions:")
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:
// <action>[:action_detail] <operation> [<condition1> ...] <resource>
// <status>[:status_detail] <action>... [<condition>...] <resource>...
//
// Examples:
// deny Object.Put *
@ -99,116 +103,178 @@ func parseRuleLexemes(r *apechain.Rule, lexemes []string) error {
return err
}
r.Actions, err = parseAction(lexemes[1])
if err != nil {
return err
var isObject *bool
for i, lexeme := range lexemes[1:] {
var name string
var actionType bool
name, 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
}
actionType = condition.Object == apechain.ObjectResource || condition.Object == apechain.ObjectRequest
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])
if err != nil {
return err
}
r.Resources, err = parseResource(lexemes[len(lexemes)-1])
return err
return nil
}
func parseStatus(lexeme string) (apechain.Status, error) {
action, expression, found := strings.Cut(lexeme, ":")
switch action = strings.ToLower(action); action {
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", errUnknownActionDetail, expression)
return 0, fmt.Errorf("%w: %s", errUnknownStatusDetail, expression)
}
case "allow":
if found {
return 0, errUnknownActionDetail
return 0, errUnknownStatusDetail
}
return apechain.Allow, nil
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) {
case "object.put":
return apechain.Actions{Names: []string{nativeschema.MethodPutObject}}, nil
return nativeschema.MethodPutObject, true, nil
case "object.get":
return apechain.Actions{Names: []string{nativeschema.MethodGetObject}}, nil
return nativeschema.MethodGetObject, true, nil
case "object.head":
return apechain.Actions{Names: []string{nativeschema.MethodHeadObject}}, nil
return nativeschema.MethodHeadObject, true, nil
case "object.delete":
return apechain.Actions{Names: []string{nativeschema.MethodDeleteObject}}, nil
return nativeschema.MethodDeleteObject, true, nil
case "object.search":
return apechain.Actions{Names: []string{nativeschema.MethodSearchObject}}, nil
return nativeschema.MethodSearchObject, true, nil
case "object.range":
return apechain.Actions{Names: []string{nativeschema.MethodRangeObject}}, nil
return nativeschema.MethodRangeObject, true, nil
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:
}
return apechain.Actions{}, fmt.Errorf("%w: %s", errUnknownOperation, lexeme)
return "", false, fmt.Errorf("%w: %s", errUnknownAction, lexeme)
}
func parseResource(lexeme string) (apechain.Resources, error) {
if lexeme == "*" {
return apechain.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, nil
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 == "/*" {
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 apechain.Resources{Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, lexeme)}}, nil
return "", errUnsupportedResourceFormat
}
const (
ObjectResource = "object.resource"
ObjectRequest = "object.request"
ContainerResource = "container.resource"
ContainerRequest = "container.request"
)
var typeToCondObject = map[string]apechain.ObjectType{
ObjectResource: apechain.ObjectResource,
ObjectRequest: apechain.ObjectRequest,
ObjectResource: apechain.ObjectResource,
ObjectRequest: apechain.ObjectRequest,
ContainerResource: apechain.ContainerResource,
ContainerRequest: apechain.ContainerRequest,
}
func parseConditions(lexemes []string) ([]apechain.Condition, error) {
conds := make([]apechain.Condition, 0)
func parseCondition(lexeme string) (*apechain.Condition, error) {
typ, expression, found := strings.Cut(lexeme, ":")
typ = strings.ToLower(typ)
for _, lexeme := range lexemes {
typ, expression, found := strings.Cut(lexeme, ":")
typ = strings.ToLower(typ)
objType, ok := typeToCondObject[typ]
if ok {
if !found {
return nil, fmt.Errorf("%w: %s", errInvalidConditionFormat, lexeme)
}
var lhs, rhs string
var binExpFound bool
var cond apechain.Condition
cond.Object = objType
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
conds = append(conds, cond)
} else {
return nil, fmt.Errorf("%w: %s", errUnknownCondObjectType, typ)
objType, ok := typeToCondObject[typ]
if ok {
if !found {
return nil, fmt.Errorf("%w: %s", errInvalidConditionFormat, lexeme)
}
}
return conds, nil
var cond apechain.Condition
cond.Object = objType
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)
}

View file

@ -1,6 +1,7 @@
package util
import (
"fmt"
"testing"
policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
@ -16,13 +17,76 @@ func TestParseAPERule(t *testing.T) {
expectRule policyengine.Rule
}{
{
name: "Valid allow rule",
name: "Valid allow rule for all objects",
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{
Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
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{
Status: policyengine.AccessDenied,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}},
Condition: []policyengine.Condition{},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllObjects}},
},
},
{
@ -41,8 +104,7 @@ func TestParseAPERule(t *testing.T) {
expectRule: policyengine.Rule{
Status: policyengine.QuotaLimitReached,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}},
Condition: []policyengine.Condition{},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllObjects}},
},
},
{
@ -51,7 +113,7 @@ func TestParseAPERule(t *testing.T) {
expectRule: policyengine.Rule{
Status: policyengine.Allow,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodGetObject}},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllObjects}},
Condition: []policyengine.Condition{
{
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 *",
expectRule: policyengine.Rule{
Status: policyengine.QuotaLimitReached,
Actions: policyengine.Actions{Names: []string{nativeschema.MethodGetObject}},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}},
Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllObjects}},
Condition: []policyengine.Condition{
{
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 *",
expectErr: errUnknownStatus,
},
{
name: "Invalid rule with unknown action",
rule: "allow Object.PutOut *",
expectErr: errUnknownAction,
},
{
name: "Invalid rule with unknown operation",
rule: "allow Object.PutOut *",
expectErr: errUnknownOperation,
},
{
name: "Invalid rule with unknown action detail",
name: "Invalid rule with unknown status detail",
rule: "deny:UnknownActionDetail Object.Put *",
expectErr: errUnknownActionDetail,
expectErr: errUnknownStatusDetail,
},
{
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 *",
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 {
t.Run(test.name, func(t *testing.T) {

View file

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