[#876] cli: Add support for container in local rules

Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
This commit is contained in:
Anton Nikiforov 2024-01-25 20:25:23 +03:00
parent b6fc3321c5
commit 51d1d935ef
5 changed files with 351 additions and 117 deletions

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

@ -13,21 +13,25 @@ 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")
errUnknownAction = errors.New("action is not recognized") errUnknownStatus = errors.New("status is not recognized")
errUnknownOperation = errors.New("operation is not recognized") errUnknownStatusDetail = errors.New("status detail is not recognized")
errUnknownActionDetail = errors.New("action detail is not recognized") errUnknownAction = errors.New("action 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("ChainID: " + 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,116 +103,178 @@ func parseRuleLexemes(r *apechain.Rule, lexemes []string) error {
return err return err
} }
r.Actions, err = parseAction(lexemes[1]) var isObject *bool
if err != nil { for i, lexeme := range lexemes[1:] {
return err 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]) 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 lexeme == "*" { if len(lexeme) > 0 && !strings.HasSuffix(lexeme, "/") {
return apechain.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, nil 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 ( 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) typ, expression, found := strings.Cut(lexeme, ":")
typ = strings.ToLower(typ)
for _, lexeme := range lexemes { objType, ok := typeToCondObject[typ]
typ, expression, found := strings.Cut(lexeme, ":") if ok {
typ = strings.ToLower(typ) if !found {
return nil, fmt.Errorf("%w: %s", errInvalidConditionFormat, lexeme)
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)
} }
}
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 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) {