Add support for ComposeRecord RBAC rules
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
45
tests/envoy/testdata/base/rbac_rules_compose_records.yaml
vendored
Normal file
45
tests/envoy/testdata/base/rbac_rules_compose_records.yaml
vendored
Normal 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
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user