From 14d3b7033db6e5e8b233ca16ebcf96cd4603d550 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Tue, 28 Jun 2022 12:53:51 +0200 Subject: [PATCH] Refactor RBAC evaluation processing --- automation/rest/permissions.go | 17 +-- automation/service/access_control.gen.go | 91 ++++++++++++---- automation/service/service.go | 22 +--- .../rbac/$component_access_control.go.tpl | 93 ++++++++++++---- codegen/server.rbac-access_control.cue | 12 ++ compose/rest/permissions.go | 17 +-- compose/service/access_control.gen.go | 96 ++++++++++++---- compose/service/service.go | 3 +- federation/rest/permissions.go | 17 +-- federation/service/access_control.gen.go | 93 ++++++++++++---- federation/service/service.go | 11 +- pkg/envoy/store/compose.go | 2 +- pkg/rbac/effective.go | 23 ---- pkg/rbac/evaluate.go | 27 +++++ pkg/rbac/resource.go | 14 +++ pkg/rbac/roles_test.go | 10 +- pkg/rbac/service.go | 4 +- system/rest/permissions.go | 20 +--- system/service/access_control.gen.go | 103 ++++++++++++++---- system/service/service.go | 2 +- 20 files changed, 436 insertions(+), 241 deletions(-) create mode 100644 pkg/rbac/evaluate.go diff --git a/automation/rest/permissions.go b/automation/rest/permissions.go index 73229ff82..11acd00dc 100644 --- a/automation/rest/permissions.go +++ b/automation/rest/permissions.go @@ -15,13 +15,9 @@ type ( ac permissionsAccessController } - rbacResWrap struct { - res string - } - permissionsAccessController interface { Effective(context.Context, ...rbac.Resource) rbac.EffectiveSet - Evaluate(ctx context.Context, user uint64, roles []uint64, rr ...rbac.Resource) (ee rbac.EvaluatedSet, err error) + Evaluate(ctx context.Context, user uint64, roles []uint64, rr ...string) (ee rbac.EvaluatedSet, err error) List() []map[string]string FindRulesByRoleID(context.Context, uint64) (rbac.RuleSet, error) Grant(ctx context.Context, rr ...*rbac.Rule) error @@ -39,12 +35,7 @@ func (ctrl Permissions) Effective(ctx context.Context, r *request.PermissionsEff } func (ctrl Permissions) Evaluate(ctx context.Context, r *request.PermissionsEvaluate) (interface{}, error) { - in := make([]rbac.Resource, 0, len(r.Resource)) - for _, res := range r.Resource { - in = append(in, rbacResWrap{res: res}) - } - - return ctrl.ac.Evaluate(ctx, r.UserID, r.RoleID, in...) + return ctrl.ac.Evaluate(ctx, r.UserID, r.RoleID, r.Resource...) } func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (interface{}, error) { @@ -76,7 +67,3 @@ func (ctrl Permissions) Update(ctx context.Context, r *request.PermissionsUpdate return api.OK(), ctrl.ac.Grant(ctx, r.Rules...) } - -func (ar rbacResWrap) RbacResource() string { - return ar.res -} diff --git a/automation/service/access_control.gen.go b/automation/service/access_control.gen.go index bfedabcd5..ba19741b5 100644 --- a/automation/service/access_control.gen.go +++ b/automation/service/access_control.gen.go @@ -12,29 +12,36 @@ import ( "github.com/cortezaproject/corteza-server/automation/types" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/rbac" + systemTypes "github.com/cortezaproject/corteza-server/system/types" "github.com/spf13/cast" "strings" ) type ( + roleMemberSearcher interface { + SearchRoleMembers(context.Context, systemTypes.RoleMemberFilter) (systemTypes.RoleMemberSet, systemTypes.RoleMemberFilter, error) + } + + rbacService interface { + Evaluate(rbac.Session, string, rbac.Resource) rbac.Evaluated + Grant(context.Context, ...*rbac.Rule) error + FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet) + CloneRulesByRoleID(ctx context.Context, fromRoleID uint64, toRoleID ...uint64) error + } + accessControl struct { actionlog actionlog.Recorder - roleFinder func(ctx context.Context, id uint64) ([]uint64, error) - rbac interface { - Evaluate(rbac.Session, string, rbac.Resource) rbac.Evaluated - Grant(context.Context, ...*rbac.Rule) error - FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet) - CloneRulesByRoleID(ctx context.Context, fromRoleID uint64, toRoleID ...uint64) error - } + store roleMemberSearcher + rbac rbacService } ) -func AccessControl(rf func(ctx context.Context, id uint64) ([]uint64, error)) *accessControl { +func AccessControl(rms roleMemberSearcher) *accessControl { return &accessControl{ - roleFinder: rf, - rbac: rbac.Global(), - actionlog: DefaultActionlog, + store: rms, + rbac: rbac.Global(), + actionlog: DefaultActionlog, } } @@ -43,6 +50,8 @@ func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) } // Effective returns a list of effective permissions for all given resource +// +// This function is auto-generated func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee rbac.EffectiveSet) { for _, res := range rr { r := res.RbacResource() @@ -55,38 +64,74 @@ func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee } // Evaluate returns a list of permissions evaluated for the given user/roles combo -func (svc accessControl) Evaluate(ctx context.Context, user uint64, roles []uint64, rr ...rbac.Resource) (ee rbac.EvaluatedSet, err error) { +// +// This function is auto-generated +func (svc accessControl) Evaluate(ctx context.Context, userID uint64, roles []uint64, rr ...string) (ee rbac.EvaluatedSet, err error) { // Reusing the grant permission since this is who the feature is for if !svc.CanGrant(ctx) { // @todo should be altered to check grant permissions PER resource return nil, AccessControlErrNotAllowedToSetPermissions() } - // Load roles for this user - // - // User's roles take priority over specified ones - if user != 0 { - rr, err := svc.roleFinder(ctx, user) + var ( + resources []rbac.Resource + members systemTypes.RoleMemberSet + ) + if len(rr) > 0 { + resources = make([]rbac.Resource, 0, len(rr)) + for _, r := range rr { + resources = append(resources, rbac.NewResource(r)) + } + } else { + resources = svc.Resources() + } + + // User ID specified, load its roles + if userID != 0 { + if len(roles) > 0 { + // should be prevented on the client + return nil, fmt.Errorf("userID and roles are mutually exclusive") + } + + members, _, err = svc.store.SearchRoleMembers(ctx, systemTypes.RoleMemberFilter{UserID: userID}) if err != nil { return nil, err } - roles = append(rr, roles...) + for _, m := range members { + roles = append(roles, m.RoleID) + } } - session := rbac.ParamsToSession(ctx, user, roles...) - for _, res := range rr { + if len(roles) == 0 { + // should be prevented on the client + return nil, fmt.Errorf("no roles specified") + } + + session := rbac.ParamsToSession(ctx, userID, roles...) + for _, res := range resources { r := res.RbacResource() for op := range rbacResourceOperations(r) { - eval := svc.rbac.Evaluate(session, op, res) - - ee = append(ee, eval) + ee = append(ee, svc.rbac.Evaluate(session, op, res)) } } return } +// Resources returns list of resources +// +// This function is auto-generated +func (svc accessControl) Resources() []rbac.Resource { + return []rbac.Resource{ + rbac.NewResource(types.WorkflowRbacResource(0)), + rbac.NewResource(types.ComponentRbacResource()), + } +} + +// List returns list of operations on all resources +// +// This function is auto-generated func (svc accessControl) List() (out []map[string]string) { def := []map[string]string{ { diff --git a/automation/service/service.go b/automation/service/service.go index 4898918b3..e79a07274 100644 --- a/automation/service/service.go +++ b/automation/service/service.go @@ -2,8 +2,6 @@ package service import ( "context" - "time" - "github.com/cortezaproject/corteza-server/automation/automation" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/corredor" @@ -12,9 +10,9 @@ import ( "github.com/cortezaproject/corteza-server/pkg/objstore" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/cortezaproject/corteza-server/store" - "github.com/cortezaproject/corteza-server/system/types" sysTypes "github.com/cortezaproject/corteza-server/system/types" "go.uber.org/zap" + "time" ) type ( @@ -90,7 +88,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock DefaultActionlog = actionlog.NewService(DefaultStore, log, tee, policy) } - DefaultAccessControl = AccessControl(RolesForUser(s)) + DefaultAccessControl = AccessControl(s) DefaultSession = Session(DefaultLogger.Named("session"), c.Workflow, ws) DefaultWorkflow = Workflow(DefaultLogger.Named("workflow"), c.Corredor, c.Workflow) @@ -164,19 +162,3 @@ func isStale(new *time.Time, updatedAt *time.Time, createdAt time.Time) bool { func trim1st(_ interface{}, err error) error { return err } - -// @note copied over from system/service/role@RolesForUser -func RolesForUser(s store.Storer) func(ctx context.Context, userID uint64) ([]uint64, error) { - return func(ctx context.Context, userID uint64) ([]uint64, error) { - rr, _, err := store.SearchRoles(ctx, s, types.RoleFilter{MemberID: userID}) - if err != nil { - return nil, err - } - - out := make([]uint64, len(rr)) - for i, r := range rr { - out[i] = r.ID - } - return out, nil - } -} diff --git a/codegen/assets/templates/gocode/rbac/$component_access_control.go.tpl b/codegen/assets/templates/gocode/rbac/$component_access_control.go.tpl index cd2a39f75..dafaf116a 100644 --- a/codegen/assets/templates/gocode/rbac/$component_access_control.go.tpl +++ b/codegen/assets/templates/gocode/rbac/$component_access_control.go.tpl @@ -9,6 +9,7 @@ import ( "context" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/pkg/actionlog" + systemTypes "github.com/cortezaproject/corteza-server/system/types" {{- range .imports }} {{ . }} {{- end }} @@ -16,33 +17,40 @@ import ( type ( + roleMemberSearcher interface { + SearchRoleMembers(context.Context, systemTypes.RoleMemberFilter) (systemTypes.RoleMemberSet, systemTypes.RoleMemberFilter, error) + } + + rbacService interface { + Evaluate(rbac.Session, string, rbac.Resource) rbac.Evaluated + Grant(context.Context, ...*rbac.Rule) error + FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet) + CloneRulesByRoleID(ctx context.Context, fromRoleID uint64, toRoleID ...uint64) error + } + accessControl struct { actionlog actionlog.Recorder - roleFinder func(ctx context.Context, id uint64) ([]uint64, error) - rbac interface { - Evaluate(rbac.Session, string, rbac.Resource) rbac.Evaluated - Grant(context.Context, ...*rbac.Rule) error - FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet) - CloneRulesByRoleID(ctx context.Context, fromRoleID uint64, toRoleID ...uint64) error - } + store roleMemberSearcher + rbac rbacService } ) -func AccessControl(rf func(ctx context.Context, id uint64) ([]uint64, error)) *accessControl { +func AccessControl(rms roleMemberSearcher) *accessControl { return &accessControl{ - roleFinder: rf, - rbac: rbac.Global(), - actionlog: DefaultActionlog, + store: rms, + rbac: rbac.Global(), + actionlog: DefaultActionlog, } } - func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) bool { return svc.rbac.Evaluate(rbac.ContextToSession(ctx), op, res).Can } // Effective returns a list of effective permissions for all given resource +// +// This function is auto-generated func (svc accessControl) Effective(ctx context.Context, rr ... rbac.Resource) (ee rbac.EffectiveSet) { for _, res := range rr { r := res.RbacResource() @@ -55,38 +63,75 @@ func (svc accessControl) Effective(ctx context.Context, rr ... rbac.Resource) (e } // Evaluate returns a list of permissions evaluated for the given user/roles combo -func (svc accessControl) Evaluate(ctx context.Context, user uint64, roles []uint64, rr ...rbac.Resource) (ee rbac.EvaluatedSet, err error) { +// +// This function is auto-generated +func (svc accessControl) Evaluate(ctx context.Context, userID uint64, roles []uint64, rr ...string) (ee rbac.EvaluatedSet, err error) { // Reusing the grant permission since this is who the feature is for if !svc.CanGrant(ctx) { // @todo should be altered to check grant permissions PER resource return nil, AccessControlErrNotAllowedToSetPermissions() } - // Load roles for this user - // - // User's roles take priority over specified ones - if user != 0 { - rr, err := svc.roleFinder(ctx, user) + var ( + resources []rbac.Resource + members systemTypes.RoleMemberSet + ) + if len(rr) > 0 { + resources = make([]rbac.Resource, 0, len(rr)) + for _, r := range rr { + resources = append(resources, rbac.NewResource(r)) + } + } else { + resources = svc.Resources() + } + + // User ID specified, load its roles + if userID != 0 { + if len(roles) > 0 { + // should be prevented on the client + return nil, fmt.Errorf("userID and roles are mutually exclusive") + } + + members, _, err = svc.store.SearchRoleMembers(ctx, systemTypes.RoleMemberFilter{UserID: userID}) if err != nil { return nil, err } - roles = append(rr, roles...) + for _, m := range members { + roles = append(roles, m.RoleID) + } } - session := rbac.ParamsToSession(ctx, user, roles...) - for _, res := range rr { + if len(roles) == 0 { + // should be prevented on the client + return nil, fmt.Errorf("no roles specified") + } + + session := rbac.ParamsToSession(ctx, userID, roles...) + for _, res := range resources { r := res.RbacResource() for op := range rbacResourceOperations(r) { - eval := svc.rbac.Evaluate(session, op, res) - - ee = append(ee, eval) + ee = append(ee, svc.rbac.Evaluate(session, op, res)) } } return } +// Resources returns list of resources +// +// This function is auto-generated +func (svc accessControl) Resources() []rbac.Resource { + return []rbac.Resource{ + {{- range .resources }} + rbac.NewResource({{ .resFunc }}({{ range .references }}0,{{ end }})), + {{- end }} + } +} + +// List returns list of operations on all resources +// +// This function is auto-generated func (svc accessControl) List() (out []map[string]string) { def := []map[string]string{ {{- range .operations }} diff --git a/codegen/server.rbac-access_control.cue b/codegen/server.rbac-access_control.cue index 237e447f6..172fd6042 100644 --- a/codegen/server.rbac-access_control.cue +++ b/codegen/server.rbac-access_control.cue @@ -16,6 +16,18 @@ import ( "\"github.com/cortezaproject/corteza-server/\(cmp.ident)/types\"", ] + // All known RBAC resources + resources: [ + for res in cmp.resources if res.rbac != _|_ { + resFunc: "types.\(res.expIdent)RbacResource" + references: [ for p in res.parents {p}, {param: "id", refField: "ID"}] + }, + { + resFunc: "types.ComponentRbacResource" + component: true + }, + ] + // All possible RBAC operations on component and resources // flattened operations: [ diff --git a/compose/rest/permissions.go b/compose/rest/permissions.go index dcc98682b..ab7dae5e8 100644 --- a/compose/rest/permissions.go +++ b/compose/rest/permissions.go @@ -15,13 +15,9 @@ type ( ac permissionsAccessController } - rbacResWrap struct { - res string - } - permissionsAccessController interface { Effective(context.Context, ...rbac.Resource) rbac.EffectiveSet - Evaluate(ctx context.Context, user uint64, roles []uint64, rr ...rbac.Resource) (ee rbac.EvaluatedSet, err error) + Evaluate(ctx context.Context, user uint64, roles []uint64, rr ...string) (ee rbac.EvaluatedSet, err error) List() []map[string]string FindRulesByRoleID(context.Context, uint64) (rbac.RuleSet, error) Grant(ctx context.Context, rr ...*rbac.Rule) error @@ -39,12 +35,7 @@ func (ctrl Permissions) Effective(ctx context.Context, r *request.PermissionsEff } func (ctrl Permissions) Evaluate(ctx context.Context, r *request.PermissionsEvaluate) (interface{}, error) { - in := make([]rbac.Resource, 0, len(r.Resource)) - for _, res := range r.Resource { - in = append(in, rbacResWrap{res: res}) - } - - return ctrl.ac.Evaluate(ctx, r.UserID, r.RoleID, in...) + return ctrl.ac.Evaluate(ctx, r.UserID, r.RoleID, r.Resource...) } func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (interface{}, error) { @@ -76,7 +67,3 @@ func (ctrl Permissions) Update(ctx context.Context, r *request.PermissionsUpdate return api.OK(), ctrl.ac.Grant(ctx, r.Rules...) } - -func (ar rbacResWrap) RbacResource() string { - return ar.res -} diff --git a/compose/service/access_control.gen.go b/compose/service/access_control.gen.go index 9716ca877..fac000f1a 100644 --- a/compose/service/access_control.gen.go +++ b/compose/service/access_control.gen.go @@ -12,29 +12,36 @@ import ( "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/rbac" + systemTypes "github.com/cortezaproject/corteza-server/system/types" "github.com/spf13/cast" "strings" ) type ( + roleMemberSearcher interface { + SearchRoleMembers(context.Context, systemTypes.RoleMemberFilter) (systemTypes.RoleMemberSet, systemTypes.RoleMemberFilter, error) + } + + rbacService interface { + Evaluate(rbac.Session, string, rbac.Resource) rbac.Evaluated + Grant(context.Context, ...*rbac.Rule) error + FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet) + CloneRulesByRoleID(ctx context.Context, fromRoleID uint64, toRoleID ...uint64) error + } + accessControl struct { actionlog actionlog.Recorder - roleFinder func(ctx context.Context, id uint64) ([]uint64, error) - rbac interface { - Evaluate(rbac.Session, string, rbac.Resource) rbac.Evaluated - Grant(context.Context, ...*rbac.Rule) error - FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet) - CloneRulesByRoleID(ctx context.Context, fromRoleID uint64, toRoleID ...uint64) error - } + store roleMemberSearcher + rbac rbacService } ) -func AccessControl(rf func(ctx context.Context, id uint64) ([]uint64, error)) *accessControl { +func AccessControl(rms roleMemberSearcher) *accessControl { return &accessControl{ - roleFinder: rf, - rbac: rbac.Global(), - actionlog: DefaultActionlog, + store: rms, + rbac: rbac.Global(), + actionlog: DefaultActionlog, } } @@ -43,6 +50,8 @@ func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) } // Effective returns a list of effective permissions for all given resource +// +// This function is auto-generated func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee rbac.EffectiveSet) { for _, res := range rr { r := res.RbacResource() @@ -55,38 +64,79 @@ func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee } // Evaluate returns a list of permissions evaluated for the given user/roles combo -func (svc accessControl) Evaluate(ctx context.Context, user uint64, roles []uint64, rr ...rbac.Resource) (ee rbac.EvaluatedSet, err error) { +// +// This function is auto-generated +func (svc accessControl) Evaluate(ctx context.Context, userID uint64, roles []uint64, rr ...string) (ee rbac.EvaluatedSet, err error) { // Reusing the grant permission since this is who the feature is for if !svc.CanGrant(ctx) { // @todo should be altered to check grant permissions PER resource return nil, AccessControlErrNotAllowedToSetPermissions() } - // Load roles for this user - // - // User's roles take priority over specified ones - if user != 0 { - rr, err := svc.roleFinder(ctx, user) + var ( + resources []rbac.Resource + members systemTypes.RoleMemberSet + ) + if len(rr) > 0 { + resources = make([]rbac.Resource, 0, len(rr)) + for _, r := range rr { + resources = append(resources, rbac.NewResource(r)) + } + } else { + resources = svc.Resources() + } + + // User ID specified, load its roles + if userID != 0 { + if len(roles) > 0 { + // should be prevented on the client + return nil, fmt.Errorf("userID and roles are mutually exclusive") + } + + members, _, err = svc.store.SearchRoleMembers(ctx, systemTypes.RoleMemberFilter{UserID: userID}) if err != nil { return nil, err } - roles = append(rr, roles...) + for _, m := range members { + roles = append(roles, m.RoleID) + } } - session := rbac.ParamsToSession(ctx, user, roles...) - for _, res := range rr { + if len(roles) == 0 { + // should be prevented on the client + return nil, fmt.Errorf("no roles specified") + } + + session := rbac.ParamsToSession(ctx, userID, roles...) + for _, res := range resources { r := res.RbacResource() for op := range rbacResourceOperations(r) { - eval := svc.rbac.Evaluate(session, op, res) - - ee = append(ee, eval) + ee = append(ee, svc.rbac.Evaluate(session, op, res)) } } return } +// Resources returns list of resources +// +// This function is auto-generated +func (svc accessControl) Resources() []rbac.Resource { + return []rbac.Resource{ + rbac.NewResource(types.ChartRbacResource(0, 0)), + rbac.NewResource(types.ModuleRbacResource(0, 0)), + rbac.NewResource(types.ModuleFieldRbacResource(0, 0, 0)), + rbac.NewResource(types.NamespaceRbacResource(0)), + rbac.NewResource(types.PageRbacResource(0, 0)), + rbac.NewResource(types.RecordRbacResource(0, 0, 0)), + rbac.NewResource(types.ComponentRbacResource()), + } +} + +// List returns list of operations on all resources +// +// This function is auto-generated func (svc accessControl) List() (out []map[string]string) { def := []map[string]string{ { diff --git a/compose/service/service.go b/compose/service/service.go index da3a342f4..1b8b5c436 100644 --- a/compose/service/service.go +++ b/compose/service/service.go @@ -10,7 +10,6 @@ import ( "github.com/cortezaproject/corteza-server/pkg/discovery" automationService "github.com/cortezaproject/corteza-server/automation/service" - systemService "github.com/cortezaproject/corteza-server/automation/service" "github.com/cortezaproject/corteza-server/compose/automation" "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/actionlog" @@ -127,7 +126,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, c Config) } } - DefaultAccessControl = AccessControl(systemService.RolesForUser(s)) + DefaultAccessControl = AccessControl(s) DefaultResourceTranslation = ResourceTranslationsManager(locale.Global()) if DefaultObjectStore == nil { diff --git a/federation/rest/permissions.go b/federation/rest/permissions.go index a650a0bb7..9c9a2e7d3 100644 --- a/federation/rest/permissions.go +++ b/federation/rest/permissions.go @@ -15,13 +15,9 @@ type ( ac permissionsAccessController } - rbacResWrap struct { - res string - } - permissionsAccessController interface { Effective(context.Context, ...rbac.Resource) rbac.EffectiveSet - Evaluate(ctx context.Context, user uint64, roles []uint64, rr ...rbac.Resource) (ee rbac.EvaluatedSet, err error) + Evaluate(ctx context.Context, user uint64, roles []uint64, rr ...string) (ee rbac.EvaluatedSet, err error) List() []map[string]string FindRulesByRoleID(context.Context, uint64) (rbac.RuleSet, error) Grant(ctx context.Context, rr ...*rbac.Rule) error @@ -39,12 +35,7 @@ func (ctrl Permissions) Effective(ctx context.Context, r *request.PermissionsEff } func (ctrl Permissions) Evaluate(ctx context.Context, r *request.PermissionsEvaluate) (interface{}, error) { - in := make([]rbac.Resource, 0, len(r.Resource)) - for _, res := range r.Resource { - in = append(in, rbacResWrap{res: res}) - } - - return ctrl.ac.Evaluate(ctx, r.UserID, r.RoleID, in...) + return ctrl.ac.Evaluate(ctx, r.UserID, r.RoleID, r.Resource...) } func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (interface{}, error) { @@ -76,7 +67,3 @@ func (ctrl Permissions) Update(ctx context.Context, r *request.PermissionsUpdate return api.OK(), ctrl.ac.Grant(ctx, r.Rules...) } - -func (ar rbacResWrap) RbacResource() string { - return ar.res -} diff --git a/federation/service/access_control.gen.go b/federation/service/access_control.gen.go index f4cdb892b..b2c0891e4 100644 --- a/federation/service/access_control.gen.go +++ b/federation/service/access_control.gen.go @@ -12,29 +12,36 @@ import ( "github.com/cortezaproject/corteza-server/federation/types" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/rbac" + systemTypes "github.com/cortezaproject/corteza-server/system/types" "github.com/spf13/cast" "strings" ) type ( + roleMemberSearcher interface { + SearchRoleMembers(context.Context, systemTypes.RoleMemberFilter) (systemTypes.RoleMemberSet, systemTypes.RoleMemberFilter, error) + } + + rbacService interface { + Evaluate(rbac.Session, string, rbac.Resource) rbac.Evaluated + Grant(context.Context, ...*rbac.Rule) error + FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet) + CloneRulesByRoleID(ctx context.Context, fromRoleID uint64, toRoleID ...uint64) error + } + accessControl struct { actionlog actionlog.Recorder - roleFinder func(ctx context.Context, id uint64) ([]uint64, error) - rbac interface { - Evaluate(rbac.Session, string, rbac.Resource) rbac.Evaluated - Grant(context.Context, ...*rbac.Rule) error - FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet) - CloneRulesByRoleID(ctx context.Context, fromRoleID uint64, toRoleID ...uint64) error - } + store roleMemberSearcher + rbac rbacService } ) -func AccessControl(rf func(ctx context.Context, id uint64) ([]uint64, error)) *accessControl { +func AccessControl(rms roleMemberSearcher) *accessControl { return &accessControl{ - roleFinder: rf, - rbac: rbac.Global(), - actionlog: DefaultActionlog, + store: rms, + rbac: rbac.Global(), + actionlog: DefaultActionlog, } } @@ -43,6 +50,8 @@ func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) } // Effective returns a list of effective permissions for all given resource +// +// This function is auto-generated func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee rbac.EffectiveSet) { for _, res := range rr { r := res.RbacResource() @@ -55,38 +64,76 @@ func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee } // Evaluate returns a list of permissions evaluated for the given user/roles combo -func (svc accessControl) Evaluate(ctx context.Context, user uint64, roles []uint64, rr ...rbac.Resource) (ee rbac.EvaluatedSet, err error) { +// +// This function is auto-generated +func (svc accessControl) Evaluate(ctx context.Context, userID uint64, roles []uint64, rr ...string) (ee rbac.EvaluatedSet, err error) { // Reusing the grant permission since this is who the feature is for if !svc.CanGrant(ctx) { // @todo should be altered to check grant permissions PER resource return nil, AccessControlErrNotAllowedToSetPermissions() } - // Load roles for this user - // - // User's roles take priority over specified ones - if user != 0 { - rr, err := svc.roleFinder(ctx, user) + var ( + resources []rbac.Resource + members systemTypes.RoleMemberSet + ) + if len(rr) > 0 { + resources = make([]rbac.Resource, 0, len(rr)) + for _, r := range rr { + resources = append(resources, rbac.NewResource(r)) + } + } else { + resources = svc.Resources() + } + + // User ID specified, load its roles + if userID != 0 { + if len(roles) > 0 { + // should be prevented on the client + return nil, fmt.Errorf("userID and roles are mutually exclusive") + } + + members, _, err = svc.store.SearchRoleMembers(ctx, systemTypes.RoleMemberFilter{UserID: userID}) if err != nil { return nil, err } - roles = append(rr, roles...) + for _, m := range members { + roles = append(roles, m.RoleID) + } } - session := rbac.ParamsToSession(ctx, user, roles...) - for _, res := range rr { + if len(roles) == 0 { + // should be prevented on the client + return nil, fmt.Errorf("no roles specified") + } + + session := rbac.ParamsToSession(ctx, userID, roles...) + for _, res := range resources { r := res.RbacResource() for op := range rbacResourceOperations(r) { - eval := svc.rbac.Evaluate(session, op, res) - - ee = append(ee, eval) + ee = append(ee, svc.rbac.Evaluate(session, op, res)) } } return } +// Resources returns list of resources +// +// This function is auto-generated +func (svc accessControl) Resources() []rbac.Resource { + return []rbac.Resource{ + rbac.NewResource(types.NodeRbacResource(0)), + rbac.NewResource(types.ExposedModuleRbacResource(0, 0)), + rbac.NewResource(types.SharedModuleRbacResource(0, 0)), + rbac.NewResource(types.ComponentRbacResource()), + } +} + +// List returns list of operations on all resources +// +// This function is auto-generated func (svc accessControl) List() (out []map[string]string) { def := []map[string]string{ { diff --git a/federation/service/service.go b/federation/service/service.go index 6d1cc28e0..10f68984c 100644 --- a/federation/service/service.go +++ b/federation/service/service.go @@ -2,21 +2,18 @@ package service import ( "context" - "time" - - "github.com/cortezaproject/corteza-server/pkg/logger" - cs "github.com/cortezaproject/corteza-server/compose/service" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/id" "github.com/cortezaproject/corteza-server/pkg/label" + "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/cortezaproject/corteza-server/store" - "github.com/cortezaproject/corteza-server/system/service" ss "github.com/cortezaproject/corteza-server/system/service" "github.com/cortezaproject/corteza-server/system/types" "go.uber.org/zap" + "time" ) type ( @@ -85,11 +82,11 @@ func Initialize(_ context.Context, log *zap.Logger, s store.Storer, c Config) (e DefaultActionlog = actionlog.NewService(DefaultStore, log, tee, policy) } - DefaultAccessControl = AccessControl(service.RolesForUser(s)) + DefaultAccessControl = AccessControl(s) DefaultNode = Node( DefaultStore, - service.DefaultUser, + ss.DefaultUser, DefaultActionlog, func(ctx context.Context, i auth.Identifiable) (token []byte, err error) { return auth.TokenIssuer.Issue( diff --git a/pkg/envoy/store/compose.go b/pkg/envoy/store/compose.go index 4224e691b..339daa453 100644 --- a/pkg/envoy/store/compose.go +++ b/pkg/envoy/store/compose.go @@ -178,7 +178,7 @@ func (d *composeDecoder) decodeComposeRecord(ctx context.Context, s store.Storer } } - ac := service.AccessControl(nil) + ac := service.AccessControl(s) if len(d.namespaceID) > 0 { ffNs := make([]*composeRecordFilter, 0, len(ff)+len(d.namespaceID)) diff --git a/pkg/rbac/effective.go b/pkg/rbac/effective.go index 36829d6a0..7a9965bf2 100644 --- a/pkg/rbac/effective.go +++ b/pkg/rbac/effective.go @@ -8,29 +8,6 @@ type ( } EffectiveSet []effective - - Evaluated struct { - Resource string `json:"resource"` - Operation string `json:"operation"` - Access Access `json:"-"` - Can bool `json:"can"` - Step explanation `json:"step"` - - RoleID uint64 `json:"roleID,string,omitempty"` - Rule *Rule `json:"rule,omitempty"` - - Default *Evaluated `json:"default,omitempty"` - } - EvaluatedSet []Evaluated - - explanation string -) - -const ( - stepIntegrity explanation = "integrity" - stepBypass explanation = "bypass" - stepRuleless explanation = "ruleless" - stepEvaluated explanation = "evaluated" ) func (ee *EffectiveSet) Push(res, op string, allow bool) { diff --git a/pkg/rbac/evaluate.go b/pkg/rbac/evaluate.go new file mode 100644 index 000000000..7dda038e4 --- /dev/null +++ b/pkg/rbac/evaluate.go @@ -0,0 +1,27 @@ +package rbac + +type ( + Evaluated struct { + Resource string `json:"resource"` + Operation string `json:"operation"` + Access Access `json:"-"` + Can bool `json:"can"` + Step explanation `json:"step"` + + RoleID uint64 `json:"roleID,string,omitempty"` + Rule *Rule `json:"rule,omitempty"` + + Default *Evaluated `json:"default,omitempty"` + } + + EvaluatedSet []Evaluated + + explanation string +) + +const ( + stepIntegrity explanation = "integrity" + stepBypass explanation = "bypass" + stepRuleless explanation = "ruleless" + stepEvaluated explanation = "evaluated" +) diff --git a/pkg/rbac/resource.go b/pkg/rbac/resource.go index 47a700296..83afa2b41 100644 --- a/pkg/rbac/resource.go +++ b/pkg/rbac/resource.go @@ -6,6 +6,7 @@ import ( ) type ( + resource string Resource interface { RbacResource() string } @@ -22,6 +23,19 @@ const ( wildcard = "*" ) +// NewResource constructs untyped resource from the given string +// +// This is a utility method that should not be used for standard permission checking and granting +// it's intended to be used for testing end permission evaluation where we do not have access to the resource struct +func NewResource(s string) Resource { + return resource(s) +} + +// RbacResource returns string of an untyped resource +func (t resource) RbacResource() string { + return string(t) +} + // ResourceType extracts 1st part of the resource // // ns::cmp:res/c returns ns::cmp:res diff --git a/pkg/rbac/roles_test.go b/pkg/rbac/roles_test.go index 2fd04cd1e..5ab539179 100644 --- a/pkg/rbac/roles_test.go +++ b/pkg/rbac/roles_test.go @@ -6,14 +6,6 @@ import ( "github.com/stretchr/testify/require" ) -type ( - testResource string -) - -func (t testResource) RbacResource() string { - return string(t) -} - func Test_partitionRoles(t *testing.T) { var ( req = require.New(t) @@ -49,7 +41,7 @@ func Test_getContextRoles(t *testing.T) { } } - tres = testResource("testResource") + tres = NewResource("testResource") tcc = []struct { name string diff --git a/pkg/rbac/service.go b/pkg/rbac/service.go index f5b342747..876fdd346 100644 --- a/pkg/rbac/service.go +++ b/pkg/rbac/service.go @@ -110,7 +110,7 @@ func (svc *service) Check(ses Session, op string, res Resource) (v Access) { return access } -// Eval evaluates access for the given parameters +// Evaluate access for the given session, operation and resource // // The evaluation outputs verbose details to assist the UI. func (svc *service) Evaluate(ses Session, op string, res Resource) Evaluated { @@ -321,7 +321,7 @@ func (svc *service) SignificantRoles(res Resource, op string) (aRR, dRR []uint64 return svc.rules.sigRoles(res.RbacResource(), op) } -func (svc service) String() (out string) { +func (svc *service) String() (out string) { tpl := "%-5v %-20s to %-20s %-30s\n" out += strings.Repeat("-", 120) + "\n" diff --git a/system/rest/permissions.go b/system/rest/permissions.go index 2b461572b..24dc65b09 100644 --- a/system/rest/permissions.go +++ b/system/rest/permissions.go @@ -16,13 +16,9 @@ type ( ac permissionsAccessController } - rbacResWrap struct { - res string - } - permissionsAccessController interface { Effective(context.Context, ...rbac.Resource) rbac.EffectiveSet - Evaluate(ctx context.Context, user uint64, roles []uint64, rr ...rbac.Resource) (ee rbac.EvaluatedSet, err error) + Evaluate(ctx context.Context, user uint64, roles []uint64, rr ...string) (ee rbac.EvaluatedSet, err error) List() []map[string]string FindRulesByRoleID(context.Context, uint64) (rbac.RuleSet, error) CloneRulesByRoleID(ctx context.Context, roleID uint64, toRoleID ...uint64) error @@ -41,12 +37,7 @@ func (ctrl Permissions) Effective(ctx context.Context, r *request.PermissionsEff } func (ctrl Permissions) Evaluate(ctx context.Context, r *request.PermissionsEvaluate) (interface{}, error) { - in := make([]rbac.Resource, 0, len(r.Resource)) - for _, res := range r.Resource { - in = append(in, rbacResWrap{res: res}) - } - - return ctrl.ac.Evaluate(ctx, r.UserID, r.RoleID, in...) + return ctrl.ac.Evaluate(ctx, r.UserID, r.RoleID, r.Resource...) } func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (interface{}, error) { @@ -79,11 +70,10 @@ func (ctrl Permissions) Update(ctx context.Context, r *request.PermissionsUpdate return api.OK(), ctrl.ac.Grant(ctx, r.Rules...) } +// Clone all RBAC rules on ALL components (not just system) +// +// @todo needs to be moved under roles func (ctrl Permissions) Clone(ctx context.Context, r *request.PermissionsClone) (interface{}, error) { // Clone rules from role S to role T return api.OK(), ctrl.ac.CloneRulesByRoleID(ctx, r.RoleID, payload.ParseUint64s(r.CloneToRoleID)...) } - -func (ar rbacResWrap) RbacResource() string { - return ar.res -} diff --git a/system/service/access_control.gen.go b/system/service/access_control.gen.go index 69393b4a6..12872c891 100644 --- a/system/service/access_control.gen.go +++ b/system/service/access_control.gen.go @@ -12,29 +12,36 @@ import ( "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/system/types" + systemTypes "github.com/cortezaproject/corteza-server/system/types" "github.com/spf13/cast" "strings" ) type ( + roleMemberSearcher interface { + SearchRoleMembers(context.Context, systemTypes.RoleMemberFilter) (systemTypes.RoleMemberSet, systemTypes.RoleMemberFilter, error) + } + + rbacService interface { + Evaluate(rbac.Session, string, rbac.Resource) rbac.Evaluated + Grant(context.Context, ...*rbac.Rule) error + FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet) + CloneRulesByRoleID(ctx context.Context, fromRoleID uint64, toRoleID ...uint64) error + } + accessControl struct { actionlog actionlog.Recorder - roleFinder func(ctx context.Context, id uint64) ([]uint64, error) - rbac interface { - Evaluate(rbac.Session, string, rbac.Resource) rbac.Evaluated - Grant(context.Context, ...*rbac.Rule) error - FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet) - CloneRulesByRoleID(ctx context.Context, fromRoleID uint64, toRoleID ...uint64) error - } + store roleMemberSearcher + rbac rbacService } ) -func AccessControl(rf func(ctx context.Context, id uint64) ([]uint64, error)) *accessControl { +func AccessControl(rms roleMemberSearcher) *accessControl { return &accessControl{ - roleFinder: rf, - rbac: rbac.Global(), - actionlog: DefaultActionlog, + store: rms, + rbac: rbac.Global(), + actionlog: DefaultActionlog, } } @@ -43,6 +50,8 @@ func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) } // Effective returns a list of effective permissions for all given resource +// +// This function is auto-generated func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee rbac.EffectiveSet) { for _, res := range rr { r := res.RbacResource() @@ -55,38 +64,86 @@ func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee } // Evaluate returns a list of permissions evaluated for the given user/roles combo -func (svc accessControl) Evaluate(ctx context.Context, user uint64, roles []uint64, rr ...rbac.Resource) (ee rbac.EvaluatedSet, err error) { +// +// This function is auto-generated +func (svc accessControl) Evaluate(ctx context.Context, userID uint64, roles []uint64, rr ...string) (ee rbac.EvaluatedSet, err error) { // Reusing the grant permission since this is who the feature is for if !svc.CanGrant(ctx) { // @todo should be altered to check grant permissions PER resource return nil, AccessControlErrNotAllowedToSetPermissions() } - // Load roles for this user - // - // User's roles take priority over specified ones - if user != 0 { - rr, err := svc.roleFinder(ctx, user) + var ( + resources []rbac.Resource + members systemTypes.RoleMemberSet + ) + if len(rr) > 0 { + resources = make([]rbac.Resource, 0, len(rr)) + for _, r := range rr { + resources = append(resources, rbac.NewResource(r)) + } + } else { + resources = svc.Resources() + } + + // User ID specified, load its roles + if userID != 0 { + if len(roles) > 0 { + // should be prevented on the client + return nil, fmt.Errorf("userID and roles are mutually exclusive") + } + + members, _, err = svc.store.SearchRoleMembers(ctx, systemTypes.RoleMemberFilter{UserID: userID}) if err != nil { return nil, err } - roles = append(rr, roles...) + for _, m := range members { + roles = append(roles, m.RoleID) + } } - session := rbac.ParamsToSession(ctx, user, roles...) - for _, res := range rr { + if len(roles) == 0 { + // should be prevented on the client + return nil, fmt.Errorf("no roles specified") + } + + session := rbac.ParamsToSession(ctx, userID, roles...) + for _, res := range resources { r := res.RbacResource() for op := range rbacResourceOperations(r) { - eval := svc.rbac.Evaluate(session, op, res) - - ee = append(ee, eval) + ee = append(ee, svc.rbac.Evaluate(session, op, res)) } } return } +// Resources returns list of resources +// +// This function is auto-generated +func (svc accessControl) Resources() []rbac.Resource { + return []rbac.Resource{ + rbac.NewResource(types.ApplicationRbacResource(0)), + rbac.NewResource(types.ApigwRouteRbacResource(0)), + rbac.NewResource(types.AuthClientRbacResource(0)), + rbac.NewResource(types.DataPrivacyRequestRbacResource(0)), + rbac.NewResource(types.DataPrivacyRequestCommentRbacResource(0)), + rbac.NewResource(types.QueueRbacResource(0)), + rbac.NewResource(types.QueueMessageRbacResource(0)), + rbac.NewResource(types.ReportRbacResource(0)), + rbac.NewResource(types.RoleRbacResource(0)), + rbac.NewResource(types.TemplateRbacResource(0)), + rbac.NewResource(types.UserRbacResource(0)), + rbac.NewResource(types.DalConnectionRbacResource(0)), + rbac.NewResource(types.DalSensitivityLevelRbacResource(0)), + rbac.NewResource(types.ComponentRbacResource()), + } +} + +// List returns list of operations on all resources +// +// This function is auto-generated func (svc accessControl) List() (out []map[string]string) { def := []map[string]string{ { diff --git a/system/service/service.go b/system/service/service.go index 3e3cf4358..4c81c6306 100644 --- a/system/service/service.go +++ b/system/service/service.go @@ -147,7 +147,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, primaryCon } } - DefaultAccessControl = AccessControl(RolesForUser(s)) + DefaultAccessControl = AccessControl(s) DefaultSettings = Settings(ctx, DefaultStore, DefaultLogger, DefaultAccessControl, CurrentSettings)