From 80e60d2f20054e8d22acac92f1004802753bdebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Jerman?= Date: Thu, 15 Jul 2021 12:51:43 +0200 Subject: [PATCH] Add support for ComposeRecord RBAC rules --- pkg/envoy/store/rbac_rule_marshal.go | 36 +++--- pkg/envoy/store/rbac_rule_unmarshal.go | 15 --- pkg/envoy/store/system.go | 37 ++++-- pkg/envoy/yaml/rbac_rules_marshal.go | 21 +++- pkg/rbac/rule.go | 12 ++ tests/envoy/store_yaml_base_test.go | 82 ++++++++++++++ .../base/rbac_rules_compose_records.yaml | 45 ++++++++ tests/envoy/yaml_store_base_test.go | 106 ++++++++++++++++++ 8 files changed, 314 insertions(+), 40 deletions(-) create mode 100644 tests/envoy/testdata/base/rbac_rules_compose_records.yaml diff --git a/pkg/envoy/store/rbac_rule_marshal.go b/pkg/envoy/store/rbac_rule_marshal.go index 1ba3156d3..4f720b902 100644 --- a/pkg/envoy/store/rbac_rule_marshal.go +++ b/pkg/envoy/store/rbac_rule_marshal.go @@ -85,6 +85,7 @@ func (n *rbacRule) Encode(ctx context.Context, pl *payload) (err error) { p0ID := uint64(0) p1ID := uint64(0) + p2ID := uint64(0) switch n.rule.Resource { case composeTypes.ComponentResourceType: @@ -178,24 +179,25 @@ func (n *rbacRule) Encode(ctx context.Context, pl *payload) (err error) { res.Resource = composeTypes.PageRbacResource(p0ID, p1ID) case composeTypes.RecordResourceType: - return fmt.Errorf("importing rbac rules on record level is not supported") - //p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefPath[0].Identifiers) - //if p0 == nil { - // return resource.ComposeNamespaceErrUnresolved(n.res.RefPath[0].Identifiers) - //} - // - //p1 := resource.FindComposeModule(pl.state.ParentResources, n.res.RefPath[1].Identifiers) - //if p1 == nil { - // return resource.ComposeNamespaceErrUnresolved(n.res.RefPath[1].Identifiers) - //} + if len(n.res.RefPath) > 0 { + p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefPath[0].Identifiers) + if p0 == nil { + return resource.ComposeNamespaceErrUnresolved(n.res.RefPath[0].Identifiers) + } + p0ID = p0.ID + } + if len(n.res.RefPath) > 1 { + p1 := resource.FindComposeModule(pl.state.ParentResources, n.res.RefPath[1].Identifiers) + if p1 == nil { + return resource.ComposeModuleErrUnresolved(n.res.RefPath[1].Identifiers) + } + p1ID = p1.ID + } + + // @todo specific record RBAC + + res.Resource = composeTypes.RecordRbacResource(p0ID, p1ID, p2ID) - //// - //p2 := resource.FindComposeRecord(pl.state.ParentResources, n.res.RefResource.Identifiers) - //if p2 == nil { - // return resource.ComposeNamespaceErrUnresolved(n.res.RefResource.Identifiers) - //} - // - //res.Resource = composeTypes.RecordRbacResource(p0.ID, p1.ID, p2.ID) case composeTypes.ModuleFieldResourceType: return fmt.Errorf("importing rbac rules on module field level is not supported") //p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefPath[0].Identifiers) diff --git a/pkg/envoy/store/rbac_rule_unmarshal.go b/pkg/envoy/store/rbac_rule_unmarshal.go index 729494770..cb2be53dd 100644 --- a/pkg/envoy/store/rbac_rule_unmarshal.go +++ b/pkg/envoy/store/rbac_rule_unmarshal.go @@ -35,20 +35,5 @@ func rbacResToRef(rr string) (*resource.Ref, error) { return ref, nil } - // When len is 1; only top-level defined (system, compose, ....) - //if len(parts) == 1 { - // ref.ResourceType = rr - // return ref, nil - //} - - //When len is 3; both levels defined; resource ref also provided - //if len(parts) == 3 { - // ref.ResourceType = strings.Join(parts[0:2], rbacSep) + rbacSep - // if parts[2] != "*" { - // ref.Identifiers = resource.MakeIdentifiers(parts[2]) - // } - // return ref, nil - //} - return nil, fmt.Errorf("invalid resource provided: %s", rr) } diff --git a/pkg/envoy/store/system.go b/pkg/envoy/store/system.go index fa227bda7..1ad69e8fc 100644 --- a/pkg/envoy/store/system.go +++ b/pkg/envoy/store/system.go @@ -276,6 +276,20 @@ func (d *systemDecoder) decodeRbac(ctx context.Context, s store.Storer, ff []*rb var nn rbac.RuleSet var err error + c := func(r *resource.Ref, f *rbacFilter) (bool, error) { + if r == nil { + return true, nil + } + + // the first identifier is the most specific .. the ID + id, err := cast.ToUint64E(r.Identifiers.First()) + if err != nil { + return false, err + } + + return f.resourceID[id], nil + } + for _, f := range ff { aux := *f @@ -301,20 +315,29 @@ func (d *systemDecoder) decodeRbac(ctx context.Context, s store.Storer, ff []*rb } // somesort of a generic rule; no specifc resource - if ref == nil { + // @todo check for pp inclusion!! + if ref == nil && len(pp) == 0 { mm = append(mm, r) } else { - // the first identifier is the most specific .. the ID - id, err := cast.ToUint64E(ref.Identifiers.First()) - if err != nil { + // check the resource ref and the path refs for validity + if ok, err := c(ref, f); err != nil { return &auxRsp{ err: err, } + } else if !ok { + continue + } + for _, p := range pp { + if ok, err := c(p, f); err != nil { + return &auxRsp{ + err: err, + } + } else if !ok { + continue + } } - if f.resourceID[id] { - mm = append(mm, r) - } + mm = append(mm, r) } } diff --git a/pkg/envoy/yaml/rbac_rules_marshal.go b/pkg/envoy/yaml/rbac_rules_marshal.go index 9564176d9..09109dd30 100644 --- a/pkg/envoy/yaml/rbac_rules_marshal.go +++ b/pkg/envoy/yaml/rbac_rules_marshal.go @@ -59,6 +59,7 @@ func (r *rbacRule) Encode(ctx context.Context, doc *Document, state *envoy.Resou p0ID := "*" p1ID := "*" + p2ID := "*" switch r.res.Resource { case composeTypes.ComponentResourceType, @@ -136,7 +137,25 @@ func (r *rbacRule) Encode(ctx context.Context, doc *Document, state *envoy.Resou r.res.Resource = fmt.Sprintf(composeTypes.PageRbacResourceTpl(), composeTypes.PageResourceType, p0ID, p1ID) case composeTypes.RecordResourceType: - return fmt.Errorf("importing rbac rules on record level is not supported") + if len(res.RefPath) > 0 { + p0 := resource.FindComposeNamespace(state.ParentResources, res.RefPath[0].Identifiers) + if p0 == nil { + return resource.ComposeNamespaceErrUnresolved(res.RefPath[0].Identifiers) + } + p0ID = p0.Slug + } + if len(res.RefPath) > 1 { + p1 := resource.FindComposeModule(state.ParentResources, res.RefPath[1].Identifiers) + if p1 == nil { + return resource.ComposeModuleErrUnresolved(res.RefPath[1].Identifiers) + } + p1ID = p1.Handle + } + + // @todo specific record RBAC + + r.res.Resource = fmt.Sprintf(composeTypes.RecordRbacResourceTpl(), composeTypes.RecordResourceType, p0ID, p1ID, p2ID) + case composeTypes.ModuleFieldResourceType: return fmt.Errorf("importing rbac rules on module field level is not supported") case systemTypes.UserResourceType: diff --git a/pkg/rbac/rule.go b/pkg/rbac/rule.go index 69b6ba7f0..3c5585ca1 100644 --- a/pkg/rbac/rule.go +++ b/pkg/rbac/rule.go @@ -56,6 +56,18 @@ func (set RuleSet) Less(i, j int) bool { return level(set[i].Resource) > level(set[j].Resource) } +func (set RuleSet) FilterAccess(a Access) (out RuleSet) { + out = make(RuleSet, 0, len(set)) + + for _, s := range set { + if s.Access == a { + out = append(out, s) + } + } + + return out +} + // AllowRule helper func to create allow rule func AllowRule(id uint64, r, o string) *Rule { return &Rule{id, r, o, Allow, false} diff --git a/tests/envoy/store_yaml_base_test.go b/tests/envoy/store_yaml_base_test.go index 63a1a05c2..80a5bba16 100644 --- a/tests/envoy/store_yaml_base_test.go +++ b/tests/envoy/store_yaml_base_test.go @@ -630,6 +630,88 @@ func TestStoreYaml_base(t *testing.T) { } }, }, + + { + name: "ComposeRecord rbac", + pre: func(ctx context.Context, s store.Storer) (error, *su.DecodeFilter) { + rl := sTestRole(ctx, t, s, "base") + ns := sTestComposeNamespace(ctx, t, s, "base") + mod := sTestComposeModule(ctx, t, s, ns.ID, "base") + + rr := rbac.RuleSet{ + { + RoleID: rl.ID, + Resource: ctypes.RecordRbacResource(0, 0, 0), + Operation: "allow.op1", + Access: rbac.Allow, + }, + { + RoleID: rl.ID, + Resource: ctypes.RecordRbacResource(ns.ID, 0, 0), + Operation: "allow.op2", + Access: rbac.Deny, + }, + { + RoleID: rl.ID, + Resource: ctypes.RecordRbacResource(ns.ID, mod.ID, 0), + Operation: "allow.op3", + Access: rbac.Allow, + }, + } + if err := store.CreateRbacRule(ctx, s, rr...); err != nil { + t.Fatal(err) + } + + df := su.NewDecodeFilter(). + Roles(&stypes.RoleFilter{ + Handle: "base_role", + }). + ComposeNamespace(&ctypes.NamespaceFilter{ + Slug: "base_namespace", + }). + ComposeModule(&ctypes.ModuleFilter{ + Handle: "base_module", + }). + Rbac(&rbac.RuleFilter{}) + return nil, df + }, + check: func(ctx context.Context, s store.Storer, req *require.Assertions) { + rl, err := store.LookupRoleByHandle(ctx, s, "base_role") + req.NoError(err) + + ns, err := store.LookupComposeNamespaceBySlug(ctx, s, "base_namespace") + req.NoError(err) + + mod, err := store.LookupComposeModuleByNamespaceIDHandle(ctx, s, ns.ID, "base_module") + req.NoError(err) + + rr, _, err := store.SearchRbacRules(ctx, s, rbac.RuleFilter{}) + req.NoError(err) + req.Len(rr, 3) + + for _, r := range rr { + switch r.Operation { + case "allow.op1": + req.Equal(rl.ID, r.RoleID) + req.Equal(rbac.Allow, r.Access) + req.Equal(ctypes.RecordRbacResource(0, 0, 0), r.Resource) + + case "allow.op2": + req.Equal(rl.ID, r.RoleID) + req.Equal(rbac.Deny, r.Access) + req.Equal(ctypes.RecordRbacResource(ns.ID, 0, 0), r.Resource) + + case "allow.op3": + req.Equal(rl.ID, r.RoleID) + req.Equal(rbac.Allow, r.Access) + req.Equal(ctypes.RecordRbacResource(ns.ID, mod.ID, 0), r.Resource) + + default: + req.FailNow("unexpected rbac operation for test cases: ", r.Operation) + } + } + }, + }, } for _, c := range cases { diff --git a/tests/envoy/testdata/base/rbac_rules_compose_records.yaml b/tests/envoy/testdata/base/rbac_rules_compose_records.yaml new file mode 100644 index 000000000..07b4e0ec8 --- /dev/null +++ b/tests/envoy/testdata/base/rbac_rules_compose_records.yaml @@ -0,0 +1,45 @@ +roles: + r1: + name: r1 name + r2: + name: r2 name + +namespaces: + ns1: + name: ns1 name + +modules: + mod1: + name: mod1 name + fields: + f1: + label: f1 label + kind: String + required: true + +records: + mod1: + - values: + f1: "v1" + +allow: + r1: + corteza::compose:record/*/*/*: + - allow.op1 + corteza::compose:record/ns1/*/*: + - allow.op2 + corteza::compose:record/ns1/mod1/*: + - allow.op3 + # corteza::compose:record/ns1/mod1/rec: + # - delete + +deny: + r2: + corteza::compose:record/*/*/*: + - deny.op1 + corteza::compose:record/ns1/*/*: + - deny.op2 + corteza::compose:record/ns1/mod1/*: + - deny.op3 + # corteza::compose:record/ns1/mod1/rec: + # - delete diff --git a/tests/envoy/yaml_store_base_test.go b/tests/envoy/yaml_store_base_test.go index f9ff1fabc..275380ab9 100644 --- a/tests/envoy/yaml_store_base_test.go +++ b/tests/envoy/yaml_store_base_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "path" + "strconv" "testing" atypes "github.com/cortezaproject/corteza-server/automation/types" @@ -652,3 +653,108 @@ func TestYamlStore_base(t *testing.T) { }) } } + +func TestYamlStore_ComposeRecordRBAC(t *testing.T) { + var ( + ctx = context.Background() + namespace = "base" + s = initServices(ctx, t) + err error + ) + + ni := uint64(10) + su.NextID = func() uint64 { + ni++ + return ni + } + + ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser()) + + // Allow rec. field values to be updated for this user. + rbac.Global().Grant( + ctx, + rbac.AllowRule( + auth.ServiceUser().Roles()[0], + ctypes.ModuleFieldRbacResource(0, 0, 0), + "record.value.update", + ), + ) + + f := "rbac_rules_compose_records.yaml" + t.Run(fmt.Sprintf("testdata/%s/%s", namespace, f), func(t *testing.T) { + // before running tests, reset ID and truncate store + ni = 10 + truncateStore(ctx, s, t) + + req := require.New(t) + + var nn []resource.Interface + nn, err = decodeYaml(ctx, namespace, f) + req.NoError(err) + + err = encode(ctx, s, nn) + req.NoError(err) + + rr, _, err := store.SearchRbacRules(ctx, s, rbac.RuleFilter{}) + req.NoError(err) + + // compose stuff + ns, err := store.LookupComposeNamespaceBySlug(ctx, s, "ns1") + req.NoError(err) + + mod1, err := store.LookupComposeModuleByNamespaceIDHandle(ctx, s, ns.ID, "mod1") + req.NoError(err) + + role1, err := store.LookupRoleByHandle(ctx, s, "r1") + req.NoError(err) + role2, err := store.LookupRoleByHandle(ctx, s, "r2") + req.NoError(err) + + // size + req.Len(rr, 6) + req.Len(rr.FilterAccess(rbac.Allow), 3) + req.Len(rr.FilterAccess(rbac.Deny), 3) + + // allows + var rule *rbac.Rule + const tpl = "corteza::compose:record/%s/%s/%s" + + // allows + rule = rr[0] + req.Equal(role1.ID, rule.RoleID) + req.Equal("allow.op1", rule.Operation) + req.Equal(rbac.Allow, rule.Access) + req.Equal(fmt.Sprintf(tpl, "*", "*", "*"), rule.Resource) + + rule = rr[1] + req.Equal(role1.ID, rule.RoleID) + req.Equal("allow.op2", rule.Operation) + req.Equal(rbac.Allow, rule.Access) + req.Equal(fmt.Sprintf(tpl, strconv.FormatUint(ns.ID, 10), "*", "*"), rule.Resource) + + rule = rr[2] + req.Equal(role1.ID, rule.RoleID) + req.Equal("allow.op3", rule.Operation) + req.Equal(rbac.Allow, rule.Access) + req.Equal(fmt.Sprintf(tpl, strconv.FormatUint(ns.ID, 10), strconv.FormatUint(mod1.ID, 10), "*"), rule.Resource) + + // denies + rule = rr[3] + req.Equal(role2.ID, rule.RoleID) + req.Equal("deny.op1", rule.Operation) + req.Equal(rbac.Deny, rule.Access) + req.Equal(fmt.Sprintf(tpl, "*", "*", "*"), rule.Resource) + + rule = rr[4] + req.Equal(role2.ID, rule.RoleID) + req.Equal("deny.op2", rule.Operation) + req.Equal(rbac.Deny, rule.Access) + req.Equal(fmt.Sprintf(tpl, strconv.FormatUint(ns.ID, 10), "*", "*"), rule.Resource) + + rule = rr[5] + req.Equal(role2.ID, rule.RoleID) + req.Equal("deny.op3", rule.Operation) + req.Equal(rbac.Deny, rule.Access) + req.Equal(fmt.Sprintf(tpl, strconv.FormatUint(ns.ID, 10), strconv.FormatUint(mod1.ID, 10), "*"), rule.Resource) + }) +}