diff --git a/pkg/api/api.go b/pkg/api/api.go index cad49e0c4c6..215ff274848 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -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)) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index d7227f4127d..9f40753df17 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -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)}} } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 63e95b1afd7..4f3a9c04ef5 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -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, diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index e6c71d8b775..7cbbcfd1912 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -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 diff --git a/pkg/registry/backgroundsvcs/background_services.go b/pkg/registry/backgroundsvcs/background_services.go index 19f8cfc0d26..8e316697aa4 100644 --- a/pkg/registry/backgroundsvcs/background_services.go +++ b/pkg/registry/backgroundsvcs/background_services.go @@ -40,6 +40,7 @@ import ( "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/store/sanitizer" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlesimpl" + "github.com/grafana/grafana/pkg/services/team/teamapi" "github.com/grafana/grafana/pkg/services/updatechecker" ) @@ -62,7 +63,7 @@ func ProvideBackgroundServiceRegistry( _ serviceaccounts.Service, _ *guardian.Provider, _ *plugindashboardsservice.DashboardUpdater, _ *sanitizer.Provider, _ *grpcserver.HealthService, _ entity.EntityStoreServer, _ *grpcserver.ReflectionService, _ *ldapapi.Service, - _ *apiregistry.Service, _ auth.IDService, + _ *apiregistry.Service, _ auth.IDService, _ *teamapi.TeamAPI, ) *BackgroundServiceRegistry { return NewBackgroundServiceRegistry( httpServer, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 44755d416b4..a6f4567bc65 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -141,6 +141,7 @@ import ( "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlesimpl" "github.com/grafana/grafana/pkg/services/tag" "github.com/grafana/grafana/pkg/services/tag/tagimpl" + "github.com/grafana/grafana/pkg/services/team/teamapi" "github.com/grafana/grafana/pkg/services/team/teamimpl" tempuser "github.com/grafana/grafana/pkg/services/temp_user" "github.com/grafana/grafana/pkg/services/temp_user/tempuserimpl" @@ -342,6 +343,7 @@ var wireBasicSet = wire.NewSet( resolver.ProvideEntityReferenceResolver, httpentitystore.ProvideHTTPEntityStore, teamimpl.ProvideService, + teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), diff --git a/pkg/services/preference/prefapi/api.go b/pkg/services/preference/prefapi/api.go new file mode 100644 index 00000000000..a77639571eb --- /dev/null +++ b/pkg/services/preference/prefapi/api.go @@ -0,0 +1,107 @@ +// shared logic between httpserver and teamapi +package prefapi + +import ( + "context" + "net/http" + + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/kinds/preferences" + "github.com/grafana/grafana/pkg/services/dashboards" + pref "github.com/grafana/grafana/pkg/services/preference" +) + +func UpdatePreferencesFor(ctx context.Context, + dashboardService dashboards.DashboardService, preferenceService pref.Service, + 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 := 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 := preferenceService.Save(ctx, &saveCmd); err != nil { + return response.ErrOrFallback(http.StatusInternalServerError, "Failed to save preferences", err) + } + + return response.Success("Preferences updated") +} + +func GetPreferencesFor(ctx context.Context, + dashboardService dashboards.DashboardService, preferenceService pref.Service, + orgID, userID, teamID int64) response.Response { + prefsQuery := pref.GetPreferenceQuery{UserID: userID, OrgID: orgID, TeamID: teamID} + + preference, err := 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 := 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) +} diff --git a/pkg/services/team/teamapi/api.go b/pkg/services/team/teamapi/api.go new file mode 100644 index 00000000000..b744dfdaf5d --- /dev/null +++ b/pkg/services/team/teamapi/api.go @@ -0,0 +1,82 @@ +package teamapi + +import ( + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/middleware/requestmeta" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/licensing" + pref "github.com/grafana/grafana/pkg/services/preference" + "github.com/grafana/grafana/pkg/services/team" + "github.com/grafana/grafana/pkg/setting" +) + +type TeamAPI struct { + teamService team.Service + ac accesscontrol.Service + teamPermissionsService accesscontrol.TeamPermissionsService + license licensing.Licensing + cfg *setting.Cfg + preferenceService pref.Service + ds dashboards.DashboardService +} + +func ProvideTeamAPI( + routeRegister routing.RouteRegister, + teamService team.Service, + ac accesscontrol.Service, + acEvaluator accesscontrol.AccessControl, + teamPermissionsService accesscontrol.TeamPermissionsService, + license licensing.Licensing, + cfg *setting.Cfg, + preferenceService pref.Service, + ds dashboards.DashboardService, +) *TeamAPI { + tapi := &TeamAPI{ + teamService: teamService, + ac: ac, + teamPermissionsService: teamPermissionsService, + license: license, + cfg: cfg, + preferenceService: preferenceService, + ds: ds, + } + + tapi.registerRoutes(routeRegister, acEvaluator) + return tapi +} + +func (tapi *TeamAPI) registerRoutes(router routing.RouteRegister, ac accesscontrol.AccessControl) { + authorize := accesscontrol.Middleware(ac) + router.Group("/api", func(apiRoute routing.RouteRegister) { + // team (admin permission required) + apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) { + teamsRoute.Post("/", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsCreate)), + routing.Wrap(tapi.createTeam)) + teamsRoute.Put("/:teamId", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite, + accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.updateTeam)) + teamsRoute.Delete("/:teamId", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsDelete, + accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.deleteTeamByID)) + teamsRoute.Get("/:teamId/members", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsRead, + accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.getTeamMembers)) + teamsRoute.Post("/:teamId/members", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite, + accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.addTeamMember)) + teamsRoute.Put("/:teamId/members/:userId", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite, + accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.updateTeamMember)) + teamsRoute.Delete("/:teamId/members/:userId", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite, + accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.removeTeamMember)) + teamsRoute.Get("/:teamId/preferences", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead, + accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.getTeamPreferences)) + teamsRoute.Put("/:teamId/preferences", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite, + accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.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(accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead, + accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.getTeamByID)) + teamsRoute.Get("/search", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead)), + routing.Wrap(tapi.searchTeams)) + }, requestmeta.SetOwner(requestmeta.TeamAuth)) + }) +} diff --git a/pkg/api/team.go b/pkg/services/team/teamapi/team.go similarity index 75% rename from pkg/api/team.go rename to pkg/services/team/teamapi/team.go index 42f61252815..ebcde9222d9 100644 --- a/pkg/api/team.go +++ b/pkg/services/team/teamapi/team.go @@ -1,4 +1,4 @@ -package api +package teamapi import ( "errors" @@ -7,9 +7,11 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/services/accesscontrol" "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/preference/prefapi" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/team/sortopts" "github.com/grafana/grafana/pkg/util" @@ -26,13 +28,13 @@ import ( // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) CreateTeam(c *contextmodel.ReqContext) response.Response { +func (tapi *TeamAPI) 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()) + t, err := tapi.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) @@ -42,7 +44,7 @@ func (hs *HTTPServer) CreateTeam(c *contextmodel.ReqContext) response.Response { // 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) + tapi.ac.ClearUserPermissionCache(c.SignedInUser) // if the request is authenticated using API tokens // the SignedInUser is an empty struct therefore @@ -55,7 +57,7 @@ func (hs *HTTPServer) CreateTeam(c *contextmodel.ReqContext) response.Response { 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(), + if err := addOrUpdateTeamMember(c.Req.Context(), tapi.teamPermissionsService, userID, c.SignedInUser.GetOrgID(), t.ID, dashboards.PERMISSION_ADMIN.String()); err != nil { c.Logger.Error("Could not add creator to team", "error", err) } @@ -80,7 +82,7 @@ func (hs *HTTPServer) CreateTeam(c *contextmodel.ReqContext) response.Response { // 404: notFoundError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) UpdateTeam(c *contextmodel.ReqContext) response.Response { +func (tapi *TeamAPI) updateTeam(c *contextmodel.ReqContext) response.Response { cmd := team.UpdateTeamCommand{} var err error if err := web.Bind(c.Req, &cmd); err != nil { @@ -92,7 +94,7 @@ func (hs *HTTPServer) UpdateTeam(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "teamId is invalid", err) } - if err := hs.teamService.UpdateTeam(c.Req.Context(), &cmd); err != nil { + if err := tapi.teamService.UpdateTeam(c.Req.Context(), &cmd); err != nil { if errors.Is(err, team.ErrTeamNameTaken) { return response.Error(http.StatusBadRequest, "Team name taken", err) } @@ -112,14 +114,14 @@ func (hs *HTTPServer) UpdateTeam(c *contextmodel.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteTeamByID(c *contextmodel.ReqContext) response.Response { +func (tapi *TeamAPI) 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 err := tapi.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) } @@ -137,7 +139,7 @@ func (hs *HTTPServer) DeleteTeamByID(c *contextmodel.ReqContext) response.Respon // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) SearchTeams(c *contextmodel.ReqContext) response.Response { +func (tapi *TeamAPI) searchTeams(c *contextmodel.ReqContext) response.Response { perPage := c.QueryInt("perpage") if perPage <= 0 { perPage = 1000 @@ -159,11 +161,11 @@ func (hs *HTTPServer) SearchTeams(c *contextmodel.ReqContext) response.Response Page: page, Limit: perPage, SignedInUser: c.SignedInUser, - HiddenUsers: hs.Cfg.HiddenUsers, + HiddenUsers: tapi.cfg.HiddenUsers, SortOpts: sortOpts, } - queryResult, err := hs.teamService.SearchTeams(c.Req.Context(), &query) + queryResult, err := tapi.teamService.SearchTeams(c.Req.Context(), &query) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to search Teams", err) } @@ -174,7 +176,7 @@ func (hs *HTTPServer) SearchTeams(c *contextmodel.ReqContext) response.Response teamIDs[strconv.FormatInt(team.ID, 10)] = true } - metadata := hs.getMultiAccessControlMetadata(c, "teams:id:", teamIDs) + metadata := tapi.getMultiAccessControlMetadata(c, "teams:id:", teamIDs) if len(metadata) > 0 { for _, team := range queryResult.Teams { team.AccessControl = metadata[strconv.FormatInt(team.ID, 10)] @@ -197,7 +199,7 @@ func (hs *HTTPServer) SearchTeams(c *contextmodel.ReqContext) response.Response // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetTeamByID(c *contextmodel.ReqContext) response.Response { +func (tapi *TeamAPI) 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) @@ -207,10 +209,10 @@ func (hs *HTTPServer) GetTeamByID(c *contextmodel.ReqContext) response.Response OrgID: c.SignedInUser.GetOrgID(), ID: teamId, SignedInUser: c.SignedInUser, - HiddenUsers: hs.Cfg.HiddenUsers, + HiddenUsers: tapi.cfg.HiddenUsers, } - queryResult, err := hs.teamService.GetTeamByID(c.Req.Context(), &query) + queryResult, err := tapi.teamService.GetTeamByID(c.Req.Context(), &query) if err != nil { if errors.Is(err, team.ErrTeamNotFound) { return response.Error(http.StatusNotFound, "Team not found", err) @@ -220,7 +222,7 @@ func (hs *HTTPServer) GetTeamByID(c *contextmodel.ReqContext) response.Response } // Add accesscontrol metadata - queryResult.AccessControl = hs.getAccessControlMetadata(c, c.SignedInUser.GetOrgID(), "teams:id:", strconv.FormatInt(queryResult.ID, 10)) + queryResult.AccessControl = tapi.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) @@ -234,13 +236,13 @@ func (hs *HTTPServer) GetTeamByID(c *contextmodel.ReqContext) response.Response // 200: getPreferencesResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetTeamPreferences(c *contextmodel.ReqContext) response.Response { +func (tapi *TeamAPI) 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) + return prefapi.GetPreferencesFor(c.Req.Context(), tapi.ds, tapi.preferenceService, c.SignedInUser.GetOrgID(), 0, teamId) } // swagger:route PUT /teams/{team_id}/preferences teams updateTeamPreferences @@ -252,7 +254,7 @@ func (hs *HTTPServer) GetTeamPreferences(c *contextmodel.ReqContext) response.Re // 400: badRequestError // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) UpdateTeamPreferences(c *contextmodel.ReqContext) response.Response { +func (tapi *TeamAPI) 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) @@ -263,7 +265,7 @@ func (hs *HTTPServer) UpdateTeamPreferences(c *contextmodel.ReqContext) response return response.Error(http.StatusBadRequest, "teamId is invalid", err) } - return hs.updatePreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, teamId, &dtoCmd) + return prefapi.UpdatePreferencesFor(c.Req.Context(), tapi.ds, tapi.preferenceService, c.SignedInUser.GetOrgID(), 0, teamId, &dtoCmd) } // swagger:parameters updateTeamPreferences @@ -355,3 +357,26 @@ type CreateTeamResponse struct { Message string `json:"message"` } `json:"body"` } + +// getMultiAccessControlMetadata returns the accesscontrol metadata associated with a given set of resources +// Context must contain permissions in the given org (see LoadPermissionsMiddleware or AuthorizeInOrgMiddleware) +func (tapi *TeamAPI) getMultiAccessControlMetadata(c *contextmodel.ReqContext, + prefix string, resourceIDs map[string]bool) map[string]accesscontrol.Metadata { + if !c.QueryBool("accesscontrol") { + return map[string]accesscontrol.Metadata{} + } + + if len(c.SignedInUser.GetPermissions()) == 0 { + return map[string]accesscontrol.Metadata{} + } + + return accesscontrol.GetResourcesMetadata(c.Req.Context(), c.SignedInUser.GetPermissions(), prefix, resourceIDs) +} + +// Metadata helpers +// getAccessControlMetadata returns the accesscontrol metadata associated with a given resource +func (tapi *TeamAPI) getAccessControlMetadata(c *contextmodel.ReqContext, + orgID int64, prefix string, resourceID string) accesscontrol.Metadata { + ids := map[string]bool{resourceID: true} + return tapi.getMultiAccessControlMetadata(c, prefix, ids)[resourceID] +} diff --git a/pkg/api/team_members.go b/pkg/services/team/teamapi/team_members.go similarity index 74% rename from pkg/api/team_members.go rename to pkg/services/team/teamapi/team_members.go index 000206758c7..a9f996a8c11 100644 --- a/pkg/api/team_members.go +++ b/pkg/services/team/teamapi/team_members.go @@ -1,4 +1,4 @@ -package api +package teamapi import ( "context" @@ -28,7 +28,7 @@ import ( // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetTeamMembers(c *contextmodel.ReqContext) response.Response { +func (tapi *TeamAPI) 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) @@ -36,21 +36,21 @@ func (hs *HTTPServer) GetTeamMembers(c *contextmodel.ReqContext) response.Respon query := team.GetTeamMembersQuery{OrgID: c.SignedInUser.GetOrgID(), TeamID: teamId, SignedInUser: c.SignedInUser} - queryResult, err := hs.teamService.GetTeamMembers(c.Req.Context(), &query) + queryResult, err := tapi.teamService.GetTeamMembers(c.Req.Context(), &query) if err != nil { - return response.Error(500, "Failed to get Team Members", err) + return response.Error(http.StatusInternalServerError, "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) { + if dtos.IsHiddenUser(member.Login, c.SignedInUser, tapi.cfg) { continue } member.AvatarURL = dtos.GetGravatarUrl(member.Email) member.Labels = []string{} - if hs.License.FeatureEnabled("teamgroupsync") && member.External { + if tapi.license.FeatureEnabled("teamgroupsync") && member.External { authProvider := login.GetAuthProviderLabel(member.AuthModule) member.Labels = append(member.Labels, authProvider) } @@ -71,7 +71,7 @@ func (hs *HTTPServer) GetTeamMembers(c *contextmodel.ReqContext) response.Respon // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) AddTeamMember(c *contextmodel.ReqContext) response.Response { +func (tapi *TeamAPI) addTeamMember(c *contextmodel.ReqContext) response.Response { cmd := team.AddTeamMemberCommand{} var err error if err := web.Bind(c.Req, &cmd); err != nil { @@ -83,17 +83,17 @@ func (hs *HTTPServer) AddTeamMember(c *contextmodel.ReqContext) response.Respons return response.Error(http.StatusBadRequest, "teamId is invalid", err) } - isTeamMember, err := hs.teamService.IsTeamMember(c.SignedInUser.GetOrgID(), cmd.TeamID, cmd.UserID) + isTeamMember, err := tapi.teamService.IsTeamMember(c.SignedInUser.GetOrgID(), cmd.TeamID, cmd.UserID) if err != nil { - return response.Error(500, "Failed to add team member.", err) + return response.Error(http.StatusInternalServerError, "Failed to add team member.", err) } if isTeamMember { - return response.Error(400, "User is already added to this team", nil) + return response.Error(http.StatusBadRequest, "User is already added to this team", nil) } - err = addOrUpdateTeamMember(c.Req.Context(), hs.teamPermissionsService, cmd.UserID, cmd.OrgID, cmd.TeamID, getPermissionName(cmd.Permission)) + err = addOrUpdateTeamMember(c.Req.Context(), tapi.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.Error(http.StatusInternalServerError, "Failed to add Member to Team", err) } return response.JSON(http.StatusOK, &util.DynMap{ @@ -111,7 +111,7 @@ func (hs *HTTPServer) AddTeamMember(c *contextmodel.ReqContext) response.Respons // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateTeamMember(c *contextmodel.ReqContext) response.Response { +func (tapi *TeamAPI) 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) @@ -126,17 +126,17 @@ func (hs *HTTPServer) UpdateTeamMember(c *contextmodel.ReqContext) response.Resp } orgId := c.SignedInUser.GetOrgID() - isTeamMember, err := hs.teamService.IsTeamMember(orgId, teamId, userId) + isTeamMember, err := tapi.teamService.IsTeamMember(orgId, teamId, userId) if err != nil { - return response.Error(500, "Failed to update team member.", err) + return response.Error(http.StatusInternalServerError, "Failed to update team member.", err) } if !isTeamMember { - return response.Error(404, "Team member not found.", nil) + return response.Error(http.StatusNotFound, "Team member not found.", nil) } - err = addOrUpdateTeamMember(c.Req.Context(), hs.teamPermissionsService, userId, orgId, teamId, getPermissionName(cmd.Permission)) + err = addOrUpdateTeamMember(c.Req.Context(), tapi.teamPermissionsService, userId, orgId, teamId, getPermissionName(cmd.Permission)) if err != nil { - return response.Error(500, "Failed to update team member.", err) + return response.Error(http.StatusInternalServerError, "Failed to update team member.", err) } return response.Success("Team member updated") } @@ -161,7 +161,7 @@ func getPermissionName(permission dashboards.PermissionType) string { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) RemoveTeamMember(c *contextmodel.ReqContext) response.Response { +func (tapi *TeamAPI) removeTeamMember(c *contextmodel.ReqContext) response.Response { orgId := c.SignedInUser.GetOrgID() teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) if err != nil { @@ -173,16 +173,16 @@ func (hs *HTTPServer) RemoveTeamMember(c *contextmodel.ReqContext) response.Resp } teamIDString := strconv.FormatInt(teamId, 10) - if _, err := hs.teamPermissionsService.SetUserPermission(c.Req.Context(), orgId, accesscontrol.User{ID: userId}, teamIDString, ""); err != nil { + if _, err := tapi.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) + return response.Error(http.StatusNotFound, "Team not found", nil) } if errors.Is(err, team.ErrTeamMemberNotFound) { - return response.Error(404, "Team member not found", nil) + return response.Error(http.StatusNotFound, "Team member not found", nil) } - return response.Error(500, "Failed to remove Member from Team", err) + return response.Error(http.StatusInternalServerError, "Failed to remove Member from Team", err) } return response.Success("Team Member removed") } diff --git a/pkg/api/team_members_test.go b/pkg/services/team/teamapi/team_members_test.go similarity index 59% rename from pkg/api/team_members_test.go rename to pkg/services/team/teamapi/team_members_test.go index 85858bc2902..da9465c538c 100644 --- a/pkg/api/team_members_test.go +++ b/pkg/services/team/teamapi/team_members_test.go @@ -1,4 +1,4 @@ -package api +package teamapi import ( "net/http" @@ -8,24 +8,52 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - ac "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/licensing" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/preference/preftest" "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" ) +func SetupAPITestServer(t *testing.T, opts ...func(a *TeamAPI)) *webtest.Server { + t.Helper() + router := routing.NewRouteRegister() + cfg := setting.NewCfg() + cfg.LDAPAuthEnabled = true + + a := ProvideTeamAPI(router, + teamtest.NewFakeService(), + actest.FakeService{}, + acimpl.ProvideAccessControl(cfg), + &actest.FakePermissionsService{}, + &licensing.OSSLicensingService{}, + cfg, + preftest.NewPreferenceServiceFake(), + dashboards.NewFakeDashboardService(t), + ) + for _, o := range opts { + o(a) + } + + server := webtest.NewServer(t, router) + + return server +} + func TestAddTeamMembersAPIEndpoint(t *testing.T) { - server := SetupAPITestServer(t, func(hs *HTTPServer) { - hs.Cfg = setting.NewCfg() - hs.teamService = teamtest.NewFakeService() - hs.teamPermissionsService = &actest.FakePermissionsService{} - }) + server := SetupAPITestServer(t) 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"}}), + authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}), ) res, err := server.SendJSON(req) require.NoError(t, err) @@ -36,7 +64,7 @@ func TestAddTeamMembersAPIEndpoint(t *testing.T) { 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"}}), + authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:2"}}), ) res, err := server.SendJSON(req) require.NoError(t, err) @@ -46,16 +74,12 @@ func TestAddTeamMembersAPIEndpoint(t *testing.T) { } func TestGetTeamMembersAPIEndpoint(t *testing.T) { - server := SetupAPITestServer(t, func(hs *HTTPServer) { - hs.Cfg = setting.NewCfg() - hs.teamService = teamtest.NewFakeService() - hs.teamPermissionsService = &actest.FakePermissionsService{} - }) + server := SetupAPITestServer(t) 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"}}), + authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"}}), ) res, err := server.SendJSON(req) require.NoError(t, err) @@ -65,7 +89,7 @@ func TestGetTeamMembersAPIEndpoint(t *testing.T) { 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"}}), + authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:2"}}), ) res, err := server.SendJSON(req) require.NoError(t, err) @@ -75,16 +99,14 @@ func TestGetTeamMembersAPIEndpoint(t *testing.T) { } func TestUpdateTeamMembersAPIEndpoint(t *testing.T) { - server := SetupAPITestServer(t, func(hs *HTTPServer) { - hs.Cfg = setting.NewCfg() + server := SetupAPITestServer(t, func(hs *TeamAPI) { 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"}}), + authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}), ) res, err := server.SendJSON(req) require.NoError(t, err) @@ -94,7 +116,7 @@ func TestUpdateTeamMembersAPIEndpoint(t *testing.T) { 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"}}), + authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:2"}}), ) res, err := server.SendJSON(req) require.NoError(t, err) @@ -104,8 +126,7 @@ func TestUpdateTeamMembersAPIEndpoint(t *testing.T) { } func TestDeleteTeamMembersAPIEndpoint(t *testing.T) { - server := SetupAPITestServer(t, func(hs *HTTPServer) { - hs.Cfg = setting.NewCfg() + server := SetupAPITestServer(t, func(hs *TeamAPI) { hs.teamService = &teamtest.FakeService{ExpectedIsMember: true} hs.teamPermissionsService = &actest.FakePermissionsService{} }) @@ -113,7 +134,7 @@ func TestDeleteTeamMembersAPIEndpoint(t *testing.T) { 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"}}), + authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}), ) res, err := server.SendJSON(req) require.NoError(t, err) @@ -123,7 +144,7 @@ func TestDeleteTeamMembersAPIEndpoint(t *testing.T) { 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"}}), + authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:2"}}), ) res, err := server.SendJSON(req) require.NoError(t, err) @@ -131,3 +152,7 @@ func TestDeleteTeamMembersAPIEndpoint(t *testing.T) { require.NoError(t, res.Body.Close()) }) } + +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)}} +} diff --git a/pkg/api/team_test.go b/pkg/services/team/teamapi/team_test.go similarity index 79% rename from pkg/api/team_test.go rename to pkg/services/team/teamapi/team_test.go index 5c6646124a7..4cd3f541046 100644 --- a/pkg/api/team_test.go +++ b/pkg/services/team/teamapi/team_test.go @@ -1,4 +1,4 @@ -package api +package teamapi import ( "fmt" @@ -10,14 +10,11 @@ import ( "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" ) @@ -31,17 +28,14 @@ const ( ) func TestTeamAPIEndpoint_CreateTeam(t *testing.T) { - server := SetupAPITestServer(t, func(hs *HTTPServer) { - hs.Cfg = setting.NewCfg() + server := SetupAPITestServer(t, func(hs *TeamAPI) { 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}})) + req = webtest.RequestWithSignedInUser(req, authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsCreate}})) res, err := server.SendJSON(req) require.NoError(t, err) assert.Equal(t, http.StatusOK, res.StatusCode) @@ -51,7 +45,7 @@ func TestTeamAPIEndpoint_CreateTeam(t *testing.T) { 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{})) + req = webtest.RequestWithSignedInUser(req, authedUserWithPermissions(1, 1, []accesscontrol.Permission{})) res, err := server.SendJSON(req) require.NoError(t, err) assert.Equal(t, http.StatusForbidden, res.StatusCode) @@ -60,14 +54,13 @@ func TestTeamAPIEndpoint_CreateTeam(t *testing.T) { } func TestTeamAPIEndpoint_SearchTeams(t *testing.T) { - server := SetupAPITestServer(t, func(hs *HTTPServer) { - hs.Cfg = setting.NewCfg() + server := SetupAPITestServer(t, func(hs *TeamAPI) { 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{})) + req = webtest.RequestWithSignedInUser(req, authedUserWithPermissions(1, 1, []accesscontrol.Permission{})) res, err := server.Send(req) require.NoError(t, err) assert.Equal(t, http.StatusForbidden, res.StatusCode) @@ -76,7 +69,7 @@ func TestTeamAPIEndpoint_SearchTeams(t *testing.T) { 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{ + req = webtest.RequestWithSignedInUser(req, authedUserWithPermissions(1, 1, []accesscontrol.Permission{ {Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll}, })) res, err := server.Send(req) @@ -87,8 +80,7 @@ func TestTeamAPIEndpoint_SearchTeams(t *testing.T) { } func TestTeamAPIEndpoint_GetTeamByID(t *testing.T) { - server := SetupAPITestServer(t, func(hs *HTTPServer) { - hs.Cfg = setting.NewCfg() + server := SetupAPITestServer(t, func(hs *TeamAPI) { hs.teamService = &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{}} }) @@ -96,7 +88,7 @@ func TestTeamAPIEndpoint_GetTeamByID(t *testing.T) { 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{})) + req = webtest.RequestWithSignedInUser(req, authedUserWithPermissions(1, 1, []accesscontrol.Permission{})) res, err := server.Send(req) require.NoError(t, err) assert.Equal(t, http.StatusForbidden, res.StatusCode) @@ -105,7 +97,7 @@ func TestTeamAPIEndpoint_GetTeamByID(t *testing.T) { 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{ + req = webtest.RequestWithSignedInUser(req, authedUserWithPermissions(1, 1, []accesscontrol.Permission{ {Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}, })) res, err := server.Send(req) @@ -116,7 +108,7 @@ func TestTeamAPIEndpoint_GetTeamByID(t *testing.T) { 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{ + req = webtest.RequestWithSignedInUser(req, authedUserWithPermissions(1, 1, []accesscontrol.Permission{ {Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:*"}, })) res, err := server.Send(req) @@ -130,8 +122,7 @@ func TestTeamAPIEndpoint_GetTeamByID(t *testing.T) { // 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() + server := SetupAPITestServer(t, func(hs *TeamAPI) { hs.teamService = &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{}} }) @@ -142,7 +133,7 @@ func TestTeamAPIEndpoint_UpdateTeam(t *testing.T) { } t.Run("Access control allows updating team with the correct permissions", func(t *testing.T) { - res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{ + res, err := request(1, authedUserWithPermissions(1, 1, []accesscontrol.Permission{ {Action: accesscontrol.ActionTeamsWrite, Scope: "teams:id:1"}, })) require.NoError(t, err) @@ -151,7 +142,7 @@ func TestTeamAPIEndpoint_UpdateTeam(t *testing.T) { }) t.Run("Access control allows updating teams with the wildcard scope", func(t *testing.T) { - res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{ + res, err := request(1, authedUserWithPermissions(1, 1, []accesscontrol.Permission{ {Action: accesscontrol.ActionTeamsWrite, Scope: "teams:*"}, })) require.NoError(t, err) @@ -160,7 +151,7 @@ func TestTeamAPIEndpoint_UpdateTeam(t *testing.T) { }) t.Run("Access control prevent updating a team with wrong scope", func(t *testing.T) { - res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{ + res, err := request(1, authedUserWithPermissions(1, 1, []accesscontrol.Permission{ {Action: accesscontrol.ActionTeamsWrite, Scope: "teams:id:2"}, })) require.NoError(t, err) @@ -173,8 +164,7 @@ func TestTeamAPIEndpoint_UpdateTeam(t *testing.T) { // 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() + server := SetupAPITestServer(t, func(hs *TeamAPI) { hs.teamService = &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{}} }) @@ -185,7 +175,7 @@ func TestTeamAPIEndpoint_DeleteTeam(t *testing.T) { } t.Run("Access control prevents deleting teams with the incorrect permissions", func(t *testing.T) { - res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{ + res, err := request(1, authedUserWithPermissions(1, 1, []accesscontrol.Permission{ {Action: accesscontrol.ActionTeamsDelete, Scope: "teams:id:2"}, })) require.NoError(t, err) @@ -194,7 +184,7 @@ func TestTeamAPIEndpoint_DeleteTeam(t *testing.T) { }) t.Run("Access control allows deleting teams with the correct permissions", func(t *testing.T) { - res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{ + res, err := request(1, authedUserWithPermissions(1, 1, []accesscontrol.Permission{ {Action: accesscontrol.ActionTeamsDelete, Scope: "teams:id:1"}, })) require.NoError(t, err) @@ -207,8 +197,7 @@ func TestTeamAPIEndpoint_DeleteTeam(t *testing.T) { // 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() + server := SetupAPITestServer(t, func(hs *TeamAPI) { hs.preferenceService = &preftest.FakePreferenceService{ExpectedPreference: &pref.Preference{}} }) @@ -219,7 +208,7 @@ func TestTeamAPIEndpoint_GetTeamPreferences(t *testing.T) { } t.Run("Access control allows getting team preferences with the correct permissions", func(t *testing.T) { - res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{ + res, err := request(1, authedUserWithPermissions(1, 1, []accesscontrol.Permission{ {Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}, })) require.NoError(t, err) @@ -228,7 +217,7 @@ func TestTeamAPIEndpoint_GetTeamPreferences(t *testing.T) { }) t.Run("Access control prevents getting team preferences with the incorrect permissions", func(t *testing.T) { - res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{ + res, err := request(1, authedUserWithPermissions(1, 1, []accesscontrol.Permission{ {Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:2"}, })) require.NoError(t, err) @@ -241,8 +230,7 @@ func TestTeamAPIEndpoint_GetTeamPreferences(t *testing.T) { // 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() + server := SetupAPITestServer(t, func(hs *TeamAPI) { hs.preferenceService = &preftest.FakePreferenceService{ExpectedPreference: &pref.Preference{}} }) @@ -253,7 +241,7 @@ func TestTeamAPIEndpoint_UpdateTeamPreferences(t *testing.T) { } t.Run("Access control allows updating team preferences with the correct permissions", func(t *testing.T) { - res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{ + res, err := request(1, authedUserWithPermissions(1, 1, []accesscontrol.Permission{ {Action: accesscontrol.ActionTeamsWrite, Scope: "teams:id:1"}, })) require.NoError(t, err) @@ -262,7 +250,7 @@ func TestTeamAPIEndpoint_UpdateTeamPreferences(t *testing.T) { }) t.Run("Access control prevents updating team preferences with the incorrect permissions", func(t *testing.T) { - res, err := request(1, userWithPermissions(1, []accesscontrol.Permission{ + res, err := request(1, authedUserWithPermissions(1, 1, []accesscontrol.Permission{ {Action: accesscontrol.ActionTeamsWrite, Scope: "teams:id:2"}, })) require.NoError(t, err)