Teams: Move team API to own service (#76347)

* move team API to its own service

* remove uneeded import

* reshare pref api logic
This commit is contained in:
Jo
2023-10-12 10:10:54 +02:00
committed by GitHub
parent 665dc1fa44
commit 466f8a1f5a
12 changed files with 343 additions and 223 deletions
-19
View File
@@ -270,25 +270,6 @@ func (hs *HTTPServer) registerRoutes() {
usersRoute.Post("/:id/using/:orgId", authorize(ac.EvalPermission(ac.ActionUsersWrite, userIDScope)), routing.Wrap(hs.UpdateUserActiveOrg))
}, requestmeta.SetOwner(requestmeta.TeamAuth))
// team (admin permission required)
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
teamsRoute.Post("/", authorize(ac.EvalPermission(ac.ActionTeamsCreate)), routing.Wrap(hs.CreateTeam))
teamsRoute.Put("/:teamId", authorize(ac.EvalPermission(ac.ActionTeamsWrite, ac.ScopeTeamsID)), routing.Wrap(hs.UpdateTeam))
teamsRoute.Delete("/:teamId", authorize(ac.EvalPermission(ac.ActionTeamsDelete, ac.ScopeTeamsID)), routing.Wrap(hs.DeleteTeamByID))
teamsRoute.Get("/:teamId/members", authorize(ac.EvalPermission(ac.ActionTeamsPermissionsRead, ac.ScopeTeamsID)), routing.Wrap(hs.GetTeamMembers))
teamsRoute.Post("/:teamId/members", authorize(ac.EvalPermission(ac.ActionTeamsPermissionsWrite, ac.ScopeTeamsID)), routing.Wrap(hs.AddTeamMember))
teamsRoute.Put("/:teamId/members/:userId", authorize(ac.EvalPermission(ac.ActionTeamsPermissionsWrite, ac.ScopeTeamsID)), routing.Wrap(hs.UpdateTeamMember))
teamsRoute.Delete("/:teamId/members/:userId", authorize(ac.EvalPermission(ac.ActionTeamsPermissionsWrite, ac.ScopeTeamsID)), routing.Wrap(hs.RemoveTeamMember))
teamsRoute.Get("/:teamId/preferences", authorize(ac.EvalPermission(ac.ActionTeamsRead, ac.ScopeTeamsID)), routing.Wrap(hs.GetTeamPreferences))
teamsRoute.Put("/:teamId/preferences", authorize(ac.EvalPermission(ac.ActionTeamsWrite, ac.ScopeTeamsID)), routing.Wrap(hs.UpdateTeamPreferences))
}, requestmeta.SetOwner(requestmeta.TeamAuth))
// team without requirement of user to be org admin
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
teamsRoute.Get("/:teamId", authorize(ac.EvalPermission(ac.ActionTeamsRead, ac.ScopeTeamsID)), routing.Wrap(hs.GetTeamByID))
teamsRoute.Get("/search", authorize(ac.EvalPermission(ac.ActionTeamsRead)), routing.Wrap(hs.SearchTeams))
}, requestmeta.SetOwner(requestmeta.TeamAuth))
// org information available to all users.
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Get("/", authorize(ac.EvalPermission(ac.ActionOrgsRead)), routing.Wrap(hs.GetCurrentOrg))
-1
View File
@@ -214,7 +214,6 @@ func setupScenarioContext(t *testing.T, url string) *scenarioContext {
return sc
}
// FIXME: This user should not be anonymous
func authedUserWithPermissions(userID, orgID int64, permissions []accesscontrol.Permission) *user.SignedInUser {
return &user.SignedInUser{UserID: userID, OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)}}
}
+1 -3
View File
@@ -170,7 +170,6 @@ type HTTPServer struct {
queryDataService query.Service
serviceAccountsService serviceaccounts.Service
authInfoService login.AuthInfoService
teamPermissionsService accesscontrol.TeamPermissionsService
NotificationService *notifications.NotificationService
DashboardService dashboards.DashboardService
dashboardProvisioningService dashboards.DashboardProvisioningService
@@ -236,7 +235,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
dsGuardian guardian.DatasourceGuardianProvider, alertNotificationService *alerting.AlertNotificationService,
dashboardsnapshotsService dashboardsnapshots.Service, pluginSettings pluginSettings.Service,
avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service,
teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService,
folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
starService star.Service, csrfService csrf.Service, basekinds *corekind.Base,
playlistService playlist.Service, apiKeyService apikey.Service, kvStore kvstore.KVStore,
@@ -319,7 +318,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
dashboardProvisioningService: dashboardProvisioningService,
folderService: folderService,
dsGuardian: dsGuardian,
teamPermissionsService: teamsPermissionsService,
AlertNotificationService: alertNotificationService,
dashboardsnapshotsService: dashboardsnapshotsService,
PluginSettings: pluginSettings,
+6 -94
View File
@@ -11,6 +11,7 @@ import (
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/preference/prefapi"
"github.com/grafana/grafana/pkg/web"
)
@@ -68,56 +69,7 @@ func (hs *HTTPServer) GetUserPreferences(c *contextmodel.ReqContext) response.Re
return response.Error(http.StatusInternalServerError, "Failed to get user preferences", errID)
}
return hs.getPreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), userID, 0)
}
func (hs *HTTPServer) getPreferencesFor(ctx context.Context, orgID, userID, teamID int64) response.Response {
prefsQuery := pref.GetPreferenceQuery{UserID: userID, OrgID: orgID, TeamID: teamID}
preference, err := hs.preferenceService.Get(ctx, &prefsQuery)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to get preferences", err)
}
var dashboardUID string
// when homedashboardID is 0, that means it is the default home dashboard, no UID would be returned in the response
if preference.HomeDashboardID != 0 {
query := dashboards.GetDashboardQuery{ID: preference.HomeDashboardID, OrgID: orgID}
queryResult, err := hs.DashboardService.GetDashboard(ctx, &query)
if err == nil {
dashboardUID = queryResult.UID
}
}
dto := preferences.Spec{}
if preference.WeekStart != nil && *preference.WeekStart != "" {
dto.WeekStart = preference.WeekStart
}
if preference.Theme != "" {
dto.Theme = &preference.Theme
}
if dashboardUID != "" {
dto.HomeDashboardUID = &dashboardUID
}
if preference.Timezone != "" {
dto.Timezone = &preference.Timezone
}
if preference.JSONData != nil {
if preference.JSONData.Language != "" {
dto.Language = &preference.JSONData.Language
}
if preference.JSONData.QueryHistory.HomeTab != "" {
dto.QueryHistory = &preferences.QueryHistoryPreference{
HomeTab: &preference.JSONData.QueryHistory.HomeTab,
}
}
}
return response.JSON(http.StatusOK, &dto)
return prefapi.GetPreferencesFor(c.Req.Context(), hs.DashboardService, hs.preferenceService, c.SignedInUser.GetOrgID(), userID, 0)
}
// swagger:route PUT /user/preferences user_preferences updateUserPreferences
@@ -142,48 +94,8 @@ func (hs *HTTPServer) UpdateUserPreferences(c *contextmodel.ReqContext) response
return response.Error(http.StatusInternalServerError, "Failed to update user preferences", errID)
}
return hs.updatePreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), userID, 0, &dtoCmd)
}
func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, teamId int64, dtoCmd *dtos.UpdatePrefsCmd) response.Response {
if dtoCmd.Theme != "" && !pref.IsValidThemeID(dtoCmd.Theme) {
return response.Error(http.StatusBadRequest, "Invalid theme", nil)
}
dashboardID := dtoCmd.HomeDashboardID
if dtoCmd.HomeDashboardUID != nil {
query := dashboards.GetDashboardQuery{UID: *dtoCmd.HomeDashboardUID, OrgID: orgID}
if query.UID == "" {
// clear the value
dashboardID = 0
} else {
queryResult, err := hs.DashboardService.GetDashboard(ctx, &query)
if err != nil {
return response.Error(http.StatusNotFound, "Dashboard not found", err)
}
dashboardID = queryResult.ID
}
}
dtoCmd.HomeDashboardID = dashboardID
saveCmd := pref.SavePreferenceCommand{
UserID: userID,
OrgID: orgID,
TeamID: teamId,
Theme: dtoCmd.Theme,
Language: dtoCmd.Language,
Timezone: dtoCmd.Timezone,
WeekStart: dtoCmd.WeekStart,
HomeDashboardID: dtoCmd.HomeDashboardID,
QueryHistory: dtoCmd.QueryHistory,
CookiePreferences: dtoCmd.Cookies,
}
if err := hs.preferenceService.Save(ctx, &saveCmd); err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to save preferences", err)
}
return response.Success("Preferences updated")
return prefapi.UpdatePreferencesFor(c.Req.Context(), hs.DashboardService,
hs.preferenceService, c.SignedInUser.GetOrgID(), userID, 0, &dtoCmd)
}
// swagger:route PATCH /user/preferences user_preferences patchUserPreferences
@@ -262,7 +174,7 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) GetOrgPreferences(c *contextmodel.ReqContext) response.Response {
return hs.getPreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, 0)
return prefapi.GetPreferencesFor(c.Req.Context(), hs.DashboardService, hs.preferenceService, c.SignedInUser.GetOrgID(), 0, 0)
}
// swagger:route PUT /org/preferences org_preferences updateOrgPreferences
@@ -281,7 +193,7 @@ func (hs *HTTPServer) UpdateOrgPreferences(c *contextmodel.ReqContext) response.
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return hs.updatePreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, 0, &dtoCmd)
return prefapi.UpdatePreferencesFor(c.Req.Context(), hs.DashboardService, hs.preferenceService, c.SignedInUser.GetOrgID(), 0, 0, &dtoCmd)
}
// swagger:route PATCH /org/preferences org_preferences patchOrgPreferences
-357
View File
@@ -1,357 +0,0 @@
package api
import (
"errors"
"net/http"
"strconv"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/team/sortopts"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
// swagger:route POST /teams teams createTeam
//
// Add Team.
//
// Responses:
// 200: createTeamResponse
// 401: unauthorisedError
// 403: forbiddenError
// 409: conflictError
// 500: internalServerError
func (hs *HTTPServer) CreateTeam(c *contextmodel.ReqContext) response.Response {
cmd := team.CreateTeamCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
t, err := hs.teamService.CreateTeam(cmd.Name, cmd.Email, c.SignedInUser.GetOrgID())
if err != nil {
if errors.Is(err, team.ErrTeamNameTaken) {
return response.Error(http.StatusConflict, "Team name taken", err)
}
return response.Error(http.StatusInternalServerError, "Failed to create Team", err)
}
// Clear permission cache for the user who's created the team, so that new permissions are fetched for their next call
// Required for cases when caller wants to immediately interact with the newly created object
hs.accesscontrolService.ClearUserPermissionCache(c.SignedInUser)
// if the request is authenticated using API tokens
// the SignedInUser is an empty struct therefore
// an additional check whether it is an actual user is required
namespace, identifier := c.SignedInUser.GetNamespacedID()
switch namespace {
case identity.NamespaceUser:
userID, err := strconv.ParseInt(identifier, 10, 64)
if err != nil {
c.Logger.Error("Could not add creator to team because user id is not a number", "error", err)
break
}
if err := addOrUpdateTeamMember(c.Req.Context(), hs.teamPermissionsService, userID, c.SignedInUser.GetOrgID(),
t.ID, dashboards.PERMISSION_ADMIN.String()); err != nil {
c.Logger.Error("Could not add creator to team", "error", err)
}
default:
c.Logger.Warn("Could not add creator to team because is not a real user")
}
return response.JSON(http.StatusOK, &util.DynMap{
"teamId": t.ID,
"message": "Team created",
})
}
// swagger:route PUT /teams/{team_id} teams updateTeam
//
// Update Team.
//
// Responses:
// 200: okResponse
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 409: conflictError
// 500: internalServerError
func (hs *HTTPServer) UpdateTeam(c *contextmodel.ReqContext) response.Response {
cmd := team.UpdateTeamCommand{}
var err error
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
cmd.OrgID = c.SignedInUser.GetOrgID()
cmd.ID, err = strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
}
if err := hs.teamService.UpdateTeam(c.Req.Context(), &cmd); err != nil {
if errors.Is(err, team.ErrTeamNameTaken) {
return response.Error(http.StatusBadRequest, "Team name taken", err)
}
return response.Error(http.StatusInternalServerError, "Failed to update Team", err)
}
return response.Success("Team updated")
}
// swagger:route DELETE /teams/{team_id} teams deleteTeamByID
//
// Delete Team By ID.
//
// Responses:
// 200: okResponse
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) DeleteTeamByID(c *contextmodel.ReqContext) response.Response {
orgID := c.SignedInUser.GetOrgID()
teamID, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
}
if err := hs.teamService.DeleteTeam(c.Req.Context(), &team.DeleteTeamCommand{OrgID: orgID, ID: teamID}); err != nil {
if errors.Is(err, team.ErrTeamNotFound) {
return response.Error(http.StatusNotFound, "Failed to delete Team. ID not found", nil)
}
return response.Error(http.StatusInternalServerError, "Failed to delete Team", err)
}
return response.Success("Team deleted")
}
// swagger:route GET /teams/search teams searchTeams
//
// Team Search With Paging.
//
// Responses:
// 200: searchTeamsResponse
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) SearchTeams(c *contextmodel.ReqContext) response.Response {
perPage := c.QueryInt("perpage")
if perPage <= 0 {
perPage = 1000
}
page := c.QueryInt("page")
if page < 1 {
page = 1
}
sortOpts, err := sortopts.ParseSortQueryParam(c.Query("sort"))
if err != nil {
return response.Err(err)
}
query := team.SearchTeamsQuery{
OrgID: c.SignedInUser.GetOrgID(),
Query: c.Query("query"),
Name: c.Query("name"),
Page: page,
Limit: perPage,
SignedInUser: c.SignedInUser,
HiddenUsers: hs.Cfg.HiddenUsers,
SortOpts: sortOpts,
}
queryResult, err := hs.teamService.SearchTeams(c.Req.Context(), &query)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to search Teams", err)
}
teamIDs := map[string]bool{}
for _, team := range queryResult.Teams {
team.AvatarURL = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
teamIDs[strconv.FormatInt(team.ID, 10)] = true
}
metadata := hs.getMultiAccessControlMetadata(c, "teams:id:", teamIDs)
if len(metadata) > 0 {
for _, team := range queryResult.Teams {
team.AccessControl = metadata[strconv.FormatInt(team.ID, 10)]
}
}
queryResult.Page = page
queryResult.PerPage = perPage
return response.JSON(http.StatusOK, queryResult)
}
// swagger:route GET /teams/{team_id} teams getTeamByID
//
// Get Team By ID.
//
// Responses:
// 200: getTeamByIDResponse
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) GetTeamByID(c *contextmodel.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 := team.GetTeamByIDQuery{
OrgID: c.SignedInUser.GetOrgID(),
ID: teamId,
SignedInUser: c.SignedInUser,
HiddenUsers: hs.Cfg.HiddenUsers,
}
queryResult, err := hs.teamService.GetTeamByID(c.Req.Context(), &query)
if err != nil {
if errors.Is(err, team.ErrTeamNotFound) {
return response.Error(http.StatusNotFound, "Team not found", err)
}
return response.Error(http.StatusInternalServerError, "Failed to get Team", err)
}
// Add accesscontrol metadata
queryResult.AccessControl = hs.getAccessControlMetadata(c, c.SignedInUser.GetOrgID(), "teams:id:", strconv.FormatInt(queryResult.ID, 10))
queryResult.AvatarURL = dtos.GetGravatarUrlWithDefault(queryResult.Email, queryResult.Name)
return response.JSON(http.StatusOK, &queryResult)
}
// swagger:route GET /teams/{team_id}/preferences teams getTeamPreferences
//
// Get Team Preferences.
//
// Responses:
// 200: getPreferencesResponse
// 401: unauthorisedError
// 500: internalServerError
func (hs *HTTPServer) GetTeamPreferences(c *contextmodel.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)
}
return hs.getPreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, teamId)
}
// swagger:route PUT /teams/{team_id}/preferences teams updateTeamPreferences
//
// Update Team Preferences.
//
// Responses:
// 200: okResponse
// 400: badRequestError
// 401: unauthorisedError
// 500: internalServerError
func (hs *HTTPServer) UpdateTeamPreferences(c *contextmodel.ReqContext) response.Response {
dtoCmd := dtos.UpdatePrefsCmd{}
if err := web.Bind(c.Req, &dtoCmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
}
return hs.updatePreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, teamId, &dtoCmd)
}
// swagger:parameters updateTeamPreferences
type UpdateTeamPreferencesParams struct {
// in:path
// required:true
TeamID string `json:"team_id"`
// in:body
// required:true
Body dtos.UpdatePrefsCmd `json:"body"`
}
// swagger:parameters getTeamByID
type GetTeamByIDParams struct {
// in:path
// required:true
TeamID string `json:"team_id"`
}
// swagger:parameters deleteTeamByID
type DeleteTeamByIDParams struct {
// in:path
// required:true
TeamID string `json:"team_id"`
}
// swagger:parameters getTeamPreferences
type GetTeamPreferencesParams struct {
// in:path
// required:true
TeamID string `json:"team_id"`
}
// swagger:parameters searchTeams
type SearchTeamsParams struct {
// in:query
// required:false
// default: 1
Page int `json:"page"`
// Number of items per page
// The totalCount field in the response can be used for pagination list E.g. if totalCount is equal to 100 teams and the perpage parameter is set to 10 then there are 10 pages of teams.
// in:query
// required:false
// default: 1000
PerPage int `json:"perpage"`
Name string `json:"name"`
// If set it will return results where the query value is contained in the name field. Query values with spaces need to be URL encoded.
// required:false
Query string `json:"query"`
}
// swagger:parameters createTeam
type CreateTeamParams struct {
// in:body
// required:true
Body team.CreateTeamCommand `json:"body"`
}
// swagger:parameters updateTeam
type UpdateTeamParams struct {
// in:body
// required:true
Body team.UpdateTeamCommand `json:"body"`
// in:path
// required:true
TeamID string `json:"team_id"`
}
// swagger:response searchTeamsResponse
type SearchTeamsResponse struct {
// The response message
// in: body
Body team.SearchTeamQueryResult `json:"body"`
}
// swagger:response getTeamByIDResponse
type GetTeamByIDResponse struct {
// The response message
// in: body
Body *team.TeamDTO `json:"body"`
}
// swagger:response createTeamResponse
type CreateTeamResponse struct {
// The response message
// in: body
Body struct {
TeamId int64 `json:"teamId"`
Message string `json:"message"`
} `json:"body"`
}
-246
View File
@@ -1,246 +0,0 @@
package api
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/services/accesscontrol"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
// swagger:route GET /teams/{team_id}/members teams getTeamMembers
//
// Get Team Members.
//
// Responses:
// 200: getTeamMembersResponse
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) GetTeamMembers(c *contextmodel.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 := team.GetTeamMembersQuery{OrgID: c.SignedInUser.GetOrgID(), TeamID: teamId, SignedInUser: c.SignedInUser}
queryResult, err := hs.teamService.GetTeamMembers(c.Req.Context(), &query)
if err != nil {
return response.Error(500, "Failed to get Team Members", err)
}
filteredMembers := make([]*team.TeamMemberDTO, 0, len(queryResult))
for _, member := range queryResult {
if dtos.IsHiddenUser(member.Login, c.SignedInUser, hs.Cfg) {
continue
}
member.AvatarURL = dtos.GetGravatarUrl(member.Email)
member.Labels = []string{}
if hs.License.FeatureEnabled("teamgroupsync") && member.External {
authProvider := login.GetAuthProviderLabel(member.AuthModule)
member.Labels = append(member.Labels, authProvider)
}
filteredMembers = append(filteredMembers, member)
}
return response.JSON(http.StatusOK, filteredMembers)
}
// swagger:route POST /teams/{team_id}/members teams addTeamMember
//
// Add Team Member.
//
// Responses:
// 200: okResponse
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) AddTeamMember(c *contextmodel.ReqContext) response.Response {
cmd := team.AddTeamMemberCommand{}
var err error
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
cmd.OrgID = c.SignedInUser.GetOrgID()
cmd.TeamID, err = strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
}
isTeamMember, err := hs.teamService.IsTeamMember(c.SignedInUser.GetOrgID(), cmd.TeamID, cmd.UserID)
if err != nil {
return response.Error(500, "Failed to add team member.", err)
}
if isTeamMember {
return response.Error(400, "User is already added to this team", nil)
}
err = addOrUpdateTeamMember(c.Req.Context(), hs.teamPermissionsService, cmd.UserID, cmd.OrgID, cmd.TeamID, getPermissionName(cmd.Permission))
if err != nil {
return response.Error(500, "Failed to add Member to Team", err)
}
return response.JSON(http.StatusOK, &util.DynMap{
"message": "Member added to Team",
})
}
// swagger:route PUT /teams/{team_id}/members/{user_id} teams updateTeamMember
//
// Update Team Member.
//
// Responses:
// 200: okResponse
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) UpdateTeamMember(c *contextmodel.ReqContext) response.Response {
cmd := team.UpdateTeamMemberCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
}
userId, err := strconv.ParseInt(web.Params(c.Req)[":userId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "userId is invalid", err)
}
orgId := c.SignedInUser.GetOrgID()
isTeamMember, err := hs.teamService.IsTeamMember(orgId, teamId, userId)
if err != nil {
return response.Error(500, "Failed to update team member.", err)
}
if !isTeamMember {
return response.Error(404, "Team member not found.", nil)
}
err = addOrUpdateTeamMember(c.Req.Context(), hs.teamPermissionsService, userId, orgId, teamId, getPermissionName(cmd.Permission))
if err != nil {
return response.Error(500, "Failed to update team member.", err)
}
return response.Success("Team member updated")
}
func getPermissionName(permission dashboards.PermissionType) string {
permissionName := permission.String()
// Team member permission is 0, which maps to an empty string.
// However, we want the team permission service to display "Member" for team members. This is a hack to make it work.
if permissionName == "" {
permissionName = "Member"
}
return permissionName
}
// swagger:route DELETE /teams/{team_id}/members/{user_id} teams removeTeamMember
//
// Remove Member From Team.
//
// Responses:
// 200: okResponse
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) RemoveTeamMember(c *contextmodel.ReqContext) response.Response {
orgId := c.SignedInUser.GetOrgID()
teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
}
userId, err := strconv.ParseInt(web.Params(c.Req)[":userId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "userId is invalid", err)
}
teamIDString := strconv.FormatInt(teamId, 10)
if _, err := hs.teamPermissionsService.SetUserPermission(c.Req.Context(), orgId, accesscontrol.User{ID: userId}, teamIDString, ""); err != nil {
if errors.Is(err, team.ErrTeamNotFound) {
return response.Error(404, "Team not found", nil)
}
if errors.Is(err, team.ErrTeamMemberNotFound) {
return response.Error(404, "Team member not found", nil)
}
return response.Error(500, "Failed to remove Member from Team", err)
}
return response.Success("Team Member removed")
}
// addOrUpdateTeamMember adds or updates a team member.
//
// Stubbable by tests.
var addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService accesscontrol.TeamPermissionsService, userID, orgID, teamID int64, permission string) error {
teamIDString := strconv.FormatInt(teamID, 10)
if _, err := resourcePermissionService.SetUserPermission(ctx, orgID, accesscontrol.User{ID: userID}, teamIDString, permission); err != nil {
return fmt.Errorf("failed setting permissions for user %d in team %d: %w", userID, teamID, err)
}
return nil
}
// swagger:parameters getTeamMembers
type GetTeamMembersParams struct {
// in:path
// required:true
TeamID string `json:"team_id"`
}
// swagger:parameters addTeamMember
type AddTeamMemberParams struct {
// in:body
// required:true
Body team.AddTeamMemberCommand `json:"body"`
// in:path
// required:true
TeamID string `json:"team_id"`
}
// swagger:parameters updateTeamMember
type UpdateTeamMemberParams struct {
// in:body
// required:true
Body team.UpdateTeamMemberCommand `json:"body"`
// in:path
// required:true
TeamID string `json:"team_id"`
// in:path
// required:true
UserID int64 `json:"user_id"`
}
// swagger:parameters removeTeamMember
type RemoveTeamMemberParams struct {
// in:path
// required:true
TeamID string `json:"team_id"`
// in:path
// required:true
UserID int64 `json:"user_id"`
}
// swagger:response getTeamMembersResponse
type GetTeamMembersResponse struct {
// The response message
// in: body
Body []*team.TeamMemberDTO `json:"body"`
}
-133
View File
@@ -1,133 +0,0 @@
package api
import (
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/team/teamtest"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
)
func TestAddTeamMembersAPIEndpoint(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.teamService = teamtest.NewFakeService()
hs.teamPermissionsService = &actest.FakePermissionsService{}
})
t.Run("should be able to add team member with correct permission", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
server.NewRequest(http.MethodPost, "/api/teams/1/members", strings.NewReader("{\"userId\": 1}")),
userWithPermissions(1, []ac.Permission{{Action: ac.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
)
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("should not be able to add team member without correct permission", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
server.NewRequest(http.MethodPost, "/api/teams/1/members", strings.NewReader("{\"userId\": 1}")),
userWithPermissions(1, []ac.Permission{{Action: ac.ActionTeamsPermissionsWrite, Scope: "teams:id:2"}}),
)
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
func TestGetTeamMembersAPIEndpoint(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.teamService = teamtest.NewFakeService()
hs.teamPermissionsService = &actest.FakePermissionsService{}
})
t.Run("should be able to get team members with correct permission", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
server.NewGetRequest("/api/teams/1/members"),
userWithPermissions(1, []ac.Permission{{Action: ac.ActionTeamsPermissionsRead, Scope: "teams:id:1"}}),
)
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("should not be able to get team members without correct permission", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
server.NewGetRequest("/api/teams/1/members"),
userWithPermissions(1, []ac.Permission{{Action: ac.ActionTeamsPermissionsRead, Scope: "teams:id:2"}}),
)
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
func TestUpdateTeamMembersAPIEndpoint(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.teamService = &teamtest.FakeService{ExpectedIsMember: true}
hs.teamPermissionsService = &actest.FakePermissionsService{}
})
t.Run("should be able to update team member with correct permission", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
server.NewRequest(http.MethodPut, "/api/teams/1/members/1", strings.NewReader("{\"permission\": 1}")),
userWithPermissions(1, []ac.Permission{{Action: ac.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
)
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("should not be able to update team member without correct permission", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
server.NewRequest(http.MethodPut, "/api/teams/1/members/1", strings.NewReader("{\"permission\": 1}")),
userWithPermissions(1, []ac.Permission{{Action: ac.ActionTeamsPermissionsWrite, Scope: "teams:id:2"}}),
)
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
func TestDeleteTeamMembersAPIEndpoint(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.teamService = &teamtest.FakeService{ExpectedIsMember: true}
hs.teamPermissionsService = &actest.FakePermissionsService{}
})
t.Run("should be able to delete team member with correct permission", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
server.NewRequest(http.MethodDelete, "/api/teams/1/members/1", nil),
userWithPermissions(1, []ac.Permission{{Action: ac.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
)
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("should not be able to delete member without correct permission", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
server.NewRequest(http.MethodDelete, "/api/teams/1/members/1", nil),
userWithPermissions(1, []ac.Permission{{Action: ac.ActionTeamsPermissionsWrite, Scope: "teams:id:2"}}),
)
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
-272
View File
@@ -1,272 +0,0 @@
package api
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/preference/preftest"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/team/teamtest"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
)
const (
searchTeamsURL = "/api/teams/search"
createTeamURL = "/api/teams/"
detailTeamURL = "/api/teams/%d"
detailTeamPreferenceURL = "/api/teams/%d/preferences"
teamCmd = `{"name": "MyTestTeam%d"}`
teamPreferenceCmd = `{"theme": "dark"}`
)
func TestTeamAPIEndpoint_CreateTeam(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.teamService = teamtest.NewFakeService()
hs.AccessControl = acimpl.ProvideAccessControl(setting.NewCfg())
hs.accesscontrolService = actest.FakeService{}
})
input := strings.NewReader(fmt.Sprintf(teamCmd, 1))
t.Run("Access control allows creating teams with the correct permissions", func(t *testing.T) {
req := server.NewPostRequest(createTeamURL, input)
req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsCreate}}))
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
input = strings.NewReader(fmt.Sprintf(teamCmd, 2))
t.Run("Access control prevents creating teams with the incorrect permissions", func(t *testing.T) {
req := server.NewPostRequest(createTeamURL, input)
req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, []accesscontrol.Permission{}))
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
func TestTeamAPIEndpoint_SearchTeams(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.teamService = teamtest.NewFakeService()
})
t.Run("Access control prevents searching for teams with the incorrect permissions", func(t *testing.T) {
req := server.NewGetRequest(searchTeamsURL)
req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, []accesscontrol.Permission{}))
res, err := server.Send(req)
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("Access control allows searching for teams with the correct permissions", func(t *testing.T) {
req := server.NewGetRequest(searchTeamsURL)
req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll},
}))
res, err := server.Send(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
func TestTeamAPIEndpoint_GetTeamByID(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.teamService = &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{}}
})
url := fmt.Sprintf(detailTeamURL, 1)
t.Run("Access control prevents getting a team when missing permissions", func(t *testing.T) {
req := server.NewGetRequest(url)
req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, []accesscontrol.Permission{}))
res, err := server.Send(req)
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("Access control allows getting a team with the correct permissions", func(t *testing.T) {
req := server.NewGetRequest(url)
req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
}))
res, err := server.Send(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("Access control allows getting a team with wildcard scope", func(t *testing.T) {
req := server.NewGetRequest(url)
req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:*"},
}))
res, err := server.Send(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
// Given a team with a user, when the user is granted X permission,
// Then the endpoint should return 200 if the user has accesscontrol.ActionTeamsWrite with teams:id:1 scope
// else return 403
func TestTeamAPIEndpoint_UpdateTeam(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.teamService = &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{}}
})
request := func(teamID int64, user *user.SignedInUser) (*http.Response, error) {
req := server.NewRequest(http.MethodPut, fmt.Sprintf(detailTeamURL, teamID), strings.NewReader(teamCmd))
req = webtest.RequestWithSignedInUser(req, user)
return server.SendJSON(req)
}
t.Run("Access control allows updating team with the correct permissions", func(t *testing.T) {
res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsWrite, Scope: "teams:id:1"},
}))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("Access control allows updating teams with the wildcard scope", func(t *testing.T) {
res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsWrite, Scope: "teams:*"},
}))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("Access control prevent updating a team with wrong scope", func(t *testing.T) {
res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsWrite, Scope: "teams:id:2"},
}))
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
// Given a team with a user, when the user is granted X permission,
// Then the endpoint should return 200 if the user has accesscontrol.ActionTeamsDelete with teams:id:1 scope
// else return 403
func TestTeamAPIEndpoint_DeleteTeam(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.teamService = &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{}}
})
request := func(teamID int64, user *user.SignedInUser) (*http.Response, error) {
req := server.NewRequest(http.MethodDelete, fmt.Sprintf(detailTeamURL, teamID), http.NoBody)
req = webtest.RequestWithSignedInUser(req, user)
return server.Send(req)
}
t.Run("Access control prevents deleting teams with the incorrect permissions", func(t *testing.T) {
res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsDelete, Scope: "teams:id:2"},
}))
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("Access control allows deleting teams with the correct permissions", func(t *testing.T) {
res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsDelete, Scope: "teams:id:1"},
}))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
// Given a team with a user, when the user is granted X permission,
// Then the endpoint should return 200 if the user has accesscontrol.ActionTeamsRead with teams:id:1 scope
// else return 403
func TestTeamAPIEndpoint_GetTeamPreferences(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.preferenceService = &preftest.FakePreferenceService{ExpectedPreference: &pref.Preference{}}
})
request := func(teamID int64, user *user.SignedInUser) (*http.Response, error) {
req := server.NewGetRequest(fmt.Sprintf(detailTeamPreferenceURL, teamID))
req = webtest.RequestWithSignedInUser(req, user)
return server.Send(req)
}
t.Run("Access control allows getting team preferences with the correct permissions", func(t *testing.T) {
res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
}))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("Access control prevents getting team preferences with the incorrect permissions", func(t *testing.T) {
res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:2"},
}))
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
// Given a team with a user, when the user is granted X permission,
// Then the endpoint should return 200 if the user has accesscontrol.ActionTeamsWrite with teams:id:1 scope
// else return 403
func TestTeamAPIEndpoint_UpdateTeamPreferences(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.preferenceService = &preftest.FakePreferenceService{ExpectedPreference: &pref.Preference{}}
})
request := func(teamID int64, user *user.SignedInUser) (*http.Response, error) {
req := server.NewRequest(http.MethodPut, fmt.Sprintf(detailTeamPreferenceURL, teamID), strings.NewReader(teamPreferenceCmd))
req = webtest.RequestWithSignedInUser(req, user)
return server.SendJSON(req)
}
t.Run("Access control allows updating team preferences with the correct permissions", func(t *testing.T) {
res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsWrite, Scope: "teams:id:1"},
}))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("Access control prevents updating team preferences with the incorrect permissions", func(t *testing.T) {
res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsWrite, Scope: "teams:id:2"},
}))
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}