diff --git a/docs/sources/http_api/team.md b/docs/sources/http_api/team.md index d9d0135fdc6..f530a6f2cf4 100644 --- a/docs/sources/http_api/team.md +++ b/docs/sources/http_api/team.md @@ -7,7 +7,13 @@ aliases = ["/docs/grafana/latest/http_api/team/"] # Team API -This API can be used to create/update/delete Teams and to add/remove users to Teams. All actions require that the user has the Admin role for the organization. +This API can be used to manage Teams and Team Memberships. + +Access to these API endpoints is restricted as follows: + +- All authenticated users are able to view details of teams they are a member of. +- Organization Admins are able to manage all teams and team members. +- If the `editors_can_admin` configuration flag is enabled, Organization Editors are able to view details of all teams and to manage teams that they are Admin members of. ## Team Search With Paging diff --git a/pkg/api/api.go b/pkg/api/api.go index fcaac446ec7..057d6456949 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -29,7 +29,7 @@ func (hs *HTTPServer) registerRoutes() { reqEditorRole := middleware.ReqEditorRole reqOrgAdmin := middleware.ReqOrgAdmin reqOrgAdminFolderAdminOrTeamAdmin := middleware.OrgAdminFolderAdminOrTeamAdmin - reqCanAccessTeams := middleware.AdminOrFeatureEnabled(hs.Cfg.EditorsCanAdmin) + reqCanAccessTeams := middleware.AdminOrEditorAndFeatureEnabled(hs.Cfg.EditorsCanAdmin) reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn(hs.Cfg) redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL(hs.Cfg) authorize := acmiddleware.Middleware(hs.AccessControl) diff --git a/pkg/api/team.go b/pkg/api/team.go index 4eb4e18d10c..29caf56dd21 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -2,6 +2,8 @@ package api import ( "errors" + "net/http" + "strconv" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" @@ -10,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/teamguardian" "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/web" ) // POST /api/teams @@ -95,16 +98,11 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { page = 1 } - var userIdFilter int64 - if hs.Cfg.EditorsCanAdmin && c.OrgRole != models.ROLE_ADMIN { - userIdFilter = c.SignedInUser.UserId - } - query := models.SearchTeamsQuery{ OrgId: c.OrgId, Query: c.Query("query"), Name: c.Query("name"), - UserIdFilter: userIdFilter, + UserIdFilter: userFilter(hs.Cfg.EditorsCanAdmin, c), Page: page, Limit: perPage, SignedInUser: c.SignedInUser, @@ -125,13 +123,32 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { return response.JSON(200, query.Result) } +// UserFilter returns the user ID used in a filter when querying a team +// 1. If the user is a viewer or editor, this will return the user's ID. +// 2. If EditorsCanAdmin is enabled and the user is an editor, this will return models.FilterIgnoreUser (0) +// 3. If the user is an admin, this will return models.FilterIgnoreUser (0) +func userFilter(editorsCanAdmin bool, c *models.ReqContext) int64 { + userIdFilter := c.SignedInUser.UserId + if (editorsCanAdmin && c.OrgRole == models.ROLE_EDITOR) || c.OrgRole == models.ROLE_ADMIN { + userIdFilter = models.FilterIgnoreUser + } + + return userIdFilter +} + // GET /api/teams/:teamId func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response { + teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) + if err != nil { + return response.Error(http.StatusBadRequest, "teamId is invalid", err) + } + query := models.GetTeamByIdQuery{ OrgId: c.OrgId, - Id: c.ParamsInt64(":teamId"), + Id: teamId, SignedInUser: c.SignedInUser, HiddenUsers: hs.Cfg.HiddenUsers, + UserIdFilter: userFilter(hs.Cfg.EditorsCanAdmin, c), } if err := bus.DispatchCtx(c.Req.Context(), &query); err != nil { diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index 01cd3e91e8c..5d5578a142a 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -16,6 +16,10 @@ import ( func (hs *HTTPServer) GetTeamMembers(c *models.ReqContext) response.Response { query := models.GetTeamMembersQuery{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId")} + if err := teamguardian.CanAdmin(hs.Bus, query.OrgId, query.TeamId, c.SignedInUser); err != nil { + return response.Error(403, "Not allowed to list team members", err) + } + if err := bus.DispatchCtx(c.Req.Context(), &query); err != nil { return response.Error(500, "Failed to get Team Members", err) } diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 08e60936e5f..a3ff1276639 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -128,20 +128,22 @@ func Auth(options *AuthOptions) web.Handler { } } -// AdminOrFeatureEnabled creates a middleware that allows access -// if the signed in user is either an Org Admin or if the -// feature flag is enabled. +// AdminOrEditorAndFeatureEnabled creates a middleware that allows +// access if the signed in user is either an Org Admin or if they +// are an Org Editor and the feature flag is enabled. // Intended for when feature flags open up access to APIs that // are otherwise only available to admins. -func AdminOrFeatureEnabled(enabled bool) web.Handler { +func AdminOrEditorAndFeatureEnabled(enabled bool) web.Handler { return func(c *models.ReqContext) { if c.OrgRole == models.ROLE_ADMIN { return } - if !enabled { - accessForbidden(c) + if c.OrgRole == models.ROLE_EDITOR && enabled { + return } + + accessForbidden(c) } } diff --git a/pkg/models/team.go b/pkg/models/team.go index 328e1815b90..8fe1ac47fd6 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -55,8 +55,12 @@ type GetTeamByIdQuery struct { SignedInUser *SignedInUser HiddenUsers map[string]struct{} Result *TeamDTO + UserIdFilter int64 } +// FilterIgnoreUser is used in a get / search teams query when the caller does not want to filter teams by user ID / membership +const FilterIgnoreUser int64 = 0 + type GetTeamsByUserQuery struct { OrgId int64 UserId int64 `json:"userId"` diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index cc299cd1b16..a2e983102a9 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -52,18 +52,6 @@ func getTeamMemberCount(filteredUsers []string) string { 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, @@ -182,17 +170,15 @@ func SearchTeams(ctx context.Context, query *models.SearchTeamsQuery) error { params := make([]interface{}, 0) filteredUsers := getFilteredUsers(query.SignedInUser, query.HiddenUsers) - if query.UserIdFilter > 0 { - sql.WriteString(getTeamSearchSQLBase(filteredUsers)) - for _, user := range filteredUsers { - params = append(params, user) - } + sql.WriteString(getTeamSelectSQLBase(filteredUsers)) + + for _, user := range filteredUsers { + params = append(params, user) + } + + if query.UserIdFilter != models.FilterIgnoreUser { + sql.WriteString(` INNER JOIN team_member ON team.id = team_member.team_id AND team_member.user_id = ?`) params = append(params, query.UserIdFilter) - } else { - sql.WriteString(getTeamSelectSQLBase(filteredUsers)) - for _, user := range filteredUsers { - params = append(params, user) - } } sql.WriteString(` WHERE team.org_id = ?`) @@ -221,6 +207,8 @@ func SearchTeams(ctx context.Context, query *models.SearchTeamsQuery) error { team := models.Team{} countSess := x.Table("team") + countSess.Where("team.org_id=?", query.OrgId) + if query.Query != "" { countSess.Where(`name `+dialect.LikeStr()+` ?`, queryWithWildcards) } @@ -229,6 +217,18 @@ func SearchTeams(ctx context.Context, query *models.SearchTeamsQuery) error { countSess.Where("name=?", query.Name) } + // If we're not retrieving all results, then only search for teams that this user has access to + if query.UserIdFilter != models.FilterIgnoreUser { + countSess. + Where(` + team.id IN ( + SELECT + team_id + FROM team_member + WHERE team_member.user_id = ? + )`, query.UserIdFilter) + } + count, err := countSess.Count(&team) query.Result.TotalCount = count @@ -245,6 +245,11 @@ func GetTeamById(ctx context.Context, query *models.GetTeamByIdQuery) error { params = append(params, user) } + if query.UserIdFilter != models.FilterIgnoreUser { + sql.WriteString(` INNER JOIN team_member ON team.id = team_member.team_id AND team_member.user_id = ?`) + params = append(params, query.UserIdFilter) + } + sql.WriteString(` WHERE team.org_id = ? and team.id = ?`) params = append(params, query.OrgId, query.Id)