[#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

@ -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) {