grafana/pkg/services/guardian/guardian.go
Serge Zaitsev fec634a091
Chore: Remove bus.Dispatch from guardian package (#46711)
* replace bus in guardian with sqlstore

* fix a couple of tests

* replace bus in the rest of the tests

* allow init guardian from other packages

* make linter happy

* init guardian in library elements

* fix another test in libraryelements

* fix more tests

* move guardian mock one level deeper

* fix more tests

* rename init functions
2022-03-21 10:49:49 +01:00

381 lines
11 KiB
Go

package guardian
import (
"context"
"errors"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
var (
ErrGuardianPermissionExists = errors.New("permission already exists")
ErrGuardianOverride = errors.New("you can only override a permission to be higher")
)
// DashboardGuardian to be used for guard against operations without access on dashboard and acl
type DashboardGuardian interface {
CanSave() (bool, error)
CanEdit() (bool, error)
CanView() (bool, error)
CanAdmin() (bool, error)
CanDelete() (bool, error)
CanCreate(folderID int64, isFolder bool) (bool, error)
CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error)
// GetAcl returns ACL.
GetAcl() ([]*models.DashboardAclInfoDTO, error)
// GetACLWithoutDuplicates returns ACL and strips any permission
// that already has an inherited permission with higher or equal
// permission.
GetACLWithoutDuplicates() ([]*models.DashboardAclInfoDTO, error)
GetHiddenACL(*setting.Cfg) ([]*models.DashboardAcl, error)
}
type dashboardGuardianImpl struct {
user *models.SignedInUser
dashId int64
orgId int64
acl []*models.DashboardAclInfoDTO
teams []*models.TeamDTO
log log.Logger
ctx context.Context
store sqlstore.Store
}
// New factory for creating a new dashboard guardian instance
// When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned
var New = func(ctx context.Context, dashId int64, orgId int64, user *models.SignedInUser) DashboardGuardian {
panic("no guardian factory implementation provided")
}
func newDashboardGuardian(ctx context.Context, dashId int64, orgId int64, user *models.SignedInUser, store sqlstore.Store) *dashboardGuardianImpl {
return &dashboardGuardianImpl{
user: user,
dashId: dashId,
orgId: orgId,
log: log.New("dashboard.permissions"),
ctx: ctx,
store: store,
}
}
func (g *dashboardGuardianImpl) CanSave() (bool, error) {
return g.HasPermission(models.PERMISSION_EDIT)
}
func (g *dashboardGuardianImpl) CanEdit() (bool, error) {
if setting.ViewersCanEdit {
return g.HasPermission(models.PERMISSION_VIEW)
}
return g.HasPermission(models.PERMISSION_EDIT)
}
func (g *dashboardGuardianImpl) CanView() (bool, error) {
return g.HasPermission(models.PERMISSION_VIEW)
}
func (g *dashboardGuardianImpl) CanAdmin() (bool, error) {
return g.HasPermission(models.PERMISSION_ADMIN)
}
func (g *dashboardGuardianImpl) CanDelete() (bool, error) {
// when using dashboard guardian without access control a user can delete a dashboard if they can save it
return g.CanSave()
}
func (g *dashboardGuardianImpl) CanCreate(_ int64, _ bool) (bool, error) {
// when using dashboard guardian without access control a user can create a dashboard if they can save it
return g.CanSave()
}
func (g *dashboardGuardianImpl) HasPermission(permission models.PermissionType) (bool, error) {
if g.user.OrgRole == models.ROLE_ADMIN {
return g.logHasPermissionResult(permission, true, nil)
}
acl, err := g.GetAcl()
if err != nil {
return g.logHasPermissionResult(permission, false, err)
}
result, err := g.checkAcl(permission, acl)
return g.logHasPermissionResult(permission, result, err)
}
func (g *dashboardGuardianImpl) logHasPermissionResult(permission models.PermissionType, hasPermission bool, err error) (bool, error) {
if err != nil {
return hasPermission, err
}
if hasPermission {
g.log.Debug("User granted access to execute action", "userId", g.user.UserId, "orgId", g.orgId, "uname", g.user.Login, "dashId", g.dashId, "action", permission)
} else {
g.log.Debug("User denied access to execute action", "userId", g.user.UserId, "orgId", g.orgId, "uname", g.user.Login, "dashId", g.dashId, "action", permission)
}
return hasPermission, err
}
func (g *dashboardGuardianImpl) checkAcl(permission models.PermissionType, acl []*models.DashboardAclInfoDTO) (bool, error) {
orgRole := g.user.OrgRole
teamAclItems := []*models.DashboardAclInfoDTO{}
for _, p := range acl {
// user match
if !g.user.IsAnonymous && p.UserId > 0 {
if p.UserId == g.user.UserId && p.Permission >= permission {
return true, nil
}
}
// role match
if p.Role != nil {
if *p.Role == orgRole && p.Permission >= permission {
return true, nil
}
}
// remember this rule for later
if p.TeamId > 0 {
teamAclItems = append(teamAclItems, p)
}
}
// do we have team rules?
if len(teamAclItems) == 0 {
return false, nil
}
// load teams
teams, err := g.getTeams()
if err != nil {
return false, err
}
// evaluate team rules
for _, p := range acl {
for _, ug := range teams {
if ug.Id == p.TeamId && p.Permission >= permission {
return true, nil
}
}
}
return false, nil
}
func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error) {
acl := []*models.DashboardAclInfoDTO{}
adminRole := models.ROLE_ADMIN
everyoneWithAdminRole := &models.DashboardAclInfoDTO{DashboardId: g.dashId, UserId: 0, TeamId: 0, Role: &adminRole, Permission: models.PERMISSION_ADMIN}
// validate that duplicate permissions don't exists
for _, p := range updatePermissions {
aclItem := &models.DashboardAclInfoDTO{DashboardId: p.DashboardID, UserId: p.UserID, TeamId: p.TeamID, Role: p.Role, Permission: p.Permission}
if aclItem.IsDuplicateOf(everyoneWithAdminRole) {
return false, ErrGuardianPermissionExists
}
for _, a := range acl {
if a.IsDuplicateOf(aclItem) {
return false, ErrGuardianPermissionExists
}
}
acl = append(acl, aclItem)
}
existingPermissions, err := g.GetAcl()
if err != nil {
return false, err
}
// validate overridden permissions to be higher
for _, a := range acl {
for _, existingPerm := range existingPermissions {
if !existingPerm.Inherited {
continue
}
if a.IsDuplicateOf(existingPerm) && a.Permission <= existingPerm.Permission {
return false, ErrGuardianOverride
}
}
}
if g.user.OrgRole == models.ROLE_ADMIN {
return true, nil
}
return g.checkAcl(permission, existingPermissions)
}
// GetAcl returns dashboard acl
func (g *dashboardGuardianImpl) GetAcl() ([]*models.DashboardAclInfoDTO, error) {
if g.acl != nil {
return g.acl, nil
}
query := models.GetDashboardAclInfoListQuery{DashboardID: g.dashId, OrgID: g.orgId}
if err := g.store.GetDashboardAclInfoList(g.ctx, &query); err != nil {
return nil, err
}
g.acl = query.Result
return g.acl, nil
}
func (g *dashboardGuardianImpl) GetACLWithoutDuplicates() ([]*models.DashboardAclInfoDTO, error) {
acl, err := g.GetAcl()
if err != nil {
return nil, err
}
nonInherited := []*models.DashboardAclInfoDTO{}
inherited := []*models.DashboardAclInfoDTO{}
for _, aclItem := range acl {
if aclItem.Inherited {
inherited = append(inherited, aclItem)
} else {
nonInherited = append(nonInherited, aclItem)
}
}
result := []*models.DashboardAclInfoDTO{}
for _, nonInheritedAclItem := range nonInherited {
duplicate := false
for _, inheritedAclItem := range inherited {
if nonInheritedAclItem.IsDuplicateOf(inheritedAclItem) && nonInheritedAclItem.Permission <= inheritedAclItem.Permission {
duplicate = true
break
}
}
if !duplicate {
result = append(result, nonInheritedAclItem)
}
}
result = append(inherited, result...)
return result, nil
}
func (g *dashboardGuardianImpl) getTeams() ([]*models.TeamDTO, error) {
if g.teams != nil {
return g.teams, nil
}
query := models.GetTeamsByUserQuery{OrgId: g.orgId, UserId: g.user.UserId}
err := g.store.GetTeamsByUser(g.ctx, &query)
g.teams = query.Result
return query.Result, err
}
func (g *dashboardGuardianImpl) GetHiddenACL(cfg *setting.Cfg) ([]*models.DashboardAcl, error) {
hiddenACL := make([]*models.DashboardAcl, 0)
if g.user.IsGrafanaAdmin {
return hiddenACL, nil
}
existingPermissions, err := g.GetAcl()
if err != nil {
return hiddenACL, err
}
for _, item := range existingPermissions {
if item.Inherited || item.UserLogin == g.user.Login {
continue
}
if _, hidden := cfg.HiddenUsers[item.UserLogin]; hidden {
hiddenACL = append(hiddenACL, &models.DashboardAcl{
OrgID: item.OrgId,
DashboardID: item.DashboardId,
UserID: item.UserId,
TeamID: item.TeamId,
Role: item.Role,
Permission: item.Permission,
Created: item.Created,
Updated: item.Updated,
})
}
}
return hiddenACL, nil
}
// nolint:unused
type FakeDashboardGuardian struct {
DashId int64
OrgId int64
User *models.SignedInUser
CanSaveValue bool
CanEditValue bool
CanViewValue bool
CanAdminValue bool
HasPermissionValue bool
CheckPermissionBeforeUpdateValue bool
CheckPermissionBeforeUpdateError error
GetAclValue []*models.DashboardAclInfoDTO
GetHiddenAclValue []*models.DashboardAcl
}
func (g *FakeDashboardGuardian) CanSave() (bool, error) {
return g.CanSaveValue, nil
}
func (g *FakeDashboardGuardian) CanEdit() (bool, error) {
return g.CanEditValue, nil
}
func (g *FakeDashboardGuardian) CanView() (bool, error) {
return g.CanViewValue, nil
}
func (g *FakeDashboardGuardian) CanAdmin() (bool, error) {
return g.CanAdminValue, nil
}
func (g *FakeDashboardGuardian) CanDelete() (bool, error) {
return g.CanSaveValue, nil
}
func (g *FakeDashboardGuardian) CanCreate(_ int64, _ bool) (bool, error) {
return g.CanSaveValue, nil
}
func (g *FakeDashboardGuardian) HasPermission(permission models.PermissionType) (bool, error) {
return g.HasPermissionValue, nil
}
func (g *FakeDashboardGuardian) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error) {
return g.CheckPermissionBeforeUpdateValue, g.CheckPermissionBeforeUpdateError
}
func (g *FakeDashboardGuardian) GetAcl() ([]*models.DashboardAclInfoDTO, error) {
return g.GetAclValue, nil
}
func (g *FakeDashboardGuardian) GetACLWithoutDuplicates() ([]*models.DashboardAclInfoDTO, error) {
return g.GetAcl()
}
func (g *FakeDashboardGuardian) GetHiddenACL(cfg *setting.Cfg) ([]*models.DashboardAcl, error) {
return g.GetHiddenAclValue, nil
}
// nolint:unused
func MockDashboardGuardian(mock *FakeDashboardGuardian) {
New = func(_ context.Context, dashId int64, orgId int64, user *models.SignedInUser) DashboardGuardian {
mock.OrgId = orgId
mock.DashId = dashId
mock.User = user
return mock
}
}