3
0

Add support for ComposeRecord RBAC rules

This commit is contained in:
Tomaž Jerman
2021-07-15 12:51:43 +02:00
parent a2b964c53e
commit 80e60d2f20
8 changed files with 314 additions and 40 deletions

View File

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

View File

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

View File

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

View File

@@ -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:

View File

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

View File

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

View File

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

View File

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