3
0

Add RBAC functions for workflow for check&grant

This commit is contained in:
Denis Arh
2021-07-13 08:51:32 +02:00
parent c5ec59e9d6
commit f662d4e794
10 changed files with 607 additions and 3 deletions
+16
View File
@@ -17,6 +17,7 @@ type (
const (
nsSep = "::"
cmpSep = ":"
pathSep = "/"
wildcard = "*"
)
@@ -34,6 +35,21 @@ func ResourceType(r string) string {
}
}
func ResourceComponent(r string) string {
var (
t = ResourceType(r)
ns = strings.Index(t, nsSep)
c = strings.LastIndex(t, cmpSep)
)
// make sure that we have both namespace + component separators
if c > ns+1 && ns > -1 {
return t[:c]
} else {
return t
}
}
func matchResource(matcher, resource string) (m bool) {
if matcher == resource {
// if resources match make sure no wildcards are resent
+21
View File
@@ -29,6 +29,27 @@ func TestResourceType(t *testing.T) {
}
}
func TestResourceComponent(t *testing.T) {
var (
tcc = []struct {
in string
exp string
}{
{"ns::cmp:r/1/2/3", "ns::cmp"},
{"ns::cmp:r", "ns::cmp"},
{"ns::cmp/", "ns::cmp"},
{"ns::cmp", "ns::cmp"},
{"cmp", "cmp"},
}
)
for _, tc := range tcc {
t.Run(tc.in, func(t *testing.T) {
require.Equal(t, tc.exp, ResourceComponent(tc.in))
})
}
}
func TestResourceMatch(t *testing.T) {
var (
tcc = []struct {
-2
View File
@@ -4,7 +4,6 @@ import (
"sort"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/require"
)
@@ -30,7 +29,6 @@ func TestRuleSetSort(t *testing.T) {
req.NotNil(rr)
sort.Sort(rr)
spew.Dump(rr)
c = i()
req.Equal(":::/1/2/3", rr[i()].Resource)
req.Equal(":::/1/*/3", rr[i()].Resource)
+40
View File
@@ -11,7 +11,9 @@ package automation
import (
"context"
"fmt"
. "github.com/cortezaproject/corteza-server/pkg/expr"
"github.com/cortezaproject/corteza-server/pkg/rbac"
"github.com/cortezaproject/corteza-server/system/types"
)
@@ -178,6 +180,44 @@ func assignToQueueMessage(res *types.QueueMessage, k string, val interface{}) er
return fmt.Errorf("unknown field '%s'", k)
}
// RbacResource is an expression type, wrapper for rbac.Resource type
type RbacResource struct{ value rbac.Resource }
// NewRbacResource creates new instance of RbacResource expression type
func NewRbacResource(val interface{}) (*RbacResource, error) {
if c, err := CastToRbacResource(val); err != nil {
return nil, fmt.Errorf("unable to create RbacResource: %w", err)
} else {
return &RbacResource{value: c}, nil
}
}
// Return underlying value on RbacResource
func (t RbacResource) Get() interface{} { return t.value }
// Return underlying value on RbacResource
func (t RbacResource) GetValue() rbac.Resource { return t.value }
// Return type name
func (RbacResource) Type() string { return "RbacResource" }
// Convert value to rbac.Resource
func (RbacResource) Cast(val interface{}) (TypedValue, error) {
return NewRbacResource(val)
}
// Assign new value to RbacResource
//
// value is first passed through CastToRbacResource
func (t *RbacResource) Assign(val interface{}) error {
if c, err := CastToRbacResource(val); err != nil {
return err
} else {
t.value = c
return nil
}
}
// RenderOptions is an expression type, wrapper for map[string]string type
type RenderOptions struct{ value map[string]string }
+12
View File
@@ -6,6 +6,7 @@ import (
"io/ioutil"
"github.com/cortezaproject/corteza-server/pkg/expr"
"github.com/cortezaproject/corteza-server/pkg/rbac"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/spf13/cast"
)
@@ -152,3 +153,14 @@ func (doc renderedDocument) String() string {
aux, _ := ioutil.ReadAll(doc.Document)
return string(aux)
}
func CastToRbacResource(val interface{}) (out rbac.Resource, err error) {
switch val := expr.UntypedValue(val).(type) {
case rbac.Resource:
return val, nil
case RbacResource:
return val.value, nil
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
+5 -1
View File
@@ -1,6 +1,7 @@
package: automation
imports:
- github.com/cortezaproject/corteza-server/system/types
- github.com/cortezaproject/corteza-server/pkg/rbac
types:
Template:
@@ -63,9 +64,12 @@ types:
- { name: 'type', exprType: 'string', goType: 'string' }
RenderOptions:
as: 'map[string]string'
QueueMessage:
as: '*types.QueueMessage'
struct:
- { name: 'Queue', exprType: 'String', goType: 'string' }
- { name: 'Payload', exprType: 'String', goType: '[]byte' }
RbacResource:
as: 'rbac.Resource'
+372
View File
@@ -0,0 +1,372 @@
package automation
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
// system/automation/rbac_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza-server/automation/types"
"github.com/cortezaproject/corteza-server/pkg/expr"
"github.com/cortezaproject/corteza-server/pkg/rbac"
"github.com/cortezaproject/corteza-server/pkg/wfexec"
"github.com/cortezaproject/corteza-server/system/types"
)
var _ wfexec.ExecResponse
type (
rbacHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h rbacHandler) register() {
h.reg.AddFunctions(
h.Allow(),
h.Deny(),
h.Inherit(),
h.Check(),
)
}
type (
rbacAllowArgs struct {
hasResource bool
Resource rbac.Resource
hasRole bool
Role interface{}
roleID uint64
roleHandle string
roleRes *types.Role
hasOperation bool
Operation string
}
)
func (a rbacAllowArgs) GetRole() (bool, uint64, string, *types.Role) {
return a.hasRole, a.roleID, a.roleHandle, a.roleRes
}
// Allow function RBAC: Allow operation on resource to a role
//
// expects implementation of allow function:
// func (h rbacHandler) allow(ctx context.Context, args *rbacAllowArgs) (err error) {
// return
// }
func (h rbacHandler) Allow() *atypes.Function {
return &atypes.Function{
Ref: "rbacAllow",
Kind: "function",
Labels: map[string]string{"users": "rbac"},
Meta: &atypes.FunctionMeta{
Short: "RBAC: Allow operation on resource to a role",
},
Parameters: []*atypes.Param{
{
Name: "resource",
Types: []string{"RbacResource"}, Required: true,
},
{
Name: "role",
Types: []string{"ID", "Handle", "Role"}, Required: true,
},
{
Name: "operation",
Types: []string{"String"}, Required: true,
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &rbacAllowArgs{
hasResource: in.Has("resource"),
hasRole: in.Has("role"),
hasOperation: in.Has("operation"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Role argument
if args.hasRole {
aux := expr.Must(expr.Select(in, "role"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.roleID = aux.Get().(uint64)
case h.reg.Type("Handle").Type():
args.roleHandle = aux.Get().(string)
case h.reg.Type("Role").Type():
args.roleRes = aux.Get().(*types.Role)
}
}
return out, h.allow(ctx, args)
},
}
}
type (
rbacDenyArgs struct {
hasResource bool
Resource rbac.Resource
hasRole bool
Role interface{}
roleID uint64
roleHandle string
roleRes *types.Role
hasOperation bool
Operation string
}
)
func (a rbacDenyArgs) GetRole() (bool, uint64, string, *types.Role) {
return a.hasRole, a.roleID, a.roleHandle, a.roleRes
}
// Deny function RBAC: Deny operation on resource to a role
//
// expects implementation of deny function:
// func (h rbacHandler) deny(ctx context.Context, args *rbacDenyArgs) (err error) {
// return
// }
func (h rbacHandler) Deny() *atypes.Function {
return &atypes.Function{
Ref: "rbacDeny",
Kind: "function",
Labels: map[string]string{"users": "rbac"},
Meta: &atypes.FunctionMeta{
Short: "RBAC: Deny operation on resource to a role",
},
Parameters: []*atypes.Param{
{
Name: "resource",
Types: []string{"RbacResource"}, Required: true,
},
{
Name: "role",
Types: []string{"ID", "Handle", "Role"}, Required: true,
},
{
Name: "operation",
Types: []string{"String"}, Required: true,
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &rbacDenyArgs{
hasResource: in.Has("resource"),
hasRole: in.Has("role"),
hasOperation: in.Has("operation"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Role argument
if args.hasRole {
aux := expr.Must(expr.Select(in, "role"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.roleID = aux.Get().(uint64)
case h.reg.Type("Handle").Type():
args.roleHandle = aux.Get().(string)
case h.reg.Type("Role").Type():
args.roleRes = aux.Get().(*types.Role)
}
}
return out, h.deny(ctx, args)
},
}
}
type (
rbacInheritArgs struct {
hasResource bool
Resource rbac.Resource
hasRole bool
Role interface{}
roleID uint64
roleHandle string
roleRes *types.Role
hasOperation bool
Operation string
}
)
func (a rbacInheritArgs) GetRole() (bool, uint64, string, *types.Role) {
return a.hasRole, a.roleID, a.roleHandle, a.roleRes
}
// Inherit function RBAC: Remove allow/deny operation of a role from resource
//
// expects implementation of inherit function:
// func (h rbacHandler) inherit(ctx context.Context, args *rbacInheritArgs) (err error) {
// return
// }
func (h rbacHandler) Inherit() *atypes.Function {
return &atypes.Function{
Ref: "rbacInherit",
Kind: "function",
Labels: map[string]string{"users": "rbac"},
Meta: &atypes.FunctionMeta{
Short: "RBAC: Remove allow/deny operation of a role from resource",
},
Parameters: []*atypes.Param{
{
Name: "resource",
Types: []string{"RbacResource"}, Required: true,
},
{
Name: "role",
Types: []string{"ID", "Handle", "Role"}, Required: true,
},
{
Name: "operation",
Types: []string{"String"}, Required: true,
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &rbacInheritArgs{
hasResource: in.Has("resource"),
hasRole: in.Has("role"),
hasOperation: in.Has("operation"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Role argument
if args.hasRole {
aux := expr.Must(expr.Select(in, "role"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.roleID = aux.Get().(uint64)
case h.reg.Type("Handle").Type():
args.roleHandle = aux.Get().(string)
case h.reg.Type("Role").Type():
args.roleRes = aux.Get().(*types.Role)
}
}
return out, h.inherit(ctx, args)
},
}
}
type (
rbacCheckArgs struct {
hasResource bool
Resource rbac.Resource
hasOperation bool
Operation string
hasUser bool
User *types.User
}
rbacCheckResults struct {
Can bool
}
)
// Check function RBAC: Can user perform an operation on a resource
//
// expects implementation of check function:
// func (h rbacHandler) check(ctx context.Context, args *rbacCheckArgs) (results *rbacCheckResults, err error) {
// return
// }
func (h rbacHandler) Check() *atypes.Function {
return &atypes.Function{
Ref: "rbacCheck",
Kind: "function",
Labels: map[string]string{"users": "rbac"},
Meta: &atypes.FunctionMeta{
Short: "RBAC: Can user perform an operation on a resource",
},
Parameters: []*atypes.Param{
{
Name: "resource",
Types: []string{"RbacResource"}, Required: true,
},
{
Name: "operation",
Types: []string{"String"}, Required: true,
},
{
Name: "user",
Types: []string{"User"},
},
},
Results: []*atypes.Param{
{
Name: "can",
Types: []string{"Boolean"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &rbacCheckArgs{
hasResource: in.Has("resource"),
hasOperation: in.Has("operation"),
hasUser: in.Has("user"),
}
)
if err = in.Decode(args); err != nil {
return
}
var results *rbacCheckResults
if results, err = h.check(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Can (bool) to Boolean
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Boolean").Cast(results.Can); err != nil {
return
} else if err = expr.Assign(out, "can", tval); err != nil {
return
}
}
return
},
}
}
+71
View File
@@ -0,0 +1,71 @@
package automation
import (
"context"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/rbac"
)
type (
rbacService interface {
Can(ses rbac.Session, op string, res rbac.Resource) bool
Grant(ctx context.Context, rules ...*rbac.Rule) (err error)
}
rbacHandler struct {
reg rbacHandlerRegistry
svc rbacService
}
auxResource struct{ res string }
)
var _ rbac.Resource = &auxResource{}
func (r auxResource) RbacResource() string {
return r.res
}
func RbacHandler(reg rbacHandlerRegistry, svc rbacService) *rbacHandler {
h := &rbacHandler{
reg: reg,
svc: svc,
}
h.register()
return h
}
func (h rbacHandler) allow(ctx context.Context, args *rbacAllowArgs) (err error) {
return h.grant(ctx, rbac.AllowRule(args.roleID, args.Operation, args.Resource.RbacResource()))
}
func (h rbacHandler) deny(ctx context.Context, args *rbacDenyArgs) (err error) {
return h.grant(ctx, rbac.DenyRule(args.roleID, args.Operation, args.Resource.RbacResource()))
}
func (h rbacHandler) inherit(ctx context.Context, args *rbacInheritArgs) (err error) {
return h.grant(ctx, rbac.InheritRule(args.roleID, args.Operation, args.Resource.RbacResource()))
}
// verifies grant op (granter needs to be allowed to grant on the component
func (h rbacHandler) grant(ctx context.Context, r *rbac.Rule) (err error) {
// we can safely create a fake component resource since there are
// no extra metadata hidden under it,
cmpRes := &auxResource{rbac.ResourceComponent(r.Resource)}
if !h.svc.Can(rbac.ContextToSession(ctx), "grant", cmpRes) {
return fmt.Errorf("not allowed to grant %s", r.String())
}
return h.svc.Grant(ctx, r)
}
func (h rbacHandler) check(ctx context.Context, args *rbacCheckArgs) (b *rbacCheckResults, err error) {
b = &rbacCheckResults{}
b.Can = h.svc.Can(rbac.ContextToSession(ctx), args.Operation, args.Resource)
return
}
+64
View File
@@ -0,0 +1,64 @@
imports:
- github.com/cortezaproject/corteza-server/system/types
- github.com/cortezaproject/corteza-server/pkg/rbac
params:
manageParams: &manageParams
resource:
required: true
types:
# Note that we can not accept string here because
# we need whole resource with all the info loaded
- { wf: RbacResource }
role:
required: true
types:
- { wf: ID }
- { wf: Handle }
- { wf: Role, suffix: res }
operation:
required: true
types:
- { wf: String }
labels: &labels
users: "rbac"
functions:
allow:
meta:
short: 'RBAC: Allow operation on resource to a role'
labels:
<<: *labels
params: *manageParams
deny:
meta:
short: 'RBAC: Deny operation on resource to a role'
labels:
<<: *labels
params: *manageParams
inherit:
meta:
short: 'RBAC: Remove allow/deny operation of a role from resource'
labels:
<<: *labels
params: *manageParams
check:
meta:
short: 'RBAC: Can user perform an operation on a resource'
labels:
<<: *labels
params:
resource:
required: true
types: [ { wf: RbacResource } ]
operation:
required: true
types: [ { wf: String } ]
user:
types: [ { wf: User } ]
results:
can: { wf: Boolean }
+6
View File
@@ -181,6 +181,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock
automation.Template{},
automation.RenderOptions{},
automation.RenderedDocument{},
automation.RbacResource{},
)
automation.UsersHandler(
@@ -198,6 +199,11 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock
DefaultRole,
)
automation.RbacHandler(
automationService.Registry(),
rbac.Global(),
)
return
}