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:
Agnès Toulet
2020-11-24 12:10:32 +01:00
committed by GitHub
parent 4c47fc56bb
commit 22788d1d86
21 changed files with 852 additions and 133 deletions

View File

@@ -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 {

View File

@@ -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)
})
})
}

View File

@@ -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 = ?`)

View File

@@ -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)
})
})
})
}