mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Add an option to hide certain users in the UI (#28942)
* Add an option to hide certain users in the UI * revert changes for admin users routes * fix sqlstore function name * Improve slice management Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com> * Hidden users: convert slice to map * filter with user logins instead of IDs * put HiddenUsers in Cfg struct * hide hidden users from dashboards/folders permissions list * Update conf/defaults.ini Co-authored-by: Torkel Ödegaard <torkel@grafana.com> * fix params order * fix tests * fix dashboard/folder update with hidden user * add team tests * add dashboard and folder permissions tests * fixes after merge * fix tests * API: add test for org users endpoints * update hidden users management for dashboard / folder permissions * improve dashboard / folder permissions tests * fixes after merge * Guardian: add hidden acl tests * API: add team members tests * fix team sql syntax for postgres * api tests update * fix linter error * fix tests errors after merge Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.com> Co-authored-by: Leonard Gram <leo@xlson.com>
This commit is contained in:
@@ -23,6 +23,7 @@ type DashboardGuardian interface {
|
||||
HasPermission(permission models.PermissionType) (bool, error)
|
||||
CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error)
|
||||
GetAcl() ([]*models.DashboardAclInfoDTO, error)
|
||||
GetHiddenACL(*setting.Cfg) ([]*models.DashboardAcl, error)
|
||||
}
|
||||
|
||||
type dashboardGuardianImpl struct {
|
||||
@@ -213,6 +214,38 @@ func (g *dashboardGuardianImpl) getTeams() ([]*models.TeamDTO, error) {
|
||||
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
|
||||
@@ -226,6 +259,7 @@ type FakeDashboardGuardian struct {
|
||||
CheckPermissionBeforeUpdateValue bool
|
||||
CheckPermissionBeforeUpdateError error
|
||||
GetAclValue []*models.DashboardAclInfoDTO
|
||||
GetHiddenAclValue []*models.DashboardAcl
|
||||
}
|
||||
|
||||
func (g *FakeDashboardGuardian) CanSave() (bool, error) {
|
||||
@@ -256,6 +290,10 @@ func (g *FakeDashboardGuardian) GetAcl() ([]*models.DashboardAclInfoDTO, error)
|
||||
return g.GetAclValue, nil
|
||||
}
|
||||
|
||||
func (g *FakeDashboardGuardian) GetHiddenACL(cfg *setting.Cfg) ([]*models.DashboardAcl, error) {
|
||||
return g.GetHiddenAclValue, nil
|
||||
}
|
||||
|
||||
// nolint:unused
|
||||
func MockDashboardGuardian(mock *FakeDashboardGuardian) {
|
||||
New = func(dashId int64, orgId int64, user *models.SignedInUser) DashboardGuardian {
|
||||
|
||||
@@ -6,7 +6,10 @@ import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -673,3 +676,51 @@ func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShou
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuardianGetHiddenACL(t *testing.T) {
|
||||
Convey("Get hidden ACL tests", t, func() {
|
||||
bus.ClearBusHandlers()
|
||||
|
||||
bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = []*models.DashboardAclInfoDTO{
|
||||
{Inherited: false, UserId: 1, UserLogin: "user1", Permission: models.PERMISSION_EDIT},
|
||||
{Inherited: false, UserId: 2, UserLogin: "user2", Permission: models.PERMISSION_ADMIN},
|
||||
{Inherited: true, UserId: 3, UserLogin: "user3", Permission: models.PERMISSION_VIEW},
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
cfg.HiddenUsers = map[string]struct{}{"user2": {}}
|
||||
|
||||
Convey("Should get hidden acl", func() {
|
||||
user := &models.SignedInUser{
|
||||
OrgId: orgID,
|
||||
UserId: 1,
|
||||
Login: "user1",
|
||||
}
|
||||
g := New(dashboardID, orgID, user)
|
||||
|
||||
hiddenACL, err := g.GetHiddenACL(cfg)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(hiddenACL, ShouldHaveLength, 1)
|
||||
So(hiddenACL[0].UserID, ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("Grafana admin should not get hidden acl", func() {
|
||||
user := &models.SignedInUser{
|
||||
OrgId: orgID,
|
||||
UserId: 1,
|
||||
Login: "user1",
|
||||
IsGrafanaAdmin: true,
|
||||
}
|
||||
g := New(dashboardID, orgID, user)
|
||||
|
||||
hiddenACL, err := g.GetHiddenACL(cfg)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(hiddenACL, ShouldHaveLength, 0)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package sqlstore
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
@@ -24,26 +25,54 @@ func init() {
|
||||
bus.AddHandler("sql", IsAdminOfTeams)
|
||||
}
|
||||
|
||||
func getTeamSearchSQLBase() string {
|
||||
return `SELECT
|
||||
team.id as id,
|
||||
team.org_id,
|
||||
team.name as name,
|
||||
team.email as email,
|
||||
(SELECT COUNT(*) from team_member where team_member.team_id = team.id) as member_count,
|
||||
team_member.permission
|
||||
FROM team as team
|
||||
INNER JOIN team_member on team.id = team_member.team_id AND team_member.user_id = ? `
|
||||
func getFilteredUsers(signedInUser *models.SignedInUser, hiddenUsers map[string]struct{}) []string {
|
||||
filteredUsers := make([]string, 0, len(hiddenUsers))
|
||||
if signedInUser == nil || signedInUser.IsGrafanaAdmin {
|
||||
return filteredUsers
|
||||
}
|
||||
|
||||
for u := range hiddenUsers {
|
||||
if u == signedInUser.Login {
|
||||
continue
|
||||
}
|
||||
filteredUsers = append(filteredUsers, u)
|
||||
}
|
||||
|
||||
return filteredUsers
|
||||
}
|
||||
|
||||
func getTeamSelectSQLBase() string {
|
||||
func getTeamMemberCount(filteredUsers []string) string {
|
||||
if len(filteredUsers) > 0 {
|
||||
return `(SELECT COUNT(*) FROM team_member
|
||||
INNER JOIN ` + dialect.Quote("user") + ` ON team_member.user_id = ` + dialect.Quote("user") + `.id
|
||||
WHERE team_member.team_id = team.id AND ` + dialect.Quote("user") + `.login NOT IN (?` +
|
||||
strings.Repeat(",?", len(filteredUsers)-1) + ")" +
|
||||
`) AS member_count `
|
||||
}
|
||||
|
||||
return "(SELECT COUNT(*) FROM team_member WHERE team_member.team_id = team.id) AS member_count "
|
||||
}
|
||||
|
||||
func getTeamSearchSQLBase(filteredUsers []string) string {
|
||||
return `SELECT
|
||||
team.id AS id,
|
||||
team.org_id,
|
||||
team.name AS name,
|
||||
team.email AS email,
|
||||
team_member.permission, ` +
|
||||
getTeamMemberCount(filteredUsers) +
|
||||
` FROM team AS team
|
||||
INNER JOIN team_member ON team.id = team_member.team_id AND team_member.user_id = ? `
|
||||
}
|
||||
|
||||
func getTeamSelectSQLBase(filteredUsers []string) string {
|
||||
return `SELECT
|
||||
team.id as id,
|
||||
team.org_id,
|
||||
team.name as name,
|
||||
team.email as email,
|
||||
(SELECT COUNT(*) from team_member where team_member.team_id = team.id) as member_count
|
||||
FROM team as team `
|
||||
team.email as email, ` +
|
||||
getTeamMemberCount(filteredUsers) +
|
||||
` FROM team as team `
|
||||
}
|
||||
|
||||
func CreateTeam(cmd *models.CreateTeamCommand) error {
|
||||
@@ -157,14 +186,21 @@ func SearchTeams(query *models.SearchTeamsQuery) error {
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
filteredUsers := getFilteredUsers(query.SignedInUser, query.HiddenUsers)
|
||||
if query.UserIdFilter > 0 {
|
||||
sql.WriteString(getTeamSearchSQLBase())
|
||||
sql.WriteString(getTeamSearchSQLBase(filteredUsers))
|
||||
for _, user := range filteredUsers {
|
||||
params = append(params, user)
|
||||
}
|
||||
params = append(params, query.UserIdFilter)
|
||||
} else {
|
||||
sql.WriteString(getTeamSelectSQLBase())
|
||||
sql.WriteString(getTeamSelectSQLBase(filteredUsers))
|
||||
for _, user := range filteredUsers {
|
||||
params = append(params, user)
|
||||
}
|
||||
}
|
||||
sql.WriteString(` WHERE team.org_id = ?`)
|
||||
|
||||
sql.WriteString(` WHERE team.org_id = ?`)
|
||||
params = append(params, query.OrgId)
|
||||
|
||||
if query.Query != "" {
|
||||
@@ -206,12 +242,19 @@ func SearchTeams(query *models.SearchTeamsQuery) error {
|
||||
|
||||
func GetTeamById(query *models.GetTeamByIdQuery) error {
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
filteredUsers := getFilteredUsers(query.SignedInUser, query.HiddenUsers)
|
||||
sql.WriteString(getTeamSelectSQLBase(filteredUsers))
|
||||
for _, user := range filteredUsers {
|
||||
params = append(params, user)
|
||||
}
|
||||
|
||||
sql.WriteString(getTeamSelectSQLBase())
|
||||
sql.WriteString(` WHERE team.org_id = ? and team.id = ?`)
|
||||
params = append(params, query.OrgId, query.Id)
|
||||
|
||||
var team models.TeamDTO
|
||||
exists, err := x.SQL(sql.String(), query.OrgId, query.Id).Get(&team)
|
||||
exists, err := x.SQL(sql.String(), params...).Get(&team)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -231,7 +274,7 @@ func GetTeamsByUser(query *models.GetTeamsByUserQuery) error {
|
||||
|
||||
var sql bytes.Buffer
|
||||
|
||||
sql.WriteString(getTeamSelectSQLBase())
|
||||
sql.WriteString(getTeamSelectSQLBase([]string{}))
|
||||
sql.WriteString(` INNER JOIN team_member on team.id = team_member.team_id`)
|
||||
sql.WriteString(` WHERE team.org_id = ? and team_member.user_id = ?`)
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
||||
So(team1.Name, ShouldEqual, "group1 name")
|
||||
So(team1.Email, ShouldEqual, "test1@test.com")
|
||||
So(team1.OrgId, ShouldEqual, testOrgId)
|
||||
So(team1.MemberCount, ShouldEqual, 0)
|
||||
|
||||
err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[0]})
|
||||
So(err, ShouldBeNil)
|
||||
@@ -74,6 +75,20 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
||||
So(q2.Result[0].Login, ShouldEqual, "loginuser1")
|
||||
So(q2.Result[0].OrgId, ShouldEqual, testOrgId)
|
||||
So(q2.Result[0].External, ShouldEqual, true)
|
||||
|
||||
err = SearchTeams(query)
|
||||
So(err, ShouldBeNil)
|
||||
team1 = query.Result.Teams[0]
|
||||
So(team1.MemberCount, ShouldEqual, 2)
|
||||
|
||||
getTeamQuery := &models.GetTeamByIdQuery{OrgId: testOrgId, Id: team1.Id}
|
||||
err = GetTeamById(getTeamQuery)
|
||||
So(err, ShouldBeNil)
|
||||
team1 = getTeamQuery.Result
|
||||
So(team1.Name, ShouldEqual, "group1 name")
|
||||
So(team1.Email, ShouldEqual, "test1@test.com")
|
||||
So(team1.OrgId, ShouldEqual, testOrgId)
|
||||
So(team1.MemberCount, ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("Should return latest auth module for users when getting team members", func() {
|
||||
@@ -275,6 +290,38 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Should not return hidden users in team member count", func() {
|
||||
signedInUser := &models.SignedInUser{Login: "loginuser0"}
|
||||
hiddenUsers := map[string]struct{}{"loginuser0": {}, "loginuser1": {}}
|
||||
|
||||
teamId := group1.Result.Id
|
||||
err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: teamId, UserId: userIds[0]})
|
||||
So(err, ShouldBeNil)
|
||||
err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: teamId, UserId: userIds[1]})
|
||||
So(err, ShouldBeNil)
|
||||
err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: teamId, UserId: userIds[2]})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
searchQuery := &models.SearchTeamsQuery{OrgId: testOrgId, Page: 1, Limit: 10, SignedInUser: signedInUser, HiddenUsers: hiddenUsers}
|
||||
err = SearchTeams(searchQuery)
|
||||
So(err, ShouldBeNil)
|
||||
So(searchQuery.Result.Teams, ShouldHaveLength, 2)
|
||||
team1 := searchQuery.Result.Teams[0]
|
||||
So(team1.MemberCount, ShouldEqual, 2)
|
||||
|
||||
searchQueryFilteredByUser := &models.SearchTeamsQuery{OrgId: testOrgId, Page: 1, Limit: 10, UserIdFilter: userIds[0], SignedInUser: signedInUser, HiddenUsers: hiddenUsers}
|
||||
err = SearchTeams(searchQueryFilteredByUser)
|
||||
So(err, ShouldBeNil)
|
||||
So(searchQueryFilteredByUser.Result.Teams, ShouldHaveLength, 1)
|
||||
team1 = searchQuery.Result.Teams[0]
|
||||
So(team1.MemberCount, ShouldEqual, 2)
|
||||
|
||||
getTeamQuery := &models.GetTeamByIdQuery{OrgId: testOrgId, Id: teamId, SignedInUser: signedInUser, HiddenUsers: hiddenUsers}
|
||||
err = GetTeamById(getTeamQuery)
|
||||
So(err, ShouldBeNil)
|
||||
So(getTeamQuery.Result.MemberCount, ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user