From 83ba7faa0fc3969d81eb6ebfcf8f50dc2c3220c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Jerman?= Date: Tue, 21 Jun 2022 15:53:27 +0200 Subject: [PATCH] Add /permissions/evaluate endpoints to evaluate RBAC rules --- automation/rest.yaml | 16 ++++ automation/rest/handlers/permissions.go | 19 ++++ automation/rest/permissions.go | 19 ++++ automation/rest/request/permissions.go | 86 +++++++++++++++++++ automation/service/access_control.gen.go | 47 ++++++++-- automation/service/service.go | 19 +++- .../rbac/$component_access_control.go.tpl | 45 ++++++++-- compose/rest.yaml | 16 ++++ compose/rest/handlers/permissions.go | 19 ++++ compose/rest/permissions.go | 19 ++++ compose/rest/request/permissions.go | 86 +++++++++++++++++++ compose/service/access_control.gen.go | 47 ++++++++-- compose/service/service.go | 3 +- federation/rest.yaml | 16 ++++ federation/rest/handlers/permissions.go | 19 ++++ federation/rest/permissions.go | 19 ++++ federation/rest/request/permissions.go | 86 +++++++++++++++++++ federation/service/access_control.gen.go | 47 ++++++++-- federation/service/service.go | 2 +- pkg/envoy/store/compose.go | 2 +- pkg/rbac/effective.go | 23 +++++ pkg/rbac/ruleset_checks.go | 40 ++++++--- pkg/rbac/ruleset_checks_test.go | 3 +- pkg/rbac/service.go | 69 +++++++++++++-- pkg/rbac/session.go | 8 ++ system/rest.yaml | 16 ++++ system/rest/handlers/permissions.go | 19 ++++ system/rest/permissions.go | 19 ++++ system/rest/request/permissions.go | 86 +++++++++++++++++++ system/service/access_control.gen.go | 47 ++++++++-- system/service/role.go | 15 ++++ system/service/service.go | 2 +- 32 files changed, 925 insertions(+), 54 deletions(-) diff --git a/automation/rest.yaml b/automation/rest.yaml index 79c9c8a8b..ea7d0680f 100644 --- a/automation/rest.yaml +++ b/automation/rest.yaml @@ -292,6 +292,22 @@ endpoints: type: string required: false title: Show only rules for a specific resource + - name: evaluate + path: "/evaluate" + method: GET + title: Evaluate rules for given user/role combo + parameters: + get: + - name: resource + type: "[]string" + required: false + title: Show only rules for a specific resource + - name: userID + type: uint64 + required: false + - name: roleID + type: "[]uint64" + required: false - name: read path: "/{roleID}/rules" method: GET diff --git a/automation/rest/handlers/permissions.go b/automation/rest/handlers/permissions.go index 9dffe0340..615b9c658 100644 --- a/automation/rest/handlers/permissions.go +++ b/automation/rest/handlers/permissions.go @@ -21,6 +21,7 @@ type ( PermissionsAPI interface { List(context.Context, *request.PermissionsList) (interface{}, error) Effective(context.Context, *request.PermissionsEffective) (interface{}, error) + Evaluate(context.Context, *request.PermissionsEvaluate) (interface{}, error) Read(context.Context, *request.PermissionsRead) (interface{}, error) Delete(context.Context, *request.PermissionsDelete) (interface{}, error) Update(context.Context, *request.PermissionsUpdate) (interface{}, error) @@ -30,6 +31,7 @@ type ( Permissions struct { List func(http.ResponseWriter, *http.Request) Effective func(http.ResponseWriter, *http.Request) + Evaluate func(http.ResponseWriter, *http.Request) Read func(http.ResponseWriter, *http.Request) Delete func(http.ResponseWriter, *http.Request) Update func(http.ResponseWriter, *http.Request) @@ -70,6 +72,22 @@ func NewPermissions(h PermissionsAPI) *Permissions { api.Send(w, r, value) }, + Evaluate: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewPermissionsEvaluate() + if err := params.Fill(r); err != nil { + api.Send(w, r, err) + return + } + + value, err := h.Evaluate(r.Context(), params) + if err != nil { + api.Send(w, r, err) + return + } + + api.Send(w, r, value) + }, Read: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewPermissionsRead() @@ -126,6 +144,7 @@ func (h Permissions) MountRoutes(r chi.Router, middlewares ...func(http.Handler) r.Use(middlewares...) r.Get("/permissions/", h.List) r.Get("/permissions/effective", h.Effective) + r.Get("/permissions/evaluate", h.Evaluate) r.Get("/permissions/{roleID}/rules", h.Read) r.Delete("/permissions/{roleID}/rules", h.Delete) r.Patch("/permissions/{roleID}/rules", h.Update) diff --git a/automation/rest/permissions.go b/automation/rest/permissions.go index d384c36e9..73229ff82 100644 --- a/automation/rest/permissions.go +++ b/automation/rest/permissions.go @@ -2,6 +2,7 @@ package rest import ( "context" + "github.com/cortezaproject/corteza-server/automation/rest/request" "github.com/cortezaproject/corteza-server/automation/service" "github.com/cortezaproject/corteza-server/automation/types" @@ -14,8 +15,13 @@ 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) List() []map[string]string FindRulesByRoleID(context.Context, uint64) (rbac.RuleSet, error) Grant(ctx context.Context, rr ...*rbac.Rule) error @@ -32,6 +38,15 @@ func (ctrl Permissions) Effective(ctx context.Context, r *request.PermissionsEff return ctrl.ac.Effective(ctx, types.Component{}), nil } +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...) +} + func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (interface{}, error) { return ctrl.ac.List(), nil } @@ -61,3 +76,7 @@ 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/rest/request/permissions.go b/automation/rest/request/permissions.go index 378eb2737..12b760af9 100644 --- a/automation/rest/request/permissions.go +++ b/automation/rest/request/permissions.go @@ -44,6 +44,23 @@ type ( Resource string } + PermissionsEvaluate struct { + // Resource GET parameter + // + // Show only rules for a specific resource + Resource []string + + // UserID GET parameter + // + // + UserID uint64 `json:",string"` + + // RoleID GET parameter + // + // + RoleID []uint64 + } + PermissionsRead struct { // RoleID PATH parameter // @@ -122,6 +139,75 @@ func (r *PermissionsEffective) Fill(req *http.Request) (err error) { return err } +// NewPermissionsEvaluate request +func NewPermissionsEvaluate() *PermissionsEvaluate { + return &PermissionsEvaluate{} +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) Auditable() map[string]interface{} { + return map[string]interface{}{ + "resource": r.Resource, + "userID": r.UserID, + "roleID": r.RoleID, + } +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) GetResource() []string { + return r.Resource +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) GetUserID() uint64 { + return r.UserID +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) GetRoleID() []uint64 { + return r.RoleID +} + +// Fill processes request and fills internal variables +func (r *PermissionsEvaluate) Fill(req *http.Request) (err error) { + + { + // GET params + tmp := req.URL.Query() + + if val, ok := tmp["resource[]"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } else if val, ok := tmp["resource"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } + if val, ok := tmp["userID"]; ok && len(val) > 0 { + r.UserID, err = payload.ParseUint64(val[0]), nil + if err != nil { + return err + } + } + if val, ok := tmp["roleID[]"]; ok { + r.RoleID, err = payload.ParseUint64s(val), nil + if err != nil { + return err + } + } else if val, ok := tmp["roleID"]; ok { + r.RoleID, err = payload.ParseUint64s(val), nil + if err != nil { + return err + } + } + } + + return err +} + // NewPermissionsRead request func NewPermissionsRead() *PermissionsRead { return &PermissionsRead{} diff --git a/automation/service/access_control.gen.go b/automation/service/access_control.gen.go index b7c8eb411..bfedabcd5 100644 --- a/automation/service/access_control.gen.go +++ b/automation/service/access_control.gen.go @@ -20,8 +20,9 @@ type ( accessControl struct { actionlog actionlog.Recorder - rbac interface { - Can(rbac.Session, string, rbac.Resource) bool + 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 @@ -29,15 +30,16 @@ type ( } ) -func AccessControl() *accessControl { +func AccessControl(rf func(ctx context.Context, id uint64) ([]uint64, error)) *accessControl { return &accessControl{ - rbac: rbac.Global(), - actionlog: DefaultActionlog, + roleFinder: rf, + rbac: rbac.Global(), + actionlog: DefaultActionlog, } } func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) bool { - return svc.rbac.Can(rbac.ContextToSession(ctx), op, res) + return svc.rbac.Evaluate(rbac.ContextToSession(ctx), op, res).Can } // Effective returns a list of effective permissions for all given resource @@ -52,6 +54,39 @@ func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee return } +// 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) { + // 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) + if err != nil { + return nil, err + } + + roles = append(rr, roles...) + } + + session := rbac.ParamsToSession(ctx, user, roles...) + for _, res := range rr { + r := res.RbacResource() + for op := range rbacResourceOperations(r) { + eval := svc.rbac.Evaluate(session, op, res) + + ee = append(ee, eval) + } + } + + return +} + 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 157e9f222..4898918b3 100644 --- a/automation/service/service.go +++ b/automation/service/service.go @@ -12,6 +12,7 @@ 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" ) @@ -89,7 +90,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock DefaultActionlog = actionlog.NewService(DefaultStore, log, tee, policy) } - DefaultAccessControl = AccessControl() + DefaultAccessControl = AccessControl(RolesForUser(s)) DefaultSession = Session(DefaultLogger.Named("session"), c.Workflow, ws) DefaultWorkflow = Workflow(DefaultLogger.Named("workflow"), c.Corredor, c.Workflow) @@ -163,3 +164,19 @@ 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 907532aa1..cd2a39f75 100644 --- a/codegen/assets/templates/gocode/rbac/$component_access_control.go.tpl +++ b/codegen/assets/templates/gocode/rbac/$component_access_control.go.tpl @@ -19,8 +19,9 @@ type ( accessControl struct { actionlog actionlog.Recorder + roleFinder func(ctx context.Context, id uint64) ([]uint64, error) rbac interface { - Can(rbac.Session, string, rbac.Resource) bool + 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 @@ -28,16 +29,17 @@ type ( } ) -func AccessControl() *accessControl { +func AccessControl(rf func(ctx context.Context, id uint64) ([]uint64, error)) *accessControl { return &accessControl{ - rbac: rbac.Global(), - actionlog: DefaultActionlog, + roleFinder: rf, + rbac: rbac.Global(), + actionlog: DefaultActionlog, } } func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) bool { - return svc.rbac.Can(rbac.ContextToSession(ctx), op, res) + return svc.rbac.Evaluate(rbac.ContextToSession(ctx), op, res).Can } // Effective returns a list of effective permissions for all given resource @@ -52,6 +54,39 @@ func (svc accessControl) Effective(ctx context.Context, rr ... rbac.Resource) (e return } +// 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) { + // 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) + if err != nil { + return nil, err + } + + roles = append(rr, roles...) + } + + session := rbac.ParamsToSession(ctx, user, roles...) + for _, res := range rr { + r := res.RbacResource() + for op := range rbacResourceOperations(r) { + eval := svc.rbac.Evaluate(session, op, res) + + ee = append(ee, eval) + } + } + + return +} + func (svc accessControl) List() (out []map[string]string) { def := []map[string]string{ {{- range .operations }} diff --git a/compose/rest.yaml b/compose/rest.yaml index e0684240b..ba7e6f3e3 100644 --- a/compose/rest.yaml +++ b/compose/rest.yaml @@ -1297,6 +1297,22 @@ endpoints: type: string required: false title: Show only rules for a specific resource + - name: evaluate + path: "/evaluate" + method: GET + title: Evaluate rules for given user/role combo + parameters: + get: + - name: resource + type: "[]string" + required: false + title: Show only rules for a specific resource + - name: userID + type: uint64 + required: false + - name: roleID + type: "[]uint64" + required: false - name: read path: "/{roleID}/rules" method: GET diff --git a/compose/rest/handlers/permissions.go b/compose/rest/handlers/permissions.go index 10767bc32..8ffa1be23 100644 --- a/compose/rest/handlers/permissions.go +++ b/compose/rest/handlers/permissions.go @@ -21,6 +21,7 @@ type ( PermissionsAPI interface { List(context.Context, *request.PermissionsList) (interface{}, error) Effective(context.Context, *request.PermissionsEffective) (interface{}, error) + Evaluate(context.Context, *request.PermissionsEvaluate) (interface{}, error) Read(context.Context, *request.PermissionsRead) (interface{}, error) Delete(context.Context, *request.PermissionsDelete) (interface{}, error) Update(context.Context, *request.PermissionsUpdate) (interface{}, error) @@ -30,6 +31,7 @@ type ( Permissions struct { List func(http.ResponseWriter, *http.Request) Effective func(http.ResponseWriter, *http.Request) + Evaluate func(http.ResponseWriter, *http.Request) Read func(http.ResponseWriter, *http.Request) Delete func(http.ResponseWriter, *http.Request) Update func(http.ResponseWriter, *http.Request) @@ -70,6 +72,22 @@ func NewPermissions(h PermissionsAPI) *Permissions { api.Send(w, r, value) }, + Evaluate: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewPermissionsEvaluate() + if err := params.Fill(r); err != nil { + api.Send(w, r, err) + return + } + + value, err := h.Evaluate(r.Context(), params) + if err != nil { + api.Send(w, r, err) + return + } + + api.Send(w, r, value) + }, Read: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewPermissionsRead() @@ -126,6 +144,7 @@ func (h Permissions) MountRoutes(r chi.Router, middlewares ...func(http.Handler) r.Use(middlewares...) r.Get("/permissions/", h.List) r.Get("/permissions/effective", h.Effective) + r.Get("/permissions/evaluate", h.Evaluate) r.Get("/permissions/{roleID}/rules", h.Read) r.Delete("/permissions/{roleID}/rules", h.Delete) r.Patch("/permissions/{roleID}/rules", h.Update) diff --git a/compose/rest/permissions.go b/compose/rest/permissions.go index 013f013e6..dcc98682b 100644 --- a/compose/rest/permissions.go +++ b/compose/rest/permissions.go @@ -2,6 +2,7 @@ package rest import ( "context" + "github.com/cortezaproject/corteza-server/compose/rest/request" "github.com/cortezaproject/corteza-server/compose/service" "github.com/cortezaproject/corteza-server/compose/types" @@ -14,8 +15,13 @@ 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) List() []map[string]string FindRulesByRoleID(context.Context, uint64) (rbac.RuleSet, error) Grant(ctx context.Context, rr ...*rbac.Rule) error @@ -32,6 +38,15 @@ func (ctrl Permissions) Effective(ctx context.Context, r *request.PermissionsEff return ctrl.ac.Effective(ctx, types.Component{}), nil } +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...) +} + func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (interface{}, error) { return ctrl.ac.List(), nil } @@ -61,3 +76,7 @@ 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/rest/request/permissions.go b/compose/rest/request/permissions.go index 378eb2737..12b760af9 100644 --- a/compose/rest/request/permissions.go +++ b/compose/rest/request/permissions.go @@ -44,6 +44,23 @@ type ( Resource string } + PermissionsEvaluate struct { + // Resource GET parameter + // + // Show only rules for a specific resource + Resource []string + + // UserID GET parameter + // + // + UserID uint64 `json:",string"` + + // RoleID GET parameter + // + // + RoleID []uint64 + } + PermissionsRead struct { // RoleID PATH parameter // @@ -122,6 +139,75 @@ func (r *PermissionsEffective) Fill(req *http.Request) (err error) { return err } +// NewPermissionsEvaluate request +func NewPermissionsEvaluate() *PermissionsEvaluate { + return &PermissionsEvaluate{} +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) Auditable() map[string]interface{} { + return map[string]interface{}{ + "resource": r.Resource, + "userID": r.UserID, + "roleID": r.RoleID, + } +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) GetResource() []string { + return r.Resource +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) GetUserID() uint64 { + return r.UserID +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) GetRoleID() []uint64 { + return r.RoleID +} + +// Fill processes request and fills internal variables +func (r *PermissionsEvaluate) Fill(req *http.Request) (err error) { + + { + // GET params + tmp := req.URL.Query() + + if val, ok := tmp["resource[]"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } else if val, ok := tmp["resource"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } + if val, ok := tmp["userID"]; ok && len(val) > 0 { + r.UserID, err = payload.ParseUint64(val[0]), nil + if err != nil { + return err + } + } + if val, ok := tmp["roleID[]"]; ok { + r.RoleID, err = payload.ParseUint64s(val), nil + if err != nil { + return err + } + } else if val, ok := tmp["roleID"]; ok { + r.RoleID, err = payload.ParseUint64s(val), nil + if err != nil { + return err + } + } + } + + return err +} + // NewPermissionsRead request func NewPermissionsRead() *PermissionsRead { return &PermissionsRead{} diff --git a/compose/service/access_control.gen.go b/compose/service/access_control.gen.go index 8392b57d7..9716ca877 100644 --- a/compose/service/access_control.gen.go +++ b/compose/service/access_control.gen.go @@ -20,8 +20,9 @@ type ( accessControl struct { actionlog actionlog.Recorder - rbac interface { - Can(rbac.Session, string, rbac.Resource) bool + 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 @@ -29,15 +30,16 @@ type ( } ) -func AccessControl() *accessControl { +func AccessControl(rf func(ctx context.Context, id uint64) ([]uint64, error)) *accessControl { return &accessControl{ - rbac: rbac.Global(), - actionlog: DefaultActionlog, + roleFinder: rf, + rbac: rbac.Global(), + actionlog: DefaultActionlog, } } func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) bool { - return svc.rbac.Can(rbac.ContextToSession(ctx), op, res) + return svc.rbac.Evaluate(rbac.ContextToSession(ctx), op, res).Can } // Effective returns a list of effective permissions for all given resource @@ -52,6 +54,39 @@ func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee return } +// 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) { + // 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) + if err != nil { + return nil, err + } + + roles = append(rr, roles...) + } + + session := rbac.ParamsToSession(ctx, user, roles...) + for _, res := range rr { + r := res.RbacResource() + for op := range rbacResourceOperations(r) { + eval := svc.rbac.Evaluate(session, op, res) + + ee = append(ee, eval) + } + } + + return +} + 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 7b331c89c..da3a342f4 100644 --- a/compose/service/service.go +++ b/compose/service/service.go @@ -10,6 +10,7 @@ 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" @@ -126,7 +127,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, c Config) } } - DefaultAccessControl = AccessControl() + DefaultAccessControl = AccessControl(systemService.RolesForUser(s)) DefaultResourceTranslation = ResourceTranslationsManager(locale.Global()) if DefaultObjectStore == nil { diff --git a/federation/rest.yaml b/federation/rest.yaml index c021bb5f2..5b0ca77c7 100644 --- a/federation/rest.yaml +++ b/federation/rest.yaml @@ -519,6 +519,22 @@ endpoints: type: string required: false title: Show only rules for a specific resource + - name: evaluate + path: "/evaluate" + method: GET + title: Evaluate rules for given user/role combo + parameters: + get: + - name: resource + type: "[]string" + required: false + title: Show only rules for a specific resource + - name: userID + type: uint64 + required: false + - name: roleID + type: "[]uint64" + required: false - name: read path: "/{roleID}/rules" method: GET diff --git a/federation/rest/handlers/permissions.go b/federation/rest/handlers/permissions.go index 35430592c..b48744357 100644 --- a/federation/rest/handlers/permissions.go +++ b/federation/rest/handlers/permissions.go @@ -21,6 +21,7 @@ type ( PermissionsAPI interface { List(context.Context, *request.PermissionsList) (interface{}, error) Effective(context.Context, *request.PermissionsEffective) (interface{}, error) + Evaluate(context.Context, *request.PermissionsEvaluate) (interface{}, error) Read(context.Context, *request.PermissionsRead) (interface{}, error) Delete(context.Context, *request.PermissionsDelete) (interface{}, error) Update(context.Context, *request.PermissionsUpdate) (interface{}, error) @@ -30,6 +31,7 @@ type ( Permissions struct { List func(http.ResponseWriter, *http.Request) Effective func(http.ResponseWriter, *http.Request) + Evaluate func(http.ResponseWriter, *http.Request) Read func(http.ResponseWriter, *http.Request) Delete func(http.ResponseWriter, *http.Request) Update func(http.ResponseWriter, *http.Request) @@ -70,6 +72,22 @@ func NewPermissions(h PermissionsAPI) *Permissions { api.Send(w, r, value) }, + Evaluate: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewPermissionsEvaluate() + if err := params.Fill(r); err != nil { + api.Send(w, r, err) + return + } + + value, err := h.Evaluate(r.Context(), params) + if err != nil { + api.Send(w, r, err) + return + } + + api.Send(w, r, value) + }, Read: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewPermissionsRead() @@ -126,6 +144,7 @@ func (h Permissions) MountRoutes(r chi.Router, middlewares ...func(http.Handler) r.Use(middlewares...) r.Get("/permissions/", h.List) r.Get("/permissions/effective", h.Effective) + r.Get("/permissions/evaluate", h.Evaluate) r.Get("/permissions/{roleID}/rules", h.Read) r.Delete("/permissions/{roleID}/rules", h.Delete) r.Patch("/permissions/{roleID}/rules", h.Update) diff --git a/federation/rest/permissions.go b/federation/rest/permissions.go index 73b4020a2..a650a0bb7 100644 --- a/federation/rest/permissions.go +++ b/federation/rest/permissions.go @@ -2,6 +2,7 @@ package rest import ( "context" + "github.com/cortezaproject/corteza-server/federation/rest/request" "github.com/cortezaproject/corteza-server/federation/service" "github.com/cortezaproject/corteza-server/federation/types" @@ -14,8 +15,13 @@ 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) List() []map[string]string FindRulesByRoleID(context.Context, uint64) (rbac.RuleSet, error) Grant(ctx context.Context, rr ...*rbac.Rule) error @@ -32,6 +38,15 @@ func (ctrl Permissions) Effective(ctx context.Context, r *request.PermissionsEff return ctrl.ac.Effective(ctx, types.Component{}), nil } +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...) +} + func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (interface{}, error) { return ctrl.ac.List(), nil } @@ -61,3 +76,7 @@ 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/rest/request/permissions.go b/federation/rest/request/permissions.go index 378eb2737..12b760af9 100644 --- a/federation/rest/request/permissions.go +++ b/federation/rest/request/permissions.go @@ -44,6 +44,23 @@ type ( Resource string } + PermissionsEvaluate struct { + // Resource GET parameter + // + // Show only rules for a specific resource + Resource []string + + // UserID GET parameter + // + // + UserID uint64 `json:",string"` + + // RoleID GET parameter + // + // + RoleID []uint64 + } + PermissionsRead struct { // RoleID PATH parameter // @@ -122,6 +139,75 @@ func (r *PermissionsEffective) Fill(req *http.Request) (err error) { return err } +// NewPermissionsEvaluate request +func NewPermissionsEvaluate() *PermissionsEvaluate { + return &PermissionsEvaluate{} +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) Auditable() map[string]interface{} { + return map[string]interface{}{ + "resource": r.Resource, + "userID": r.UserID, + "roleID": r.RoleID, + } +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) GetResource() []string { + return r.Resource +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) GetUserID() uint64 { + return r.UserID +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) GetRoleID() []uint64 { + return r.RoleID +} + +// Fill processes request and fills internal variables +func (r *PermissionsEvaluate) Fill(req *http.Request) (err error) { + + { + // GET params + tmp := req.URL.Query() + + if val, ok := tmp["resource[]"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } else if val, ok := tmp["resource"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } + if val, ok := tmp["userID"]; ok && len(val) > 0 { + r.UserID, err = payload.ParseUint64(val[0]), nil + if err != nil { + return err + } + } + if val, ok := tmp["roleID[]"]; ok { + r.RoleID, err = payload.ParseUint64s(val), nil + if err != nil { + return err + } + } else if val, ok := tmp["roleID"]; ok { + r.RoleID, err = payload.ParseUint64s(val), nil + if err != nil { + return err + } + } + } + + return err +} + // NewPermissionsRead request func NewPermissionsRead() *PermissionsRead { return &PermissionsRead{} diff --git a/federation/service/access_control.gen.go b/federation/service/access_control.gen.go index 5b268c8a3..f4cdb892b 100644 --- a/federation/service/access_control.gen.go +++ b/federation/service/access_control.gen.go @@ -20,8 +20,9 @@ type ( accessControl struct { actionlog actionlog.Recorder - rbac interface { - Can(rbac.Session, string, rbac.Resource) bool + 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 @@ -29,15 +30,16 @@ type ( } ) -func AccessControl() *accessControl { +func AccessControl(rf func(ctx context.Context, id uint64) ([]uint64, error)) *accessControl { return &accessControl{ - rbac: rbac.Global(), - actionlog: DefaultActionlog, + roleFinder: rf, + rbac: rbac.Global(), + actionlog: DefaultActionlog, } } func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) bool { - return svc.rbac.Can(rbac.ContextToSession(ctx), op, res) + return svc.rbac.Evaluate(rbac.ContextToSession(ctx), op, res).Can } // Effective returns a list of effective permissions for all given resource @@ -52,6 +54,39 @@ func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee return } +// 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) { + // 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) + if err != nil { + return nil, err + } + + roles = append(rr, roles...) + } + + session := rbac.ParamsToSession(ctx, user, roles...) + for _, res := range rr { + r := res.RbacResource() + for op := range rbacResourceOperations(r) { + eval := svc.rbac.Evaluate(session, op, res) + + ee = append(ee, eval) + } + } + + return +} + 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 b701749e5..6d1cc28e0 100644 --- a/federation/service/service.go +++ b/federation/service/service.go @@ -85,7 +85,7 @@ func Initialize(_ context.Context, log *zap.Logger, s store.Storer, c Config) (e DefaultActionlog = actionlog.NewService(DefaultStore, log, tee, policy) } - DefaultAccessControl = AccessControl() + DefaultAccessControl = AccessControl(service.RolesForUser(s)) DefaultNode = Node( DefaultStore, diff --git a/pkg/envoy/store/compose.go b/pkg/envoy/store/compose.go index 346994e63..4224e691b 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() + ac := service.AccessControl(nil) 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 7a9965bf2..36829d6a0 100644 --- a/pkg/rbac/effective.go +++ b/pkg/rbac/effective.go @@ -8,6 +8,29 @@ 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/ruleset_checks.go b/pkg/rbac/ruleset_checks.go index a2e840f1b..2c4956e78 100644 --- a/pkg/rbac/ruleset_checks.go +++ b/pkg/rbac/ruleset_checks.go @@ -5,20 +5,26 @@ import ( ) func check(indexedRules OptRuleSet, rolesByKind partRoles, op, res string) Access { + a, _, _ := evaluate(indexedRules, rolesByKind, op, res, false) + return a +} + +func evaluate(indexedRules OptRuleSet, rolesByKind partRoles, op, res string, parent bool) (Access, *Rule, explanation) { if member(rolesByKind, AnonymousRole) && len(rolesByKind) > 1 { // Integrity check; when user is member of anonymous role // should not be member of any other type of role - return Deny + + return Deny, nil, stepIntegrity } if member(rolesByKind, BypassRole) { // if user has at least one bypass role, we allow access - return Allow + return Allow, nil, stepBypass } if len(indexedRules) == 0 { // no rules no access - return Inherit + return Inherit, nil, stepRuleless } var rules RuleSet @@ -34,11 +40,6 @@ func check(indexedRules OptRuleSet, rolesByKind partRoles, op, res string) Acces continue } - // user has at least one bypass role - if kind == BypassRole { - return Allow - } - rules = nil for roleID, r := range indexedRules[op] { if !rolesByKind[kind][roleID] { @@ -47,17 +48,28 @@ func check(indexedRules OptRuleSet, rolesByKind partRoles, op, res string) Acces rules = append(rules, r...) } - access := checkRulesByResource(rules, op, res) + // When evaluating access for parent, omit the exact tule + if parent { + nr := make(RuleSet, 0, len(rules)) + for _, r := range rules { + if r.Resource != res { + nr = append(nr, r) + } + } + rules = nr + } + + r, access := checkRulesByResource(rules, op, res) if access != Inherit { - return access + return access, r, stepEvaluated } } - return Inherit + return Inherit, nil, stepRuleless } // Check given resource match and operation on all given rules -func checkRulesByResource(set RuleSet, op, res string) Access { +func checkRulesByResource(set RuleSet, op, res string) (*Rule, Access) { // Make sure rules are always sorted (by level) // to avoid any kind of unstable behaviour sort.Sort(set) @@ -72,11 +84,11 @@ func checkRulesByResource(set RuleSet, op, res string) Access { } if r.Access != Inherit { - return r.Access + return r, r.Access } } - return Inherit + return nil, Inherit } // at least one of the roles must be set to true diff --git a/pkg/rbac/ruleset_checks_test.go b/pkg/rbac/ruleset_checks_test.go index 5f24378ff..398206231 100644 --- a/pkg/rbac/ruleset_checks_test.go +++ b/pkg/rbac/ruleset_checks_test.go @@ -155,7 +155,8 @@ func Test_checkRulesByResource(t *testing.T) { for _, c := range cc { t.Run(c.res, func(t *testing.T) { - require.Equal(t, c.exp.String(), checkRulesByResource(c.set, c.op, c.res).String()) + _, a := checkRulesByResource(c.set, c.op, c.res) + require.Equal(t, c.exp.String(), a.String()) }) } } diff --git a/pkg/rbac/service.go b/pkg/rbac/service.go index 56bdf25fc..f5b342747 100644 --- a/pkg/rbac/service.go +++ b/pkg/rbac/service.go @@ -93,12 +93,9 @@ func (svc *service) Can(ses Session, op string, res Resource) bool { // // See RuleSet's Check() func for details func (svc *service) Check(ses Session, op string, res Resource) (v Access) { - svc.l.RLock() - defer svc.l.RUnlock() - var ( - fRoles = getContextRoles(ses, res, svc.roles) - access = check(svc.indexed, fRoles, op, res.RbacResource()) + eval, fRoles = svc.evaluate(ses, op, res, false) + access = eval.Access ) svc.logger.Debug(access.String()+" "+op+" for "+res.RbacResource(), @@ -113,6 +110,68 @@ func (svc *service) Check(ses Session, op string, res Resource) (v Access) { return access } +// Eval evaluates access for the given parameters +// +// The evaluation outputs verbose details to assist the UI. +func (svc *service) Evaluate(ses Session, op string, res Resource) Evaluated { + var ( + eval, fRoles = svc.evaluate(ses, op, res, true) + ) + + svc.logger.Debug(eval.Access.String()+" "+op+" for "+res.RbacResource(), + append( + fRoles.LogFields(), + zap.Uint64("identity", ses.Identity()), + zap.Any("indexed", len(svc.indexed)), + zap.Any("rules", len(svc.rules)), + )..., + ) + + return eval +} + +func (svc *service) evaluate(ses Session, op string, res Resource, inclParent bool) (e Evaluated, _ partRoles) { + svc.l.RLock() + defer svc.l.RUnlock() + + var ( + fRoles = getContextRoles(ses, res, svc.roles) + access, rule, expl = evaluate(svc.indexed, fRoles, op, res.RbacResource(), false) + ) + + // Check the requested resource + e = Evaluated{ + Resource: res.RbacResource(), + Operation: op, + + Access: access, + Can: access == Allow, + Rule: rule, + Step: expl, + } + if rule != nil { + e.RoleID = rule.RoleID + } + + // Check the parent resource + if inclParent { + access, rule, expl = evaluate(svc.indexed, fRoles, op, res.RbacResource(), true) + e.Default = &Evaluated{ + Resource: res.RbacResource(), + Operation: op, + Access: access, + Can: access == Allow, + Rule: rule, + Step: expl, + } + if rule != nil { + e.Default.RoleID = rule.RoleID + } + } + + return e, fRoles +} + // Grant appends and/or overwrites internal rules slice // // All rules with Inherit are removed diff --git a/pkg/rbac/session.go b/pkg/rbac/session.go index fd0a50202..86ba750f5 100644 --- a/pkg/rbac/session.go +++ b/pkg/rbac/session.go @@ -41,6 +41,14 @@ func ContextToSession(ctx context.Context) *session { return NewSession(ctx, auth.GetIdentityFromContext(ctx)) } +func ParamsToSession(ctx context.Context, user uint64, roles ...uint64) *session { + return &session{ + id: user, + rr: roles, + ctx: ctx, + } +} + func NewSession(ctx context.Context, i auth.Identifiable) *session { return &session{ id: i.Identity(), diff --git a/system/rest.yaml b/system/rest.yaml index 5c2fec72f..cf976e342 100644 --- a/system/rest.yaml +++ b/system/rest.yaml @@ -1221,6 +1221,22 @@ endpoints: type: string required: false title: Show only rules for a specific resource + - name: evaluate + path: "/evaluate" + method: GET + title: Evaluate rules for given user/role combo + parameters: + get: + - name: resource + type: "[]string" + required: false + title: Show only rules for a specific resource + - name: userID + type: uint64 + required: false + - name: roleID + type: "[]uint64" + required: false - name: read path: "/{roleID}/rules" method: GET diff --git a/system/rest/handlers/permissions.go b/system/rest/handlers/permissions.go index 979b283dc..562916637 100644 --- a/system/rest/handlers/permissions.go +++ b/system/rest/handlers/permissions.go @@ -21,6 +21,7 @@ type ( PermissionsAPI interface { List(context.Context, *request.PermissionsList) (interface{}, error) Effective(context.Context, *request.PermissionsEffective) (interface{}, error) + Evaluate(context.Context, *request.PermissionsEvaluate) (interface{}, error) Read(context.Context, *request.PermissionsRead) (interface{}, error) Delete(context.Context, *request.PermissionsDelete) (interface{}, error) Update(context.Context, *request.PermissionsUpdate) (interface{}, error) @@ -31,6 +32,7 @@ type ( Permissions struct { List func(http.ResponseWriter, *http.Request) Effective func(http.ResponseWriter, *http.Request) + Evaluate func(http.ResponseWriter, *http.Request) Read func(http.ResponseWriter, *http.Request) Delete func(http.ResponseWriter, *http.Request) Update func(http.ResponseWriter, *http.Request) @@ -72,6 +74,22 @@ func NewPermissions(h PermissionsAPI) *Permissions { api.Send(w, r, value) }, + Evaluate: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewPermissionsEvaluate() + if err := params.Fill(r); err != nil { + api.Send(w, r, err) + return + } + + value, err := h.Evaluate(r.Context(), params) + if err != nil { + api.Send(w, r, err) + return + } + + api.Send(w, r, value) + }, Read: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewPermissionsRead() @@ -144,6 +162,7 @@ func (h Permissions) MountRoutes(r chi.Router, middlewares ...func(http.Handler) r.Use(middlewares...) r.Get("/permissions/", h.List) r.Get("/permissions/effective", h.Effective) + r.Get("/permissions/evaluate", h.Evaluate) r.Get("/permissions/{roleID}/rules", h.Read) r.Delete("/permissions/{roleID}/rules", h.Delete) r.Patch("/permissions/{roleID}/rules", h.Update) diff --git a/system/rest/permissions.go b/system/rest/permissions.go index 5600f48ac..2b461572b 100644 --- a/system/rest/permissions.go +++ b/system/rest/permissions.go @@ -2,6 +2,7 @@ package rest import ( "context" + "github.com/cortezaproject/corteza-server/pkg/api" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/cortezaproject/corteza-server/pkg/rbac" @@ -15,8 +16,13 @@ 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) List() []map[string]string FindRulesByRoleID(context.Context, uint64) (rbac.RuleSet, error) CloneRulesByRoleID(ctx context.Context, roleID uint64, toRoleID ...uint64) error @@ -34,6 +40,15 @@ func (ctrl Permissions) Effective(ctx context.Context, r *request.PermissionsEff return ctrl.ac.Effective(ctx, types.Component{}), nil } +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...) +} + func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (interface{}, error) { return ctrl.ac.List(), nil } @@ -68,3 +83,7 @@ func (ctrl Permissions) Clone(ctx context.Context, r *request.PermissionsClone) // 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/rest/request/permissions.go b/system/rest/request/permissions.go index c2432bb2d..cb1866622 100644 --- a/system/rest/request/permissions.go +++ b/system/rest/request/permissions.go @@ -44,6 +44,23 @@ type ( Resource string } + PermissionsEvaluate struct { + // Resource GET parameter + // + // Show only rules for a specific resource + Resource []string + + // UserID GET parameter + // + // + UserID uint64 `json:",string"` + + // RoleID GET parameter + // + // + RoleID []uint64 + } + PermissionsRead struct { // RoleID PATH parameter // @@ -134,6 +151,75 @@ func (r *PermissionsEffective) Fill(req *http.Request) (err error) { return err } +// NewPermissionsEvaluate request +func NewPermissionsEvaluate() *PermissionsEvaluate { + return &PermissionsEvaluate{} +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) Auditable() map[string]interface{} { + return map[string]interface{}{ + "resource": r.Resource, + "userID": r.UserID, + "roleID": r.RoleID, + } +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) GetResource() []string { + return r.Resource +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) GetUserID() uint64 { + return r.UserID +} + +// Auditable returns all auditable/loggable parameters +func (r PermissionsEvaluate) GetRoleID() []uint64 { + return r.RoleID +} + +// Fill processes request and fills internal variables +func (r *PermissionsEvaluate) Fill(req *http.Request) (err error) { + + { + // GET params + tmp := req.URL.Query() + + if val, ok := tmp["resource[]"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } else if val, ok := tmp["resource"]; ok { + r.Resource, err = val, nil + if err != nil { + return err + } + } + if val, ok := tmp["userID"]; ok && len(val) > 0 { + r.UserID, err = payload.ParseUint64(val[0]), nil + if err != nil { + return err + } + } + if val, ok := tmp["roleID[]"]; ok { + r.RoleID, err = payload.ParseUint64s(val), nil + if err != nil { + return err + } + } else if val, ok := tmp["roleID"]; ok { + r.RoleID, err = payload.ParseUint64s(val), nil + if err != nil { + return err + } + } + } + + return err +} + // NewPermissionsRead request func NewPermissionsRead() *PermissionsRead { return &PermissionsRead{} diff --git a/system/service/access_control.gen.go b/system/service/access_control.gen.go index 1894b4f50..69393b4a6 100644 --- a/system/service/access_control.gen.go +++ b/system/service/access_control.gen.go @@ -20,8 +20,9 @@ type ( accessControl struct { actionlog actionlog.Recorder - rbac interface { - Can(rbac.Session, string, rbac.Resource) bool + 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 @@ -29,15 +30,16 @@ type ( } ) -func AccessControl() *accessControl { +func AccessControl(rf func(ctx context.Context, id uint64) ([]uint64, error)) *accessControl { return &accessControl{ - rbac: rbac.Global(), - actionlog: DefaultActionlog, + roleFinder: rf, + rbac: rbac.Global(), + actionlog: DefaultActionlog, } } func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) bool { - return svc.rbac.Can(rbac.ContextToSession(ctx), op, res) + return svc.rbac.Evaluate(rbac.ContextToSession(ctx), op, res).Can } // Effective returns a list of effective permissions for all given resource @@ -52,6 +54,39 @@ func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee return } +// 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) { + // 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) + if err != nil { + return nil, err + } + + roles = append(rr, roles...) + } + + session := rbac.ParamsToSession(ctx, user, roles...) + for _, res := range rr { + r := res.RbacResource() + for op := range rbacResourceOperations(r) { + eval := svc.rbac.Evaluate(session, op, res) + + ee = append(ee, eval) + } + } + + return +} + func (svc accessControl) List() (out []map[string]string) { def := []map[string]string{ { diff --git a/system/service/role.go b/system/service/role.go index 8c91c4bec..f1e227f02 100644 --- a/system/service/role.go +++ b/system/service/role.go @@ -962,3 +962,18 @@ func UpdateRbacRoles(ctx context.Context, log *zap.Logger, ru rbacRoleUpdater, b ru.UpdateRoles(rr...) return nil } + +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/system/service/service.go b/system/service/service.go index 174d75123..3e3cf4358 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() + DefaultAccessControl = AccessControl(RolesForUser(s)) DefaultSettings = Settings(ctx, DefaultStore, DefaultLogger, DefaultAccessControl, CurrentSettings)