Commit 22788d1d by Agnès Toulet Committed by GitHub

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>
parent 4c47fc56
......@@ -296,6 +296,9 @@ editors_can_admin = false
# The duration in time a user invitation remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 24h (24 hours). The minimum supported duration is 15m (15 minutes).
user_invite_max_lifetime_duration = 24h
# Enter a comma-separated list of usernames to hide them in the Grafana UI. These users are shown to Grafana admins and to themselves.
hidden_users =
[auth]
# Login cookie name
login_cookie_name = grafana_session
......
......@@ -295,6 +295,9 @@
# The duration in time a user invitation remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 24h (24 hours). The minimum supported duration is 15m (15 minutes).
;user_invite_max_lifetime_duration = 24h
# Enter a comma-separated list of users login to hide them in the Grafana UI. These users are shown to Grafana admins and themselves.
; hidden_users =
[auth]
# Login cookie name
;login_cookie_name = grafana_session
......
......@@ -170,7 +170,7 @@ func (hs *HTTPServer) registerRoutes() {
// team without requirement of user to be org admin
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
teamsRoute.Get("/:teamId", Wrap(GetTeamByID))
teamsRoute.Get("/:teamId", Wrap(hs.GetTeamByID))
teamsRoute.Get("/search", Wrap(hs.SearchTeams))
})
......@@ -184,7 +184,7 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), Wrap(UpdateOrgCurrent))
orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddressCurrent))
orgRoute.Get("/users", Wrap(GetOrgUsersForCurrentOrg))
orgRoute.Get("/users", Wrap(hs.GetOrgUsersForCurrentOrg))
orgRoute.Post("/users", quota("user"), bind(models.AddOrgUserCommand{}), Wrap(AddOrgUserToCurrentOrg))
orgRoute.Patch("/users/:userId", bind(models.UpdateOrgUserCommand{}), Wrap(UpdateOrgUserForCurrentOrg))
orgRoute.Delete("/users/:userId", Wrap(RemoveOrgUserForCurrentOrg))
......@@ -201,7 +201,7 @@ func (hs *HTTPServer) registerRoutes() {
// current org without requirement of user to be org admin
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Get("/users/lookup", Wrap(GetOrgUsersForCurrentOrgLookup))
orgRoute.Get("/users/lookup", Wrap(hs.GetOrgUsersForCurrentOrgLookup))
})
// create new org
......@@ -216,7 +216,7 @@ func (hs *HTTPServer) registerRoutes() {
orgsRoute.Put("/", bind(dtos.UpdateOrgForm{}), Wrap(UpdateOrg))
orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddress))
orgsRoute.Delete("/", Wrap(DeleteOrgByID))
orgsRoute.Get("/users", Wrap(GetOrgUsers))
orgsRoute.Get("/users", Wrap(hs.GetOrgUsers))
orgsRoute.Post("/users", bind(models.AddOrgUserCommand{}), Wrap(AddOrgUser))
orgsRoute.Patch("/users/:userId", bind(models.UpdateOrgUserCommand{}), Wrap(UpdateOrgUser))
orgsRoute.Delete("/users/:userId", Wrap(RemoveOrgUser))
......@@ -287,8 +287,8 @@ func (hs *HTTPServer) registerRoutes() {
folderUidRoute.Delete("/", Wrap(DeleteFolder))
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
folderPermissionRoute.Get("/", Wrap(GetFolderPermissionList))
folderPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), Wrap(UpdateFolderPermissions))
folderPermissionRoute.Get("/", Wrap(hs.GetFolderPermissionList))
folderPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), Wrap(hs.UpdateFolderPermissions))
})
})
})
......@@ -314,8 +314,8 @@ func (hs *HTTPServer) registerRoutes() {
dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), Wrap(hs.RestoreDashboardVersion))
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) {
dashboardPermissionRoute.Get("/", Wrap(GetDashboardPermissionList))
dashboardPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), Wrap(UpdateDashboardPermissions))
dashboardPermissionRoute.Get("/", Wrap(hs.GetDashboardPermissionList))
dashboardPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), Wrap(hs.UpdateDashboardPermissions))
})
})
})
......
......@@ -28,6 +28,7 @@ func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url
sc.context = c
sc.context.UserId = testUserID
sc.context.OrgId = testOrgID
sc.context.Login = testUserLogin
sc.context.OrgRole = role
if sc.handlerFunc != nil {
return sc.handlerFunc(sc.context)
......
......@@ -10,7 +10,7 @@ import (
"github.com/grafana/grafana/pkg/services/guardian"
)
func GetDashboardPermissionList(c *models.ReqContext) Response {
func (hs *HTTPServer) GetDashboardPermissionList(c *models.ReqContext) Response {
dashID := c.ParamsInt64(":dashboardId")
_, rsp := getDashboardHelper(c.OrgId, "", dashID, "")
......@@ -29,7 +29,12 @@ func GetDashboardPermissionList(c *models.ReqContext) Response {
return Error(500, "Failed to get dashboard permissions", err)
}
filteredAcls := make([]*models.DashboardAclInfoDTO, 0, len(acl))
for _, perm := range acl {
if dtos.IsHiddenUser(perm.UserLogin, c.SignedInUser, hs.Cfg) {
continue
}
perm.UserAvatarUrl = dtos.GetGravatarUrl(perm.UserEmail)
if perm.TeamId > 0 {
......@@ -38,12 +43,14 @@ func GetDashboardPermissionList(c *models.ReqContext) Response {
if perm.Slug != "" {
perm.Url = models.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
}
filteredAcls = append(filteredAcls, perm)
}
return JSON(200, acl)
return JSON(200, filteredAcls)
}
func UpdateDashboardPermissions(c *models.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response {
func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response {
if err := validatePermissionsUpdate(apiCmd); err != nil {
return Error(400, err.Error(), err)
}
......@@ -76,6 +83,12 @@ func UpdateDashboardPermissions(c *models.ReqContext, apiCmd dtos.UpdateDashboar
})
}
hiddenACL, err := g.GetHiddenACL(hs.Cfg)
if err != nil {
return Error(500, "Error while retrieving hidden permissions", err)
}
cmd.Items = append(cmd.Items, hiddenACL...)
if okToUpdate, err := g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate {
if err != nil {
if errors.Is(err, guardian.ErrGuardianPermissionExists) || errors.Is(err, guardian.ErrGuardianOverride) {
......
......@@ -12,8 +12,9 @@ import (
)
const (
testOrgID int64 = 1
testUserID int64 = 1
testOrgID int64 = 1
testUserID int64 = 1
testUserLogin string = "testUser"
)
func TestDataSourcesProxy_userLoggedIn(t *testing.T) {
......
......@@ -75,3 +75,15 @@ func GetGravatarUrlWithDefault(text string, defaultText string) string {
return GetGravatarUrl(text)
}
func IsHiddenUser(userLogin string, signedInUser *models.SignedInUser, cfg *setting.Cfg) bool {
if signedInUser.IsGrafanaAdmin || userLogin == signedInUser.Login {
return false
}
if _, hidden := cfg.HiddenUsers[userLogin]; hidden {
return true
}
return false
}
......@@ -12,7 +12,7 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func GetFolderPermissionList(c *models.ReqContext) Response {
func (hs *HTTPServer) GetFolderPermissionList(c *models.ReqContext) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
folder, err := s.GetFolderByUID(c.Params(":uid"))
......@@ -31,7 +31,12 @@ func GetFolderPermissionList(c *models.ReqContext) Response {
return Error(500, "Failed to get folder permissions", err)
}
filteredAcls := make([]*models.DashboardAclInfoDTO, 0, len(acl))
for _, perm := range acl {
if dtos.IsHiddenUser(perm.UserLogin, c.SignedInUser, hs.Cfg) {
continue
}
perm.FolderId = folder.Id
perm.DashboardId = 0
......@@ -44,12 +49,14 @@ func GetFolderPermissionList(c *models.ReqContext) Response {
if perm.Slug != "" {
perm.Url = models.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
}
filteredAcls = append(filteredAcls, perm)
}
return JSON(200, acl)
return JSON(200, filteredAcls)
}
func UpdateFolderPermissions(c *models.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response {
func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response {
if err := validatePermissionsUpdate(apiCmd); err != nil {
return Error(400, err.Error(), err)
}
......@@ -87,6 +94,12 @@ func UpdateFolderPermissions(c *models.ReqContext, apiCmd dtos.UpdateDashboardAc
})
}
hiddenACL, err := g.GetHiddenACL(hs.Cfg)
if err != nil {
return Error(500, "Error while retrieving hidden permissions", err)
}
cmd.Items = append(cmd.Items, hiddenACL...)
if okToUpdate, err := g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate {
if err != nil {
if errors.Is(err, guardian.ErrGuardianPermissionExists) ||
......
......@@ -53,8 +53,13 @@ func addOrgUserHelper(cmd models.AddOrgUserCommand) Response {
}
// GET /api/org/users
func GetOrgUsersForCurrentOrg(c *models.ReqContext) Response {
result, err := getOrgUsersHelper(c.OrgId, c.Query("query"), c.QueryInt("limit"))
func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *models.ReqContext) Response {
result, err := hs.getOrgUsersHelper(&models.GetOrgUsersQuery{
OrgId: c.OrgId,
Query: c.Query("query"),
Limit: c.QueryInt("limit"),
}, c.SignedInUser)
if err != nil {
return Error(500, "Failed to get users for current organization", err)
}
......@@ -63,7 +68,7 @@ func GetOrgUsersForCurrentOrg(c *models.ReqContext) Response {
}
// GET /api/org/users/lookup
func GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) Response {
func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) Response {
isAdmin, err := isOrgAdminFolderAdminOrTeamAdmin(c)
if err != nil {
return Error(500, "Failed to get users for current organization", err)
......@@ -73,7 +78,12 @@ func GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) Response {
return Error(403, "Permission denied", nil)
}
orgUsers, err := getOrgUsersHelper(c.OrgId, c.Query("query"), c.QueryInt("limit"))
orgUsers, err := hs.getOrgUsersHelper(&models.GetOrgUsersQuery{
OrgId: c.OrgId,
Query: c.Query("query"),
Limit: c.QueryInt("limit"),
}, c.SignedInUser)
if err != nil {
return Error(500, "Failed to get users for current organization", err)
}
......@@ -114,8 +124,13 @@ func isOrgAdminFolderAdminOrTeamAdmin(c *models.ReqContext) (bool, error) {
}
// GET /api/orgs/:orgId/users
func GetOrgUsers(c *models.ReqContext) Response {
result, err := getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0)
func (hs *HTTPServer) GetOrgUsers(c *models.ReqContext) Response {
result, err := hs.getOrgUsersHelper(&models.GetOrgUsersQuery{
OrgId: c.ParamsInt64(":orgId"),
Query: "",
Limit: 0,
}, c.SignedInUser)
if err != nil {
return Error(500, "Failed to get users for organization", err)
}
......@@ -123,22 +138,22 @@ func GetOrgUsers(c *models.ReqContext) Response {
return JSON(200, result)
}
func getOrgUsersHelper(orgID int64, query string, limit int) ([]*models.OrgUserDTO, error) {
q := models.GetOrgUsersQuery{
OrgId: orgID,
Query: query,
Limit: limit,
}
if err := bus.Dispatch(&q); err != nil {
func (hs *HTTPServer) getOrgUsersHelper(query *models.GetOrgUsersQuery, signedInUser *models.SignedInUser) ([]*models.OrgUserDTO, error) {
if err := bus.Dispatch(query); err != nil {
return nil, err
}
for _, user := range q.Result {
filteredUsers := make([]*models.OrgUserDTO, 0, len(query.Result))
for _, user := range query.Result {
if dtos.IsHiddenUser(user.Login, signedInUser, hs.Cfg) {
continue
}
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
filteredUsers = append(filteredUsers, user)
}
return q.Result, nil
return filteredUsers, nil
}
// PATCH /api/org/users/:userId
......
package api
import (
"encoding/json"
"net/http"
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setUpGetOrgUsersHandler() {
bus.AddHandler("test", func(query *models.GetOrgUsersQuery) error {
query.Result = []*models.OrgUserDTO{
{Email: "testUser@grafana.com", Login: testUserLogin},
{Email: "user1@grafana.com", Login: "user1"},
{Email: "user2@grafana.com", Login: "user2"},
}
return nil
})
}
func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
settings := setting.NewCfg()
hs := &HTTPServer{Cfg: settings}
loggedInUserScenario(t, "When calling GET on", "api/org/users", func(sc *scenarioContext) {
setUpGetOrgUsersHandler()
sc.handlerFunc = hs.GetOrgUsersForCurrentOrg
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
require.Equal(t, http.StatusOK, sc.resp.Code)
var resp []models.OrgUserDTO
err := json.Unmarshal(sc.resp.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Len(t, resp, 3)
})
loggedInUserScenario(t, "When calling GET as an editor with no team / folder permissions on",
"api/org/users/lookup", func(sc *scenarioContext) {
setUpGetOrgUsersHandler()
bus.AddHandler("test", func(query *models.HasAdminPermissionInFoldersQuery) error {
query.Result = false
return nil
})
bus.AddHandler("test", func(query *models.IsAdminOfTeamsQuery) error {
query.Result = false
return nil
})
sc.handlerFunc = hs.GetOrgUsersForCurrentOrgLookup
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
assert.Equal(t, http.StatusForbidden, sc.resp.Code)
var resp struct {
Message string
}
err := json.Unmarshal(sc.resp.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Equal(t, "Permission denied", resp.Message)
})
loggedInUserScenarioWithRole(t, "When calling GET as an admin on", "GET", "api/org/users/lookup",
"api/org/users/lookup", models.ROLE_ADMIN, func(sc *scenarioContext) {
setUpGetOrgUsersHandler()
sc.handlerFunc = hs.GetOrgUsersForCurrentOrgLookup
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
require.Equal(t, http.StatusOK, sc.resp.Code)
var resp []dtos.UserLookupDTO
err := json.Unmarshal(sc.resp.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Len(t, resp, 3)
})
t.Run("Given there is two hidden users", func(t *testing.T) {
settings.HiddenUsers = map[string]struct{}{
"user1": {},
testUserLogin: {},
}
t.Cleanup(func() { settings.HiddenUsers = make(map[string]struct{}) })
loggedInUserScenario(t, "When calling GET on", "api/org/users", func(sc *scenarioContext) {
setUpGetOrgUsersHandler()
sc.handlerFunc = hs.GetOrgUsersForCurrentOrg
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
require.Equal(t, http.StatusOK, sc.resp.Code)
var resp []models.OrgUserDTO
err := json.Unmarshal(sc.resp.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Len(t, resp, 2)
assert.Equal(t, testUserLogin, resp[0].Login)
assert.Equal(t, "user2", resp[1].Login)
})
loggedInUserScenarioWithRole(t, "When calling GET as an admin on", "GET", "api/org/users/lookup",
"api/org/users/lookup", models.ROLE_ADMIN, func(sc *scenarioContext) {
setUpGetOrgUsersHandler()
sc.handlerFunc = hs.GetOrgUsersForCurrentOrgLookup
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
require.Equal(t, http.StatusOK, sc.resp.Code)
var resp []dtos.UserLookupDTO
err := json.Unmarshal(sc.resp.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Len(t, resp, 2)
assert.Equal(t, testUserLogin, resp[0].Login)
assert.Equal(t, "user2", resp[1].Login)
})
})
}
......@@ -112,6 +112,8 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) Response {
UserIdFilter: userIdFilter,
Page: page,
Limit: perPage,
SignedInUser: c.SignedInUser,
HiddenUsers: hs.Cfg.HiddenUsers,
}
if err := bus.Dispatch(&query); err != nil {
......@@ -129,8 +131,13 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) Response {
}
// GET /api/teams/:teamId
func GetTeamByID(c *models.ReqContext) Response {
query := models.GetTeamByIdQuery{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}
func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) Response {
query := models.GetTeamByIdQuery{
OrgId: c.OrgId,
Id: c.ParamsInt64(":teamId"),
SignedInUser: c.SignedInUser,
HiddenUsers: hs.Cfg.HiddenUsers,
}
if err := bus.Dispatch(&query); err != nil {
if errors.Is(err, models.ErrTeamNotFound) {
......
......@@ -18,7 +18,12 @@ func (hs *HTTPServer) GetTeamMembers(c *models.ReqContext) Response {
return Error(500, "Failed to get Team Members", err)
}
filteredMembers := make([]*models.TeamMemberDTO, 0, len(query.Result))
for _, member := range query.Result {
if dtos.IsHiddenUser(member.Login, c.SignedInUser, hs.Cfg) {
continue
}
member.AvatarUrl = dtos.GetGravatarUrl(member.Email)
member.Labels = []string{}
......@@ -26,9 +31,11 @@ func (hs *HTTPServer) GetTeamMembers(c *models.ReqContext) Response {
authProvider := GetAuthProviderLabel(member.AuthModule)
member.Labels = append(member.Labels, authProvider)
}
filteredMembers = append(filteredMembers, member)
}
return JSON(200, query.Result)
return JSON(200, filteredMembers)
}
// POST /api/teams/:teamId/members
......
package api
import (
"encoding/json"
"net/http"
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setUpGetTeamMembersHandler() {
bus.AddHandler("test", func(query *models.GetTeamMembersQuery) error {
query.Result = []*models.TeamMemberDTO{
{Email: "testUser@grafana.com", Login: testUserLogin},
{Email: "user1@grafana.com", Login: "user1"},
{Email: "user2@grafana.com", Login: "user2"},
}
return nil
})
}
func TestTeamMembersAPIEndpoint_userLoggedIn(t *testing.T) {
settings := setting.NewCfg()
hs := &HTTPServer{
Cfg: settings,
License: &licensing.OSSLicensingService{},
}
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "api/teams/1/members",
"api/teams/:teamId/members", models.ROLE_ADMIN, func(sc *scenarioContext) {
setUpGetTeamMembersHandler()
sc.handlerFunc = hs.GetTeamMembers
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
require.Equal(t, http.StatusOK, sc.resp.Code)
var resp []models.TeamMemberDTO
err := json.Unmarshal(sc.resp.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Len(t, resp, 3)
})
t.Run("Given there is two hidden users", func(t *testing.T) {
settings.HiddenUsers = map[string]struct{}{
"user1": {},
testUserLogin: {},
}
t.Cleanup(func() { settings.HiddenUsers = make(map[string]struct{}) })
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "api/teams/1/members",
"api/teams/:teamId/members", models.ROLE_ADMIN, func(sc *scenarioContext) {
setUpGetTeamMembersHandler()
sc.handlerFunc = hs.GetTeamMembers
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
require.Equal(t, http.StatusOK, sc.resp.Code)
var resp []models.TeamMemberDTO
err := json.Unmarshal(sc.resp.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Len(t, resp, 2)
assert.Equal(t, testUserLogin, resp[0].Login)
assert.Equal(t, "user2", resp[1].Login)
})
})
}
......@@ -50,9 +50,11 @@ type DeleteTeamCommand struct {
}
type GetTeamByIdQuery struct {
OrgId int64
Id int64
Result *TeamDTO
OrgId int64
Id int64
SignedInUser *SignedInUser
HiddenUsers map[string]struct{}
Result *TeamDTO
}
type GetTeamsByUserQuery struct {
......@@ -68,6 +70,8 @@ type SearchTeamsQuery struct {
Page int
OrgId int64
UserIdFilter int64
SignedInUser *SignedInUser
HiddenUsers map[string]struct{}
Result SearchTeamQueryResult
}
......
......@@ -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 {
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 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.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 = ? `
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() string {
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)
})
})
})
}
......@@ -308,6 +308,7 @@ type Cfg struct {
// User
UserInviteMaxLifetime time.Duration
HiddenUsers map[string]struct{}
// Annotations
AlertingAnnotationCleanupSetting AnnotationCleanupSettings
......@@ -1118,6 +1119,13 @@ func readUserSettings(iniFile *ini.File, cfg *Cfg) error {
return errors.New("the minimum supported value for the `user_invite_max_lifetime_duration` configuration is 15m (15 minutes)")
}
cfg.HiddenUsers = make(map[string]struct{})
hiddenUsers := users.Key("hidden_users").MustString("")
for _, user := range strings.Split(hiddenUsers, ",") {
user = strings.TrimSpace(user)
cfg.HiddenUsers[user] = struct{}{}
}
return nil
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment