From f662d4e794da1404df0ed08c9c7c613e3c88f35f Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Tue, 13 Jul 2021 08:51:32 +0200 Subject: [PATCH] Add RBAC functions for workflow for check&grant --- pkg/rbac/resource.go | 16 ++ pkg/rbac/resource_test.go | 21 ++ pkg/rbac/rule_test.go | 2 - system/automation/expr_types.gen.go | 40 +++ system/automation/expr_types.go | 12 + system/automation/expr_types.yaml | 6 +- system/automation/rbac_handler.gen.go | 372 ++++++++++++++++++++++++++ system/automation/rbac_handler.go | 71 +++++ system/automation/rbac_handler.yaml | 64 +++++ system/service/service.go | 6 + 10 files changed, 607 insertions(+), 3 deletions(-) create mode 100644 system/automation/rbac_handler.gen.go create mode 100644 system/automation/rbac_handler.go create mode 100644 system/automation/rbac_handler.yaml diff --git a/pkg/rbac/resource.go b/pkg/rbac/resource.go index 4852ba6bf..5e56ce8e3 100644 --- a/pkg/rbac/resource.go +++ b/pkg/rbac/resource.go @@ -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 diff --git a/pkg/rbac/resource_test.go b/pkg/rbac/resource_test.go index c5abe78a7..479a27bc3 100644 --- a/pkg/rbac/resource_test.go +++ b/pkg/rbac/resource_test.go @@ -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 { diff --git a/pkg/rbac/rule_test.go b/pkg/rbac/rule_test.go index 11db1a2c5..34a022aab 100644 --- a/pkg/rbac/rule_test.go +++ b/pkg/rbac/rule_test.go @@ -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) diff --git a/system/automation/expr_types.gen.go b/system/automation/expr_types.gen.go index effb965f8..e0986abf8 100644 --- a/system/automation/expr_types.gen.go +++ b/system/automation/expr_types.gen.go @@ -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 } diff --git a/system/automation/expr_types.go b/system/automation/expr_types.go index c5569878a..a80efb4f0 100644 --- a/system/automation/expr_types.go +++ b/system/automation/expr_types.go @@ -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) + } +} diff --git a/system/automation/expr_types.yaml b/system/automation/expr_types.yaml index 28211155a..11e52c873 100644 --- a/system/automation/expr_types.yaml +++ b/system/automation/expr_types.yaml @@ -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' diff --git a/system/automation/rbac_handler.gen.go b/system/automation/rbac_handler.gen.go new file mode 100644 index 000000000..c12ad116b --- /dev/null +++ b/system/automation/rbac_handler.gen.go @@ -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 + }, + } +} diff --git a/system/automation/rbac_handler.go b/system/automation/rbac_handler.go new file mode 100644 index 000000000..0612c3205 --- /dev/null +++ b/system/automation/rbac_handler.go @@ -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 +} diff --git a/system/automation/rbac_handler.yaml b/system/automation/rbac_handler.yaml new file mode 100644 index 000000000..3f8420af7 --- /dev/null +++ b/system/automation/rbac_handler.yaml @@ -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 } diff --git a/system/service/service.go b/system/service/service.go index 9ae277e12..e92fce0e8 100644 --- a/system/service/service.go +++ b/system/service/service.go @@ -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 }