3
0

Add /permissions/evaluate endpoints to evaluate RBAC rules

This commit is contained in:
Tomaž Jerman
2022-06-21 15:53:27 +02:00
committed by Denis Arh
parent ae177514cf
commit 83ba7faa0f
32 changed files with 925 additions and 54 deletions
+16
View File
@@ -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
+19
View File
@@ -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)
+19
View File
@@ -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
}
+86
View File
@@ -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{}
+41 -6
View File
@@ -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{
{
+18 -1
View File
@@ -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
}
}
@@ -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 }}
+16
View File
@@ -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
+19
View File
@@ -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)
+19
View File
@@ -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
}
+86
View File
@@ -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{}
+41 -6
View File
@@ -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{
{
+2 -1
View File
@@ -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 {
+16
View File
@@ -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
+19
View File
@@ -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)
+19
View File
@@ -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
}
+86
View File
@@ -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{}
+41 -6
View File
@@ -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{
{
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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))
+23
View File
@@ -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) {
+26 -14
View File
@@ -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
+2 -1
View File
@@ -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())
})
}
}
+64 -5
View File
@@ -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
+8
View File
@@ -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(),
+16
View File
@@ -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
+19
View File
@@ -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)
+19
View File
@@ -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
}
+86
View File
@@ -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{}
+41 -6
View File
@@ -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{
{
+15
View File
@@ -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
}
}
+1 -1
View File
@@ -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)