From c42cf298decb400272388de4be718539aee7e3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Jerman?= Date: Mon, 27 Feb 2023 12:05:43 +0100 Subject: [PATCH] Tweak logic & add base RBAC import/export test --- server/automation/envoy/store_encode.gen.go | 4 +- server/automation/envoy/store_encode.go | 7 + server/automation/envoy/yaml_decode.gen.go | 84 +++-- server/automation/envoy/yaml_encode.gen.go | 10 +- server/automation/envoy/yaml_encode.go | 12 + .../gocode/envoy/store_encode.go.tpl | 4 +- .../templates/gocode/envoy/util_rbac.go.tpl | 18 +- .../templates/gocode/envoy/yaml_decode.go.tpl | 63 ++-- .../templates/gocode/envoy/yaml_encode.go.tpl | 10 +- server/codegen/server.envoy_rbac.cue | 4 +- server/compose/envoy/store_encode.gen.go | 4 +- server/compose/envoy/store_encode.go | 4 + server/compose/envoy/yaml_decode.gen.go | 84 +++-- server/compose/envoy/yaml_encode.gen.go | 10 +- server/compose/envoy/yaml_encode.go | 5 + server/pkg/envoyx/rbac_rules_parse.gen.go | 3 +- server/pkg/envoyx/rbacutils.go | 4 +- server/pkg/envoyx/util_rbac.gen.go | 286 +++++++++++------- server/pkg/rbac/rule.go | 14 +- server/system/envoy/store_encode.gen.go | 4 +- server/system/envoy/store_encode.go | 15 + server/system/envoy/store_encode_rbac.go | 80 +++++ server/system/envoy/yaml_decode.gen.go | 61 ++-- server/system/envoy/yaml_encode.gen.go | 10 +- server/system/envoy/yaml_encode.go | 195 ++++++++++++ server/tests/envoy/main_test.go | 1 + server/tests/envoy/rbac_rules_test.go | 273 +++++++++++++++++ server/tests/envoy/testdata/rbac/rbac.yaml | 33 ++ 28 files changed, 1048 insertions(+), 254 deletions(-) create mode 100644 server/automation/envoy/yaml_encode.go create mode 100644 server/system/envoy/store_encode_rbac.go create mode 100644 server/tests/envoy/rbac_rules_test.go create mode 100644 server/tests/envoy/testdata/rbac/rbac.yaml diff --git a/server/automation/envoy/store_encode.gen.go b/server/automation/envoy/store_encode.gen.go index 2753b81c9..0cd2fcfb2 100644 --- a/server/automation/envoy/store_encode.gen.go +++ b/server/automation/envoy/store_encode.gen.go @@ -83,9 +83,9 @@ func (e StoreEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt stri case types.TriggerResourceType: return e.encodeTriggers(ctx, p, s, nodes, tree) + default: + return e.encode(ctx, p, s, rt, nodes, tree) } - - return } // // // // // // // // // // // // // // // // // // // // // // // // // diff --git a/server/automation/envoy/store_encode.go b/server/automation/envoy/store_encode.go index 4976f5ffd..b32084e48 100644 --- a/server/automation/envoy/store_encode.go +++ b/server/automation/envoy/store_encode.go @@ -1,11 +1,18 @@ package envoy import ( + "context" "time" "github.com/cortezaproject/corteza/server/automation/types" + "github.com/cortezaproject/corteza/server/pkg/envoyx" + "github.com/cortezaproject/corteza/server/store" ) +func (e StoreEncoder) encode(ctx context.Context, p envoyx.EncodeParams, s store.Storer, rt string, nn envoyx.NodeSet, tree envoyx.Traverser) (err error) { + return +} + func (e StoreEncoder) setWorkflowDefaults(res *types.Workflow) (err error) { if res.CreatedAt.IsZero() { res.CreatedAt = time.Now() diff --git a/server/automation/envoy/yaml_decode.gen.go b/server/automation/envoy/yaml_decode.gen.go index 6064bc239..17d9adba7 100644 --- a/server/automation/envoy/yaml_decode.gen.go +++ b/server/automation/envoy/yaml_decode.gen.go @@ -86,29 +86,6 @@ func (d *auxYamlDoc) UnmarshalYAML(n *yaml.Node) (err error) { } return err - // Access control nodes - case "allow": - aux, err = unmarshalAllowNode(v) - d.nodes = append(d.nodes, aux...) - if err != nil { - return err - } - - case "deny": - aux, err = unmarshalDenyNode(v) - d.nodes = append(d.nodes, aux...) - if err != nil { - return err - } - - // Resource translation nodes - case "locale", "translation", "translations", "i18n": - aux, err = unmarshalLocaleNode(v) - d.nodes = append(d.nodes, aux...) - if err != nil { - return err - } - // Offload to custom handlers default: aux, err = d.unmarshalYAML(kv, v) @@ -741,10 +718,28 @@ func unmarshalDenyNode(n *yaml.Node) (out envoyx.NodeSet, err error) { func unmarshalRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err error) { if y7s.IsMapping(n.Content[1]) { - return unmarshalNestedRBACNode(n, acc) + out, err = unmarshalNestedRBACNode(n, acc) + if err != nil { + return + } + } else { + out, err = unmarshalFlatRBACNode(n, acc) + if err != nil { + return + } } - return unmarshalFlatRBACNode(n, acc) + for _, o := range out { + for _, r := range o.References { + if r.Scope.IsEmpty() { + continue + } + o.Scope = r.Scope + break + } + } + + return } // unmarshalNestedRBACNode handles RBAC rules when they are nested inside a resource @@ -765,25 +760,28 @@ func unmarshalRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err e func unmarshalNestedRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err error) { // Handles role return out, y7s.EachMap(n, func(role, perm *yaml.Node) error { - // Handles operation - return y7s.EachMap(perm, func(res, op *yaml.Node) error { - out = append(out, &envoyx.Node{ - Resource: &rbac.Rule{ - Resource: res.Value, - Operation: op.Value, - Access: acc, - }, - ResourceType: rbac.RuleResourceType, - References: envoyx.MergeRefs( - map[string]envoyx.Ref{"RoleID": { - // Providing resource type as plain text to reduce cross component references - ResourceType: "corteza::system:role", - Identifiers: envoyx.MakeIdentifiers(role.Value), - }}, - envoyx.SplitResourceIdentifier(res.Value), - ), + // Handles operations + return y7s.EachMap(perm, func(res, ops *yaml.Node) error { + // Handle operation (one RBAC rule per op) + return y7s.EachSeq(ops, func(op *yaml.Node) error { + out = append(out, &envoyx.Node{ + Resource: &rbac.Rule{ + Resource: res.Value, + Operation: op.Value, + Access: acc, + }, + ResourceType: rbac.RuleResourceType, + References: envoyx.MergeRefs( + map[string]envoyx.Ref{"RoleID": { + // Providing resource type as plain text to reduce cross component references + ResourceType: "corteza::system:role", + Identifiers: envoyx.MakeIdentifiers(role.Value), + }}, + envoyx.SplitResourceIdentifier(res.Value), + ), + }) + return nil }) - return nil }) }) } diff --git a/server/automation/envoy/yaml_encode.gen.go b/server/automation/envoy/yaml_encode.gen.go index 4123ba1e7..3d45288e2 100644 --- a/server/automation/envoy/yaml_encode.gen.go +++ b/server/automation/envoy/yaml_encode.gen.go @@ -73,8 +73,14 @@ func (e YamlEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt strin return } default: - // When this encoder doesn't handle any node it shouldn't write anything; - // this just removes the need for an extra check at the end. + out, err = e.encode(ctx, out, p, rt, nodes, tt) + if err != nil { + return + } + } + + // Don't output nil values since that will produce broken yaml docs + if out == nil { return } diff --git a/server/automation/envoy/yaml_encode.go b/server/automation/envoy/yaml_encode.go new file mode 100644 index 000000000..e08b20422 --- /dev/null +++ b/server/automation/envoy/yaml_encode.go @@ -0,0 +1,12 @@ +package envoy + +import ( + "context" + + "github.com/cortezaproject/corteza/server/pkg/envoyx" + "gopkg.in/yaml.v3" +) + +func (e YamlEncoder) encode(ctx context.Context, base *yaml.Node, p envoyx.EncodeParams, rt string, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) { + return +} diff --git a/server/codegen/assets/templates/gocode/envoy/store_encode.go.tpl b/server/codegen/assets/templates/gocode/envoy/store_encode.go.tpl index 51acec653..7eb9c0304 100644 --- a/server/codegen/assets/templates/gocode/envoy/store_encode.go.tpl +++ b/server/codegen/assets/templates/gocode/envoy/store_encode.go.tpl @@ -89,9 +89,9 @@ func (e StoreEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt stri case types.{{.expIdent}}ResourceType: return e.encode{{.expIdent}}s(ctx, p, s, nodes, tree) {{ end -}} + default: + return e.encode(ctx, p, s, rt, nodes, tree) } - - return } {{- range .resources }} diff --git a/server/codegen/assets/templates/gocode/envoy/util_rbac.go.tpl b/server/codegen/assets/templates/gocode/envoy/util_rbac.go.tpl index 06d361650..82ec8c068 100644 --- a/server/codegen/assets/templates/gocode/envoy/util_rbac.go.tpl +++ b/server/codegen/assets/templates/gocode/envoy/util_rbac.go.tpl @@ -36,6 +36,7 @@ func SplitResourceIdentifier(ref string) (out map[string]Ref) { case "corteza::{{$rootCmp.ident}}:{{.ident}}": + scope := Scope{} {{$res := .}} {{range $i, $p := .parents}} if gRef(pp, {{$i}}) == "" { @@ -48,9 +49,17 @@ func SplitResourceIdentifier(ref string) (out map[string]Ref) { {{ end }} {{ if eq $p.handle $cmp.ident }} - out["{{$i}}"] = Ref{ + {{ if and (eq $rootCmp.ident "compose") (eq $i 0) }} + aux := gRef(pp, {{ $i }}) + if aux != "" { + scope.ResourceType = "corteza::compose:namespace" + scope.Identifiers = MakeIdentifiers(aux) + } + {{ end }} + out["Path.{{$i}}"] = Ref{ ResourceType: "{{$cmp.fqrt}}", Identifiers: MakeIdentifiers(gRef(pp, {{ $i }})), + Scope: scope, } {{break}} {{ end }} @@ -60,9 +69,14 @@ func SplitResourceIdentifier(ref string) (out map[string]Ref) { if gRef(pp, {{len .parents}}) == "" { return } - out["{{len .parents}}"] = Ref{ + {{if eq .ident "namespace"}} + scope.ResourceType = "{{.fqrt}}" + scope.Identifiers = MakeIdentifiers(gRef(pp, {{len .parents}})) + {{end}} + out["Path.{{len .parents}}"] = Ref{ ResourceType: "{{.fqrt}}", Identifiers: MakeIdentifiers(gRef(pp, {{len .parents}})), + Scope: scope, } {{ end }} diff --git a/server/codegen/assets/templates/gocode/envoy/yaml_decode.go.tpl b/server/codegen/assets/templates/gocode/envoy/yaml_decode.go.tpl index f295ca14f..b503f3012 100644 --- a/server/codegen/assets/templates/gocode/envoy/yaml_decode.go.tpl +++ b/server/codegen/assets/templates/gocode/envoy/yaml_decode.go.tpl @@ -91,6 +91,7 @@ func (d *auxYamlDoc) UnmarshalYAML(n *yaml.Node) (err error) { return err {{ end }} + {{ if eq .componentIdent "system" }} // Access control nodes case "allow": aux, err = unmarshalAllowNode(v) @@ -113,6 +114,7 @@ func (d *auxYamlDoc) UnmarshalYAML(n *yaml.Node) (err error) { if err != nil { return err } + {{ end }} // Offload to custom handlers default: @@ -600,10 +602,28 @@ func unmarshalDenyNode(n *yaml.Node) (out envoyx.NodeSet, err error) { func unmarshalRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err error) { if y7s.IsMapping(n.Content[1]) { - return unmarshalNestedRBACNode(n, acc) + out, err = unmarshalNestedRBACNode(n, acc) + if err != nil { + return + } + } else { + out, err = unmarshalFlatRBACNode(n, acc) + if err != nil { + return + } } - return unmarshalFlatRBACNode(n, acc) + for _, o := range out { + for _, r := range o.References { + if r.Scope.IsEmpty() { + continue + } + o.Scope = r.Scope + break + } + } + + return } // unmarshalNestedRBACNode handles RBAC rules when they are nested inside a resource @@ -624,25 +644,28 @@ func unmarshalRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err e func unmarshalNestedRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err error) { // Handles role return out, y7s.EachMap(n, func(role, perm *yaml.Node) error { - // Handles operation - return y7s.EachMap(perm, func(res, op *yaml.Node) error { - out = append(out, &envoyx.Node{ - Resource: &rbac.Rule{ - Resource: res.Value, - Operation: op.Value, - Access: acc, - }, - ResourceType: rbac.RuleResourceType, - References: envoyx.MergeRefs( - map[string]envoyx.Ref{"RoleID": { - // Providing resource type as plain text to reduce cross component references - ResourceType: "corteza::system:role", - Identifiers: envoyx.MakeIdentifiers(role.Value), - }}, - envoyx.SplitResourceIdentifier(res.Value), - ), + // Handles operations + return y7s.EachMap(perm, func(res, ops *yaml.Node) error { + // Handle operation (one RBAC rule per op) + return y7s.EachSeq(ops, func(op *yaml.Node) error { + out = append(out, &envoyx.Node{ + Resource: &rbac.Rule{ + Resource: res.Value, + Operation: op.Value, + Access: acc, + }, + ResourceType: rbac.RuleResourceType, + References: envoyx.MergeRefs( + map[string]envoyx.Ref{"RoleID": { + // Providing resource type as plain text to reduce cross component references + ResourceType: "corteza::system:role", + Identifiers: envoyx.MakeIdentifiers(role.Value), + }}, + envoyx.SplitResourceIdentifier(res.Value), + ), + }) + return nil }) - return nil }) }) } diff --git a/server/codegen/assets/templates/gocode/envoy/yaml_encode.go.tpl b/server/codegen/assets/templates/gocode/envoy/yaml_encode.go.tpl index 139cd0af0..887849fcb 100644 --- a/server/codegen/assets/templates/gocode/envoy/yaml_encode.go.tpl +++ b/server/codegen/assets/templates/gocode/envoy/yaml_encode.go.tpl @@ -67,8 +67,14 @@ func (e YamlEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt strin } {{ end -}} default: - // When this encoder doesn't handle any node it shouldn't write anything; - // this just removes the need for an extra check at the end. + out, err = e.encode(ctx, out, p, rt, nodes, tt) + if err != nil { + return + } + } + + // Don't output nil values since that will produce broken yaml docs + if out == nil { return } diff --git a/server/codegen/server.envoy_rbac.cue b/server/codegen/server.envoy_rbac.cue index 7a06ce0c6..0c8ef71d8 100644 --- a/server/codegen/server.envoy_rbac.cue +++ b/server/codegen/server.envoy_rbac.cue @@ -32,9 +32,9 @@ import ( [ { template: "gocode/envoy/rbac_rules_parse.go.tpl" - output: "pkg/envoy/resource/rbac_rules_parse.gen.go" + output: "pkg/envoyx/rbac_rules_parse.gen.go" payload: { - package: "resource" + package: "envoyx" imports: [ for cmp in app.corteza.components { "\(cmp.ident)Types \"github.com/cortezaproject/corteza/server/\(cmp.ident)/types\"" diff --git a/server/compose/envoy/store_encode.gen.go b/server/compose/envoy/store_encode.gen.go index 526881dad..0e21b7c86 100644 --- a/server/compose/envoy/store_encode.gen.go +++ b/server/compose/envoy/store_encode.gen.go @@ -98,9 +98,9 @@ func (e StoreEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt stri case types.PageResourceType: return e.encodePages(ctx, p, s, nodes, tree) + default: + return e.encode(ctx, p, s, rt, nodes, tree) } - - return } // // // // // // // // // // // // // // // // // // // // // // // // // diff --git a/server/compose/envoy/store_encode.go b/server/compose/envoy/store_encode.go index e82b81f4a..f634aff65 100644 --- a/server/compose/envoy/store_encode.go +++ b/server/compose/envoy/store_encode.go @@ -12,6 +12,10 @@ import ( "github.com/cortezaproject/corteza/server/store" ) +func (e StoreEncoder) encode(ctx context.Context, p envoyx.EncodeParams, s store.Storer, rt string, nn envoyx.NodeSet, tree envoyx.Traverser) (err error) { + return +} + func (e StoreEncoder) setChartDefaults(res *types.Chart) (err error) { if res.CreatedAt.IsZero() { res.CreatedAt = time.Now() diff --git a/server/compose/envoy/yaml_decode.gen.go b/server/compose/envoy/yaml_decode.gen.go index 5caa2cbd6..cd0a4e36a 100644 --- a/server/compose/envoy/yaml_decode.gen.go +++ b/server/compose/envoy/yaml_decode.gen.go @@ -127,29 +127,6 @@ func (d *auxYamlDoc) UnmarshalYAML(n *yaml.Node) (err error) { } return err - // Access control nodes - case "allow": - aux, err = unmarshalAllowNode(v) - d.nodes = append(d.nodes, aux...) - if err != nil { - return err - } - - case "deny": - aux, err = unmarshalDenyNode(v) - d.nodes = append(d.nodes, aux...) - if err != nil { - return err - } - - // Resource translation nodes - case "locale", "translation", "translations", "i18n": - aux, err = unmarshalLocaleNode(v) - d.nodes = append(d.nodes, aux...) - if err != nil { - return err - } - // Offload to custom handlers default: aux, err = d.unmarshalYAML(kv, v) @@ -1803,10 +1780,28 @@ func unmarshalDenyNode(n *yaml.Node) (out envoyx.NodeSet, err error) { func unmarshalRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err error) { if y7s.IsMapping(n.Content[1]) { - return unmarshalNestedRBACNode(n, acc) + out, err = unmarshalNestedRBACNode(n, acc) + if err != nil { + return + } + } else { + out, err = unmarshalFlatRBACNode(n, acc) + if err != nil { + return + } } - return unmarshalFlatRBACNode(n, acc) + for _, o := range out { + for _, r := range o.References { + if r.Scope.IsEmpty() { + continue + } + o.Scope = r.Scope + break + } + } + + return } // unmarshalNestedRBACNode handles RBAC rules when they are nested inside a resource @@ -1827,25 +1822,28 @@ func unmarshalRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err e func unmarshalNestedRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err error) { // Handles role return out, y7s.EachMap(n, func(role, perm *yaml.Node) error { - // Handles operation - return y7s.EachMap(perm, func(res, op *yaml.Node) error { - out = append(out, &envoyx.Node{ - Resource: &rbac.Rule{ - Resource: res.Value, - Operation: op.Value, - Access: acc, - }, - ResourceType: rbac.RuleResourceType, - References: envoyx.MergeRefs( - map[string]envoyx.Ref{"RoleID": { - // Providing resource type as plain text to reduce cross component references - ResourceType: "corteza::system:role", - Identifiers: envoyx.MakeIdentifiers(role.Value), - }}, - envoyx.SplitResourceIdentifier(res.Value), - ), + // Handles operations + return y7s.EachMap(perm, func(res, ops *yaml.Node) error { + // Handle operation (one RBAC rule per op) + return y7s.EachSeq(ops, func(op *yaml.Node) error { + out = append(out, &envoyx.Node{ + Resource: &rbac.Rule{ + Resource: res.Value, + Operation: op.Value, + Access: acc, + }, + ResourceType: rbac.RuleResourceType, + References: envoyx.MergeRefs( + map[string]envoyx.Ref{"RoleID": { + // Providing resource type as plain text to reduce cross component references + ResourceType: "corteza::system:role", + Identifiers: envoyx.MakeIdentifiers(role.Value), + }}, + envoyx.SplitResourceIdentifier(res.Value), + ), + }) + return nil }) - return nil }) }) } diff --git a/server/compose/envoy/yaml_encode.gen.go b/server/compose/envoy/yaml_encode.gen.go index 6d444e486..a029056ee 100644 --- a/server/compose/envoy/yaml_encode.gen.go +++ b/server/compose/envoy/yaml_encode.gen.go @@ -103,8 +103,14 @@ func (e YamlEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt strin } default: - // When this encoder doesn't handle any node it shouldn't write anything; - // this just removes the need for an extra check at the end. + out, err = e.encode(ctx, out, p, rt, nodes, tt) + if err != nil { + return + } + } + + // Don't output nil values since that will produce broken yaml docs + if out == nil { return } diff --git a/server/compose/envoy/yaml_encode.go b/server/compose/envoy/yaml_encode.go index 5f94d74ed..789082ea9 100644 --- a/server/compose/envoy/yaml_encode.go +++ b/server/compose/envoy/yaml_encode.go @@ -7,8 +7,13 @@ import ( "github.com/cortezaproject/corteza/server/compose/types" "github.com/cortezaproject/corteza/server/pkg/envoyx" "github.com/cortezaproject/corteza/server/pkg/y7s" + "gopkg.in/yaml.v3" ) +func (e YamlEncoder) encode(ctx context.Context, base *yaml.Node, p envoyx.EncodeParams, rt string, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) { + return +} + func (e YamlEncoder) encodeChartConfigC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, chart *types.Chart, cfg types.ChartConfig) (_ any, err error) { reports, _ := y7s.MakeSeq() diff --git a/server/pkg/envoyx/rbac_rules_parse.gen.go b/server/pkg/envoyx/rbac_rules_parse.gen.go index 23afeab53..6d8ff1b50 100644 --- a/server/pkg/envoyx/rbac_rules_parse.gen.go +++ b/server/pkg/envoyx/rbac_rules_parse.gen.go @@ -8,12 +8,11 @@ package envoyx import ( "fmt" - "strings" - automationTypes "github.com/cortezaproject/corteza/server/automation/types" composeTypes "github.com/cortezaproject/corteza/server/compose/types" federationTypes "github.com/cortezaproject/corteza/server/federation/types" systemTypes "github.com/cortezaproject/corteza/server/system/types" + "strings" ) // Parse generates resource setting logic for each resource diff --git a/server/pkg/envoyx/rbacutils.go b/server/pkg/envoyx/rbacutils.go index 77ddd83bf..cebc0a2b0 100644 --- a/server/pkg/envoyx/rbacutils.go +++ b/server/pkg/envoyx/rbacutils.go @@ -69,7 +69,7 @@ func RBACRulesForNodes(rr rbac.RuleSet, nn ...*Node) (rules NodeSet, err error) ref.Scope = n.Scope // @todo make the thing not a pointer - rf[fmt.Sprintf("Resource.%d", i)] = *ref + rf[fmt.Sprintf("Path.%d", i)] = *ref } // Ref to the rule @@ -93,7 +93,7 @@ func RBACRulesForNodes(rr rbac.RuleSet, nn ...*Node) (rules NodeSet, err error) if dups[r.RoleID][r.Resource] == nil { dups[r.RoleID][r.Resource] = make(map[string]bool) } - dups[r.RoleID][r.Resource][r.Resource] = true + dups[r.RoleID][r.Resource][r.Operation] = true } } diff --git a/server/pkg/envoyx/util_rbac.gen.go b/server/pkg/envoyx/util_rbac.gen.go index a4edd8c8d..942b6831c 100644 --- a/server/pkg/envoyx/util_rbac.gen.go +++ b/server/pkg/envoyx/util_rbac.gen.go @@ -30,468 +30,548 @@ func SplitResourceIdentifier(ref string) (out map[string]Ref) { switch rt { case "corteza::system:apigwFilter": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:apigw-filter", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:apigwRoute": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:apigw-route", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:application": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:application", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:attachment": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:attachment", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:authClient": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:auth-client", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:authConfirmedClient": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:auth-confirmed-client", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:authOa2token": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:auth-oa2token", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:authSession": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:auth-session", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:credential": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:credential", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:dalConnection": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:dal-connection", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:dalSensitivityLevel": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:dal-sensitivity-level", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:dataPrivacyRequest": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:data-privacy-request", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:dataPrivacyRequestComment": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:data-privacy-request-comment", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:queue": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:queue", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:queueMessage": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:queue-message", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:reminder": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:reminder", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:report": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:report", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:resourceTranslation": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:resource-translation", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:role": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:role", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:roleMember": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:role-member", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:settingValue": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:settings", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:template": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:template", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::system:user": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::system:user", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::compose:attachment": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::compose:attachment", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::compose:chart": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + aux := gRef(pp, 0) + if aux != "" { + scope.ResourceType = "corteza::compose:namespace" + scope.Identifiers = MakeIdentifiers(aux) + } + + out["Path.0"] = Ref{ ResourceType: "corteza::compose:namespace", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } if gRef(pp, 1) == "" { return } - out["1"] = Ref{ + + out["Path.1"] = Ref{ ResourceType: "corteza::compose:chart", Identifiers: MakeIdentifiers(gRef(pp, 1)), + Scope: scope, } case "corteza::compose:module": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + aux := gRef(pp, 0) + if aux != "" { + scope.ResourceType = "corteza::compose:namespace" + scope.Identifiers = MakeIdentifiers(aux) + } + + out["Path.0"] = Ref{ ResourceType: "corteza::compose:namespace", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } if gRef(pp, 1) == "" { return } - out["1"] = Ref{ + + out["Path.1"] = Ref{ ResourceType: "corteza::compose:module", Identifiers: MakeIdentifiers(gRef(pp, 1)), + Scope: scope, } case "corteza::compose:moduleField": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + aux := gRef(pp, 0) + if aux != "" { + scope.ResourceType = "corteza::compose:namespace" + scope.Identifiers = MakeIdentifiers(aux) + } + + out["Path.0"] = Ref{ ResourceType: "corteza::compose:namespace", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } if gRef(pp, 1) == "" { return } - out["1"] = Ref{ + out["Path.1"] = Ref{ ResourceType: "corteza::compose:module", Identifiers: MakeIdentifiers(gRef(pp, 1)), + Scope: scope, } if gRef(pp, 2) == "" { return } - out["2"] = Ref{ + + out["Path.2"] = Ref{ ResourceType: "corteza::compose:module-field", Identifiers: MakeIdentifiers(gRef(pp, 2)), + Scope: scope, } case "corteza::compose:namespace": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + scope.ResourceType = "corteza::compose:namespace" + scope.Identifiers = MakeIdentifiers(gRef(pp, 0)) + + out["Path.0"] = Ref{ ResourceType: "corteza::compose:namespace", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::compose:page": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + aux := gRef(pp, 0) + if aux != "" { + scope.ResourceType = "corteza::compose:namespace" + scope.Identifiers = MakeIdentifiers(aux) + } + + out["Path.0"] = Ref{ ResourceType: "corteza::compose:namespace", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } if gRef(pp, 1) == "" { return } - out["1"] = Ref{ + + out["Path.1"] = Ref{ ResourceType: "corteza::compose:page", Identifiers: MakeIdentifiers(gRef(pp, 1)), + Scope: scope, } case "corteza::compose:record": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + aux := gRef(pp, 0) + if aux != "" { + scope.ResourceType = "corteza::compose:namespace" + scope.Identifiers = MakeIdentifiers(aux) + } + + out["Path.0"] = Ref{ ResourceType: "corteza::compose:namespace", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } if gRef(pp, 1) == "" { return } - out["1"] = Ref{ + out["Path.1"] = Ref{ ResourceType: "corteza::compose:module", Identifiers: MakeIdentifiers(gRef(pp, 1)), + Scope: scope, } if gRef(pp, 2) == "" { return } - out["2"] = Ref{ + + out["Path.2"] = Ref{ ResourceType: "corteza::compose:record", Identifiers: MakeIdentifiers(gRef(pp, 2)), + Scope: scope, } case "corteza::compose:recordRevision": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::compose:record-revision", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::automation:session": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::automation:session", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::automation:trigger": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::automation:trigger", Identifiers: MakeIdentifiers(gRef(pp, 0)), + Scope: scope, } case "corteza::automation:workflow": + scope := Scope{} if gRef(pp, 0) == "" { return } - out["0"] = Ref{ + + out["Path.0"] = Ref{ ResourceType: "corteza::automation:workflow", Identifiers: MakeIdentifiers(gRef(pp, 0)), - } - - case "corteza::federation:exposedModule": - - if gRef(pp, 0) == "" { - return - } - - if gRef(pp, 1) == "" { - return - } - out["1"] = Ref{ - ResourceType: "corteza::federation:exposed-module", - Identifiers: MakeIdentifiers(gRef(pp, 1)), - } - - case "corteza::federation:moduleMapping": - - if gRef(pp, 0) == "" { - return - } - - if gRef(pp, 1) == "" { - return - } - out["1"] = Ref{ - ResourceType: "corteza::federation:module-mapping", - Identifiers: MakeIdentifiers(gRef(pp, 1)), - } - - case "corteza::federation:node": - - if gRef(pp, 0) == "" { - return - } - out["0"] = Ref{ - ResourceType: "corteza::federation:node", - Identifiers: MakeIdentifiers(gRef(pp, 0)), - } - - case "corteza::federation:nodeSync": - - if gRef(pp, 0) == "" { - return - } - out["0"] = Ref{ - ResourceType: "corteza::federation:node-sync", - Identifiers: MakeIdentifiers(gRef(pp, 0)), - } - - case "corteza::federation:sharedModule": - - if gRef(pp, 0) == "" { - return - } - - if gRef(pp, 1) == "" { - return - } - out["1"] = Ref{ - ResourceType: "corteza::federation:shared-module", - Identifiers: MakeIdentifiers(gRef(pp, 1)), + Scope: scope, } } diff --git a/server/pkg/rbac/rule.go b/server/pkg/rbac/rule.go index ee317b100..565179bf1 100644 --- a/server/pkg/rbac/rule.go +++ b/server/pkg/rbac/rule.go @@ -64,6 +64,18 @@ func (set RuleSet) FilterAccess(a Access) (out RuleSet) { return out } +func (set RuleSet) FilterOperation(op string) (out RuleSet) { + out = make(RuleSet, 0, len(set)) + + for _, s := range set { + if s.Operation == op { + out = append(out, s) + } + } + + return out +} + // FilterResource returns rules that match given list of resources // Wildcards are not used! // @@ -117,7 +129,7 @@ func (u *Rule) setValue(name string, pos uint, v any) (err error) { pp := strings.Split(name, ".") switch pp[0] { - case "resource", "Resource": + case "resource", "Resource", "Path", "path": ix, err := strconv.ParseUint(pp[1], 10, 64) if err != nil { return err diff --git a/server/system/envoy/store_encode.gen.go b/server/system/envoy/store_encode.gen.go index 5314732eb..9ba7f294f 100644 --- a/server/system/envoy/store_encode.gen.go +++ b/server/system/envoy/store_encode.gen.go @@ -131,9 +131,9 @@ func (e StoreEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt stri case types.DalSensitivityLevelResourceType: return e.encodeDalSensitivityLevels(ctx, p, s, nodes, tree) + default: + return e.encode(ctx, p, s, rt, nodes, tree) } - - return } // // // // // // // // // // // // // // // // // // // // // // // // // diff --git a/server/system/envoy/store_encode.go b/server/system/envoy/store_encode.go index 0e24d012e..93186cc67 100644 --- a/server/system/envoy/store_encode.go +++ b/server/system/envoy/store_encode.go @@ -5,11 +5,26 @@ import ( "time" "github.com/cortezaproject/corteza/server/pkg/envoyx" + "github.com/cortezaproject/corteza/server/pkg/rbac" "github.com/cortezaproject/corteza/server/store" "github.com/cortezaproject/corteza/server/system/types" ) func (e StoreEncoder) prepare(ctx context.Context, p envoyx.EncodeParams, s store.Storer, rt string, nn envoyx.NodeSet) (err error) { + switch rt { + case rbac.RuleResourceType: + return e.prepareRbacRule(ctx, p, s, nn) + } + + return +} + +func (e StoreEncoder) encode(ctx context.Context, p envoyx.EncodeParams, s store.Storer, rt string, nn envoyx.NodeSet, tree envoyx.Traverser) (err error) { + switch rt { + case rbac.RuleResourceType: + return e.encodeRbacRules(ctx, p, s, nn, tree) + } + return } diff --git a/server/system/envoy/store_encode_rbac.go b/server/system/envoy/store_encode_rbac.go new file mode 100644 index 000000000..0f37f7722 --- /dev/null +++ b/server/system/envoy/store_encode_rbac.go @@ -0,0 +1,80 @@ +package envoy + +import ( + "context" + "fmt" + + "github.com/cortezaproject/corteza/server/pkg/envoyx" + "github.com/cortezaproject/corteza/server/pkg/rbac" + "github.com/cortezaproject/corteza/server/store" +) + +func (e StoreEncoder) prepareRbacRule(ctx context.Context, p envoyx.EncodeParams, s store.Storer, nn envoyx.NodeSet) (err error) { + // @todo existing RBAC rules? + for _, n := range nn { + if n.Resource == nil { + panic("unexpected state: cannot call prepareRbacRule with nodes without a defined Resource") + } + + res, ok := n.Resource.(*rbac.Rule) + if !ok { + panic("unexpected resource type: node expecting type of RbacRule") + } + + // Run expressions on the nodes + err = e.runEvals(ctx, false, n) + if err != nil { + return + } + + // @todo merge conflicts if we do existing assertion + + n.Resource = res + } + + return +} + +// encodeRbacRules encodes a set of resource into the database +func (e StoreEncoder) encodeRbacRules(ctx context.Context, p envoyx.EncodeParams, s store.Storer, nn envoyx.NodeSet, tree envoyx.Traverser) (err error) { + for _, n := range nn { + err = e.encodeRbacRule(ctx, p, s, n, tree) + if err != nil { + return + } + } + + return +} + +// encodeRbacRule encodes the resource into the database +func (e StoreEncoder) encodeRbacRule(ctx context.Context, p envoyx.EncodeParams, s store.Storer, n *envoyx.Node, tree envoyx.Traverser) (err error) { + // Grab dependency references + var auxID uint64 + for fieldLabel, ref := range n.References { + rn := tree.ParentForRef(n, ref) + if rn == nil { + err = fmt.Errorf("missing node for ref %v", ref) + return + } + + auxID = rn.Resource.GetID() + if auxID == 0 { + err = fmt.Errorf("related resource doesn't provide an ID") + return + } + + err = n.Resource.SetValue(fieldLabel, 0, auxID) + if err != nil { + return + } + } + + // Flush to the DB + err = store.UpsertRbacRule(ctx, s, n.Resource.(*rbac.Rule)) + if err != nil { + return + } + + return +} diff --git a/server/system/envoy/yaml_decode.gen.go b/server/system/envoy/yaml_decode.gen.go index 2472ad73b..332063426 100644 --- a/server/system/envoy/yaml_decode.gen.go +++ b/server/system/envoy/yaml_decode.gen.go @@ -3207,10 +3207,28 @@ func unmarshalDenyNode(n *yaml.Node) (out envoyx.NodeSet, err error) { func unmarshalRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err error) { if y7s.IsMapping(n.Content[1]) { - return unmarshalNestedRBACNode(n, acc) + out, err = unmarshalNestedRBACNode(n, acc) + if err != nil { + return + } + } else { + out, err = unmarshalFlatRBACNode(n, acc) + if err != nil { + return + } } - return unmarshalFlatRBACNode(n, acc) + for _, o := range out { + for _, r := range o.References { + if r.Scope.IsEmpty() { + continue + } + o.Scope = r.Scope + break + } + } + + return } // unmarshalNestedRBACNode handles RBAC rules when they are nested inside a resource @@ -3231,25 +3249,28 @@ func unmarshalRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err e func unmarshalNestedRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err error) { // Handles role return out, y7s.EachMap(n, func(role, perm *yaml.Node) error { - // Handles operation - return y7s.EachMap(perm, func(res, op *yaml.Node) error { - out = append(out, &envoyx.Node{ - Resource: &rbac.Rule{ - Resource: res.Value, - Operation: op.Value, - Access: acc, - }, - ResourceType: rbac.RuleResourceType, - References: envoyx.MergeRefs( - map[string]envoyx.Ref{"RoleID": { - // Providing resource type as plain text to reduce cross component references - ResourceType: "corteza::system:role", - Identifiers: envoyx.MakeIdentifiers(role.Value), - }}, - envoyx.SplitResourceIdentifier(res.Value), - ), + // Handles operations + return y7s.EachMap(perm, func(res, ops *yaml.Node) error { + // Handle operation (one RBAC rule per op) + return y7s.EachSeq(ops, func(op *yaml.Node) error { + out = append(out, &envoyx.Node{ + Resource: &rbac.Rule{ + Resource: res.Value, + Operation: op.Value, + Access: acc, + }, + ResourceType: rbac.RuleResourceType, + References: envoyx.MergeRefs( + map[string]envoyx.Ref{"RoleID": { + // Providing resource type as plain text to reduce cross component references + ResourceType: "corteza::system:role", + Identifiers: envoyx.MakeIdentifiers(role.Value), + }}, + envoyx.SplitResourceIdentifier(res.Value), + ), + }) + return nil }) - return nil }) }) } diff --git a/server/system/envoy/yaml_encode.gen.go b/server/system/envoy/yaml_encode.gen.go index a8913434d..d825363d0 100644 --- a/server/system/envoy/yaml_encode.gen.go +++ b/server/system/envoy/yaml_encode.gen.go @@ -157,8 +157,14 @@ func (e YamlEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt strin return } default: - // When this encoder doesn't handle any node it shouldn't write anything; - // this just removes the need for an extra check at the end. + out, err = e.encode(ctx, out, p, rt, nodes, tt) + if err != nil { + return + } + } + + // Don't output nil values since that will produce broken yaml docs + if out == nil { return } diff --git a/server/system/envoy/yaml_encode.go b/server/system/envoy/yaml_encode.go index 6916e5895..19ca437b5 100644 --- a/server/system/envoy/yaml_encode.go +++ b/server/system/envoy/yaml_encode.go @@ -3,13 +3,208 @@ package envoy import ( "context" "fmt" + "strings" "github.com/cortezaproject/corteza/server/pkg/envoyx" + "github.com/cortezaproject/corteza/server/pkg/rbac" "github.com/cortezaproject/corteza/server/pkg/y7s" "github.com/cortezaproject/corteza/server/system/types" "gopkg.in/yaml.v3" ) +type ( + // Wrapper to avoid constant type casting for role nodes + rbacRuleWrap struct { + node *envoyx.Node + rule *rbac.Rule + } + + // Wrapper to keep role and rules together + rbacRuleRoleWrap struct { + rules []rbacRuleWrap + roleNode *envoyx.Node + role *types.Role + } +) + +func (e YamlEncoder) encode(ctx context.Context, base *yaml.Node, p envoyx.EncodeParams, rt string, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) { + switch rt { + case rbac.RuleResourceType: + return e.encodeRbacRules(ctx, base, p, nodes, tt) + } + + return +} + +func (e YamlEncoder) encodeRbacRules(ctx context.Context, base *yaml.Node, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) { + err = e.resolveRulePathDeps(ctx, tt, nodes) + if err != nil { + return + } + + // Batch by access (allow, deny) + allows := make([]rbacRuleWrap, 0, len(nodes)) + denies := make([]rbacRuleWrap, 0, len(nodes)) + for _, n := range nodes { + r := n.Resource.(*rbac.Rule) + if r.Access == rbac.Allow { + allows = append(allows, rbacRuleWrap{ + node: n, + rule: r, + }) + } else { + denies = append(denies, rbacRuleWrap{ + node: n, + rule: r, + }) + } + } + + // Encode allows + var aux *yaml.Node + aux, err = e.encodeRbacRulesByRole(p, allows, tt) + if err != nil { + return + } + out = base + out, err = y7s.AddMap(out, "allow", aux) + if err != nil { + return + } + + // Encode denies + aux, err = e.encodeRbacRulesByRole(p, denies, tt) + if err != nil { + return + } + out, err = y7s.AddMap(out, "deny", aux) + if err != nil { + return + } + + return +} + +func (e YamlEncoder) encodeRbacRulesByRole(p envoyx.EncodeParams, rules []rbacRuleWrap, tt envoyx.Traverser) (out *yaml.Node, err error) { + // Batch by role; make sure to keep the multiple identifier thing in mind + byRole := make(map[string]rbacRuleRoleWrap) + for _, r := range rules { + role := r.node.References["RoleID"] + aux := rbacRuleRoleWrap{} + ok := false + + // Check if an identifier already exists + for _, ident := range role.Identifiers.Slice { + aux, ok = byRole[ident] + if ok { + break + } + } + + // If not, create a new entry and resolve the role + if !ok { + aux.roleNode = tt.ParentForRef(r.node, role) + if aux.roleNode != nil { + aux.role = aux.roleNode.Resource.(*types.Role) + } + } + + // Add the rule to the batch and update potentially missing identifiers + aux.rules = append(aux.rules, r) + for _, ident := range role.Identifiers.Slice { + byRole[ident] = aux + } + } + + // Encode + var aux *yaml.Node + for _, r := range byRole { + aux, err = e.encodeRbacRulesByResource(p, r.rules) + if err != nil { + return + } + + out, err = y7s.AddMap(out, + r.roleNode.Identifiers.FriendlyIdentifier(), aux, + ) + if err != nil { + return + } + } + + return +} + +func (e YamlEncoder) encodeRbacRulesByResource(p envoyx.EncodeParams, rules []rbacRuleWrap) (out *yaml.Node, err error) { + byResource := make(map[string][]rbacRuleWrap) + + // Batch + for _, r := range rules { + byResource[r.rule.Resource] = append(byResource[r.rule.Resource], r) + } + + // Encode + var aux *yaml.Node + for resource, rules := range byResource { + aux, err = e.encodeRbacRuleOperations(p, rules) + if err != nil { + return + } + + out, err = y7s.AddMap(out, resource, aux) + if err != nil { + return + } + } + + return +} + +func (e YamlEncoder) encodeRbacRuleOperations(p envoyx.EncodeParams, rules []rbacRuleWrap) (out *yaml.Node, err error) { + ops, _ := y7s.MakeSeq() + + for _, r := range rules { + ops, err = y7s.AddSeq(ops, r.rule.Operation) + if err != nil { + return + } + } + + return ops, nil +} + +func (e YamlEncoder) resolveRulePathDeps(ctx context.Context, tt envoyx.Traverser, nodes envoyx.NodeSet) (err error) { + for _, n := range nodes { + + var auxIdent string + for fieldLabel, ref := range n.References { + // Only resolve path refs; others are done later on + if !strings.Contains(fieldLabel, "Path.") { + continue + } + + rn := tt.ParentForRef(n, ref) + if rn == nil { + err = fmt.Errorf("missing node for ref %v", ref) + return + } + + auxIdent = rn.Identifiers.FriendlyIdentifier() + if auxIdent == "" || auxIdent == "0" { + err = fmt.Errorf("related resource doesn't provide an ID") + return + } + + err = n.Resource.SetValue(fieldLabel, 0, auxIdent) + if err != nil { + return + } + } + } + + return +} + func (e YamlEncoder) encodeAuthClientSecurityC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, ac *types.AuthClient, sec *types.AuthClientSecurity) (_ any, err error) { sqPermittedRoles, err := e.encodeRoleSlice(n, tt, "Security.PermittedRoles", sec.PermittedRoles) if err != nil { diff --git a/server/tests/envoy/main_test.go b/server/tests/envoy/main_test.go index 52a46f2a6..652a10f00 100644 --- a/server/tests/envoy/main_test.go +++ b/server/tests/envoy/main_test.go @@ -125,6 +125,7 @@ func cleanup(t *testing.T) { store.TruncateUsers(ctx, defaultStore), store.TruncateDalConnections(ctx, defaultStore), store.TruncateDalSensitivityLevels(ctx, defaultStore), + store.TruncateRbacRules(ctx, defaultStore), store.TruncateAutomationWorkflows(ctx, defaultStore), store.TruncateAutomationTriggers(ctx, defaultStore), diff --git a/server/tests/envoy/rbac_rules_test.go b/server/tests/envoy/rbac_rules_test.go new file mode 100644 index 000000000..3cb0c945e --- /dev/null +++ b/server/tests/envoy/rbac_rules_test.go @@ -0,0 +1,273 @@ +package envoy + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/cortezaproject/corteza/server/compose/types" + "github.com/cortezaproject/corteza/server/pkg/envoyx" + "github.com/cortezaproject/corteza/server/pkg/rbac" + "github.com/cortezaproject/corteza/server/store" + systemTypes "github.com/cortezaproject/corteza/server/system/types" + "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/require" +) + +func TestRbacImportExport(t *testing.T) { + var ( + ctx = context.Background() + req = require.New(t) + nodes envoyx.NodeSet + gg *envoyx.DepGraph + err error + ) + + cleanup(t) + + // The test + // + // * imports some YAML files + // * checks the DB state + // * exports the DB into a YAML + // * clears the DB + // * imports the YAML + // * checks the DB state the same way as before + // + // The above outlined flow allows us to trivially check if the data is both + // imported and exported correctly. + // + // The initial step could also manually populate the DB but the YAML import + // is more convenient. + + t.Run("initial import", func(t *testing.T) { + t.Run("parse configs", func(t *testing.T) { + nodes, err = defaultEnvoy.Decode(ctx, envoyx.DecodeParams{ + Type: envoyx.DecodeTypeURI, + Params: map[string]any{ + "uri": "file://testdata/rbac", + }, + }) + req.NoError(err) + }) + + t.Run("bake", func(t *testing.T) { + gg, err = defaultEnvoy.Bake(ctx, envoyx.EncodeParams{ + Type: envoyx.EncodeTypeStore, + Params: map[string]any{ + "storer": defaultStore, + "dal": defaultDal, + }, + }, nodes...) + req.NoError(err) + }) + + t.Run("import into DB", func(t *testing.T) { + err = defaultEnvoy.Encode(ctx, envoyx.EncodeParams{ + Type: envoyx.EncodeTypeStore, + Params: map[string]any{ + "storer": defaultStore, + "dal": defaultDal, + }, + }, gg) + req.NoError(err) + }) + + assertRBACState(ctx, t, defaultStore, req) + }) + + // Prepare a temp file where we'll dump the YAML into + auxFile, err := os.CreateTemp(os.TempDir(), "*.yaml") + req.NoError(err) + spew.Dump(auxFile.Name()) + // defer os.Remove(auxFile.Name()) + defer auxFile.Close() + + var rules envoyx.NodeSet + t.Run("export", func(t *testing.T) { + t.Run("export from DB", func(t *testing.T) { + nodes, err = defaultEnvoy.Decode(ctx, envoyx.DecodeParams{ + Type: envoyx.DecodeTypeStore, + Params: map[string]any{ + "storer": defaultStore, + }, + Filter: map[string]envoyx.ResourceFilter{ + types.ModuleResourceType: {}, + types.NamespaceResourceType: {}, + + systemTypes.RoleResourceType: {}, + }, + }) + req.NoError(err) + }) + + var rr rbac.RuleSet + t.Run("get all rules", func(t *testing.T) { + rr, _, err = store.SearchRbacRules(ctx, defaultStore, rbac.RuleFilter{}) + req.NoError(err) + }) + + t.Run("connect rules to resources", func(t *testing.T) { + rules, err = envoyx.RBACRulesForNodes(rr, nodes...) + req.NoError(err) + + nodes = append(nodes, rules...) + }) + + t.Run("bake", func(t *testing.T) { + gg, err = defaultEnvoy.Bake(ctx, envoyx.EncodeParams{ + Type: envoyx.EncodeTypeStore, + Params: map[string]any{ + "storer": defaultStore, + "dal": defaultDal, + }, + }, nodes...) + req.NoError(err) + }) + + t.Run("write file", func(t *testing.T) { + err = defaultEnvoy.Encode(ctx, envoyx.EncodeParams{ + Type: envoyx.EncodeTypeIo, + Params: map[string]any{ + "writer": auxFile, + }, + }, gg) + req.NoError(err) + }) + }) + + cleanup(t) + + t.Run("second import", func(t *testing.T) { + t.Run("yaml parse", func(t *testing.T) { + nodes, err = defaultEnvoy.Decode(ctx, envoyx.DecodeParams{ + Type: envoyx.DecodeTypeURI, + Params: map[string]any{ + "uri": fmt.Sprintf("file://%s", auxFile.Name()), + }, + }) + req.NoError(err) + }) + + t.Run("bake", func(t *testing.T) { + gg, err = defaultEnvoy.Bake(ctx, envoyx.EncodeParams{ + Type: envoyx.EncodeTypeStore, + Params: map[string]any{ + "storer": defaultStore, + "dal": defaultDal, + }, + }, nodes...) + req.NoError(err) + }) + + t.Run("run import", func(t *testing.T) { + err = defaultEnvoy.Encode(ctx, envoyx.EncodeParams{ + Type: envoyx.EncodeTypeStore, + Params: map[string]any{ + "storer": defaultStore, + "dal": defaultDal, + }, + }, gg) + req.NoError(err) + }) + + assertRBACState(ctx, t, defaultStore, req) + }) +} + +func assertRBACState(ctx context.Context, t *testing.T, s store.Storer, req *require.Assertions) { + t.Run("check state", func(t *testing.T) { + namespaces, _, err := store.SearchComposeNamespaces(ctx, defaultStore, types.NamespaceFilter{}) + req.NoError(err) + ns1 := namespaces[0] + + modules, _, err := store.SearchComposeModules(ctx, defaultStore, types.ModuleFilter{}) + req.NoError(err) + mod1 := modules[0] + + roles, _, err := store.SearchRoles(ctx, defaultStore, systemTypes.RoleFilter{}) + req.NoError(err) + role1 := roles.FindByHandle("test_role_1") + role2 := roles.FindByHandle("test_role_2") + + rr, _, err := store.SearchRbacRules(ctx, defaultStore, rbac.RuleFilter{}) + req.NoError(err) + + allows := rr.FilterAccess(rbac.Allow) + compareRules(req, *allows.FilterOperation("op1")[0], rbac.Rule{ + RoleID: role1.ID, + Resource: "corteza::compose:namespace/*", + Operation: "op1", + Access: rbac.Allow, + }) + + compareRules(req, *allows.FilterOperation("op2")[0], rbac.Rule{ + RoleID: role1.ID, + Resource: fmt.Sprintf("corteza::compose:namespace/%d", ns1.ID), + Operation: "op2", + Access: rbac.Allow, + }) + + compareRules(req, *allows.FilterOperation("op3")[0], rbac.Rule{ + RoleID: role1.ID, + Resource: fmt.Sprintf("corteza::compose:module/%d/*", ns1.ID), + Operation: "op3", + Access: rbac.Allow, + }) + + compareRules(req, *allows.FilterOperation("op4")[0], rbac.Rule{ + RoleID: role1.ID, + Resource: fmt.Sprintf("corteza::compose:module/%d/%d", ns1.ID, mod1.ID), + Operation: "op4", + Access: rbac.Allow, + }) + + denies := rr.FilterAccess(rbac.Deny) + compareRules(req, *denies.FilterOperation("op1")[0], rbac.Rule{ + RoleID: role2.ID, + Resource: "corteza::compose:namespace/*", + Operation: "op1", + Access: rbac.Deny, + }) + + compareRules(req, *denies.FilterOperation("op2")[0], rbac.Rule{ + RoleID: role2.ID, + Resource: fmt.Sprintf("corteza::compose:namespace/%d", ns1.ID), + Operation: "op2", + Access: rbac.Deny, + }) + + compareRules(req, *denies.FilterOperation("op3")[0], rbac.Rule{ + RoleID: role2.ID, + Resource: fmt.Sprintf("corteza::compose:module/%d/*", ns1.ID), + Operation: "op3", + Access: rbac.Deny, + }) + + compareRules(req, *denies.FilterOperation("op4")[0], rbac.Rule{ + RoleID: role2.ID, + Resource: fmt.Sprintf("corteza::compose:module/%d/%d", ns1.ID, mod1.ID), + Operation: "op4", + Access: rbac.Deny, + }) + }) +} + +func compareRules(req *require.Assertions, a, b rbac.Rule) { + if a.RoleID != b.RoleID { + req.FailNow("RoleID missmatch") + } + + if a.Resource != b.Resource { + req.FailNow("Resource missmatch") + } + + if a.Operation != b.Operation { + req.FailNow("Operation missmatch") + } + + if a.Access != b.Access { + req.FailNow("Access missmatch") + } +} diff --git a/server/tests/envoy/testdata/rbac/rbac.yaml b/server/tests/envoy/testdata/rbac/rbac.yaml new file mode 100644 index 000000000..0c55e0653 --- /dev/null +++ b/server/tests/envoy/testdata/rbac/rbac.yaml @@ -0,0 +1,33 @@ +namespace: + test_ns_1: + name: Test Namespace 1 + modules: + test_mod_1: + name: Test Module 1 +roles: + test_role_1: + name: Test Role 1 + test_role_2: + name: Test Role 2 + +allow: + test_role_1: + corteza::compose:namespace/*: + - op1 + corteza::compose:namespace/test_ns_1: + - op2 + corteza::compose:module/test_ns_1/*: + - op3 + corteza::compose:module/test_ns_1/test_mod_1: + - op4 + +deny: + test_role_2: + corteza::compose:namespace/*: + - op1 + corteza::compose:namespace/test_ns_1: + - op2 + corteza::compose:module/test_ns_1/*: + - op3 + corteza::compose:module/test_ns_1/test_mod_1: + - op4