From 51d1d935ef60660cfdd32afccd1ac97dfbfc35b8 Mon Sep 17 00:00:00 2001 From: Anton Nikiforov Date: Thu, 25 Jan 2024 20:25:23 +0300 Subject: [PATCH] [#876] cli: Add support for `container` in local rules Signed-off-by: Anton Nikiforov --- cmd/frostfs-cli/modules/control/add_rule.go | 29 +-- cmd/frostfs-cli/modules/control/get_rule.go | 5 +- cmd/frostfs-cli/modules/control/list_rules.go | 4 +- cmd/frostfs-cli/modules/util/ape.go | 216 ++++++++++++------ cmd/frostfs-cli/modules/util/ape_test.go | 214 +++++++++++++++-- 5 files changed, 351 insertions(+), 117 deletions(-) diff --git a/cmd/frostfs-cli/modules/control/add_rule.go b/cmd/frostfs-cli/modules/control/add_rule.go index 4fe8194b..7e0805cd 100644 --- a/cmd/frostfs-cli/modules/control/add_rule.go +++ b/cmd/frostfs-cli/modules/control/add_rule.go @@ -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_detail] [ ...] ", - 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") } diff --git a/cmd/frostfs-cli/modules/control/get_rule.go b/cmd/frostfs-cli/modules/control/get_rule.go index 9508850e..e2271c44 100644 --- a/cmd/frostfs-cli/modules/control/get_rule.go +++ b/cmd/frostfs-cli/modules/control/get_rule.go @@ -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() { diff --git a/cmd/frostfs-cli/modules/control/list_rules.go b/cmd/frostfs-cli/modules/control/list_rules.go index ef494b13..c6265176 100644 --- a/cmd/frostfs-cli/modules/control/list_rules.go +++ b/cmd/frostfs-cli/modules/control/list_rules.go @@ -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) } } diff --git a/cmd/frostfs-cli/modules/util/ape.go b/cmd/frostfs-cli/modules/util/ape.go index 79966613..6a7eca94 100644 --- a/cmd/frostfs-cli/modules/util/ape.go +++ b/cmd/frostfs-cli/modules/util/ape.go @@ -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_detail] [ ...] +// [:status_detail] ... [...] ... // // 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) } diff --git a/cmd/frostfs-cli/modules/util/ape_test.go b/cmd/frostfs-cli/modules/util/ape_test.go index 1cab8e6a..3e476609 100644 --- a/cmd/frostfs-cli/modules/util/ape_test.go +++ b/cmd/frostfs-cli/modules/util/ape_test.go @@ -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) {