grafana/pkg/api/org_users.go
Gabriel MABILLE 96cbe70b14
User: Support sort query param for user and org user, search endpoints (#75229)
* User: Add sort option to user search

* Switch to an approach that uses the dashboard search options

* Cable user sort on the org endpoint

* Alias user table with u in org store

* Add test and cover orgs/:orgID/users/search endpoint

* Add test to userimpl store

* Simplify the store_test with sortopts.ParseSortQueryParam

* Account for PR feedback

* Positive check

* Update docs

* Update docs

* Switch to ErrOrFallback

Co-authored-by: Karl Persson <kalle.persson@grafana.com>

---------

Co-authored-by: Karl Persson <kalle.persson@grafana.com>
2023-09-28 10:16:18 +02:00

634 lines
20 KiB
Go

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/featuremgmt"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/searchusers/sortopts"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
// swagger:route POST /org/users org addOrgUserToCurrentOrg
//
// Add a new user to the current organization.
//
// Adds a global user to the current organization.
//
// If you are running Grafana Enterprise and have Fine-grained access control enabled
// you need to have a permission with action: `org.users:add` with scope `users:*`.
//
// Responses:
// 200: okResponse
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) AddOrgUserToCurrentOrg(c *contextmodel.ReqContext) response.Response {
cmd := org.AddOrgUserCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
cmd.OrgID = c.SignedInUser.GetOrgID()
return hs.addOrgUserHelper(c, cmd)
}
// swagger:route POST /orgs/{org_id}/users orgs addOrgUser
//
// Add a new user to the current organization.
//
// Adds a global user to the current organization.
//
// If you are running Grafana Enterprise and have Fine-grained access control enabled
// you need to have a permission with action: `org.users:add` with scope `users:*`.
//
// Responses:
// 200: okResponse
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) AddOrgUser(c *contextmodel.ReqContext) response.Response {
cmd := org.AddOrgUserCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
var err error
cmd.OrgID, err = strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "orgId is invalid", err)
}
return hs.addOrgUserHelper(c, cmd)
}
func (hs *HTTPServer) addOrgUserHelper(c *contextmodel.ReqContext, cmd org.AddOrgUserCommand) response.Response {
if !cmd.Role.IsValid() {
return response.Error(http.StatusBadRequest, "Invalid role specified", nil)
}
if !c.SignedInUser.GetOrgRole().Includes(cmd.Role) && !c.SignedInUser.GetIsGrafanaAdmin() {
return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil)
}
userQuery := user.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail}
userToAdd, err := hs.userService.GetByLogin(c.Req.Context(), &userQuery)
if err != nil {
return response.Error(http.StatusNotFound, "User not found", nil)
}
cmd.UserID = userToAdd.ID
if err := hs.orgService.AddOrgUser(c.Req.Context(), &cmd); err != nil {
if errors.Is(err, org.ErrOrgUserAlreadyAdded) {
return response.JSON(http.StatusConflict, util.DynMap{
"message": "User is already member of this organization",
"userId": cmd.UserID,
})
}
return response.Error(http.StatusInternalServerError, "Could not add user to organization", err)
}
return response.JSON(http.StatusOK, util.DynMap{
"message": "User added to organization",
"userId": cmd.UserID,
})
}
// swagger:route GET /org/users org getOrgUsersForCurrentOrg
//
// Get all users within the current organization.
//
// Returns all org users within the current organization. Accessible to users with org admin role.
// If you are running Grafana Enterprise and have Fine-grained access control enabled
// you need to have a permission with action: `org.users:read` with scope `users:*`.
//
// Responses:
// 200: getOrgUsersForCurrentOrgResponse
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *contextmodel.ReqContext) response.Response {
result, err := hs.searchOrgUsersHelper(c, &org.SearchOrgUsersQuery{
OrgID: c.SignedInUser.GetOrgID(),
Query: c.Query("query"),
Limit: c.QueryInt("limit"),
User: c.SignedInUser,
})
if err != nil {
return response.Error(500, "Failed to get users for current organization", err)
}
return response.JSON(http.StatusOK, result.OrgUsers)
}
// swagger:route GET /org/users/lookup org getOrgUsersForCurrentOrgLookup
//
// Get all users within the current organization (lookup)
//
// Returns all org users within the current organization, but with less detailed information.
// Accessible to users with org admin role, admin in any folder or admin of any team.
// Mainly used by Grafana UI for providing list of users when adding team members and when editing folder/dashboard permissions.
//
// Responses:
// 200: getOrgUsersForCurrentOrgLookupResponse
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *contextmodel.ReqContext) response.Response {
orgUsersResult, err := hs.searchOrgUsersHelper(c, &org.SearchOrgUsersQuery{
OrgID: c.SignedInUser.GetOrgID(),
Query: c.Query("query"),
Limit: c.QueryInt("limit"),
User: c.SignedInUser,
DontEnforceAccessControl: !hs.License.FeatureEnabled("accesscontrol.enforcement"),
})
if err != nil {
return response.Error(500, "Failed to get users for current organization", err)
}
result := make([]*dtos.UserLookupDTO, 0)
for _, u := range orgUsersResult.OrgUsers {
result = append(result, &dtos.UserLookupDTO{
UserID: u.UserID,
Login: u.Login,
AvatarURL: u.AvatarURL,
})
}
return response.JSON(http.StatusOK, result)
}
// swagger:route GET /orgs/{org_id}/users orgs getOrgUsers
//
// Get Users in Organization.
//
// If you are running Grafana Enterprise and have Fine-grained access control enabled
// you need to have a permission with action: `org.users:read` with scope `users:*`.
//
// Security:
// - basic:
//
// Responses:
// 200: getOrgUsersResponse
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) GetOrgUsers(c *contextmodel.ReqContext) response.Response {
orgId, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "orgId is invalid", err)
}
result, err := hs.searchOrgUsersHelper(c, &org.SearchOrgUsersQuery{
OrgID: orgId,
Query: "",
Limit: 0,
User: c.SignedInUser,
})
if err != nil {
return response.Error(500, "Failed to get users for organization", err)
}
return response.JSON(http.StatusOK, result.OrgUsers)
}
// swagger:route GET /orgs/{org_id}/users/search orgs searchOrgUsers
//
// Search Users in Organization.
//
// If you are running Grafana Enterprise and have Fine-grained access control enabled
// you need to have a permission with action: `org.users:read` with scope `users:*`.
//
// Security:
// - basic:
//
// Responses:
// 200: searchOrgUsersResponse
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) SearchOrgUsers(c *contextmodel.ReqContext) response.Response {
orgID, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "orgId is invalid", err)
}
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)
}
result, err := hs.searchOrgUsersHelper(c, &org.SearchOrgUsersQuery{
OrgID: orgID,
Query: c.Query("query"),
Page: page,
Limit: perPage,
User: c.SignedInUser,
SortOpts: sortOpts,
})
if err != nil {
return response.Error(500, "Failed to get users for organization", err)
}
return response.JSON(http.StatusOK, result)
}
// SearchOrgUsersWithPaging is an HTTP handler to search for org users with paging.
// GET /api/org/users/search
func (hs *HTTPServer) SearchOrgUsersWithPaging(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 := &org.SearchOrgUsersQuery{
OrgID: c.SignedInUser.GetOrgID(),
Query: c.Query("query"),
Page: page,
Limit: perPage,
User: c.SignedInUser,
SortOpts: sortOpts,
}
result, err := hs.searchOrgUsersHelper(c, query)
if err != nil {
return response.Error(500, "Failed to get users for current organization", err)
}
return response.JSON(http.StatusOK, result)
}
func (hs *HTTPServer) searchOrgUsersHelper(c *contextmodel.ReqContext, query *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error) {
result, err := hs.orgService.SearchOrgUsers(c.Req.Context(), query)
if err != nil {
return nil, err
}
filteredUsers := make([]*org.OrgUserDTO, 0, len(result.OrgUsers))
userIDs := map[string]bool{}
authLabelsUserIDs := make([]int64, 0, len(result.OrgUsers))
for _, user := range result.OrgUsers {
if dtos.IsHiddenUser(user.Login, c.SignedInUser, hs.Cfg) {
continue
}
user.AvatarURL = dtos.GetGravatarUrl(user.Email)
userIDs[fmt.Sprint(user.UserID)] = true
authLabelsUserIDs = append(authLabelsUserIDs, user.UserID)
filteredUsers = append(filteredUsers, user)
}
modules, err := hs.authInfoService.GetUserLabels(c.Req.Context(), login.GetUserLabelsQuery{
UserIDs: authLabelsUserIDs,
})
if err != nil {
hs.log.Warn("failed to retrieve users IDP label", err)
}
// Get accesscontrol metadata and IPD labels for users in the target org
accessControlMetadata := map[string]accesscontrol.Metadata{}
if c.QueryBool("accesscontrol") && c.SignedInUser.Permissions != nil {
// TODO https://github.com/grafana/grafana-authnz-team/issues/268 - user access control service for fetching permissions from another organization
permissions, ok := c.SignedInUser.Permissions[query.OrgID]
if ok {
accessControlMetadata = accesscontrol.GetResourcesMetadata(c.Req.Context(), permissions, "users:id:", userIDs)
}
}
for i := range filteredUsers {
filteredUsers[i].AccessControl = accessControlMetadata[fmt.Sprint(filteredUsers[i].UserID)]
if module, ok := modules[filteredUsers[i].UserID]; ok {
filteredUsers[i].AuthLabels = []string{login.GetAuthProviderLabel(module)}
filteredUsers[i].IsExternallySynced = login.IsExternallySynced(hs.Cfg, module)
}
}
result.OrgUsers = filteredUsers
result.Page = query.Page
result.PerPage = query.Limit
return result, nil
}
// swagger:route PATCH /org/users/{user_id} org updateOrgUserForCurrentOrg
//
// Updates the given user.
//
// If you are running Grafana Enterprise and have Fine-grained access control enabled
// you need to have a permission with action: `org.users.role:update` with scope `users:*`.
//
// Responses:
// 200: okResponse
// 400: badRequestError
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) UpdateOrgUserForCurrentOrg(c *contextmodel.ReqContext) response.Response {
cmd := org.UpdateOrgUserCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
cmd.OrgID = c.SignedInUser.GetOrgID()
var err error
cmd.UserID, err = strconv.ParseInt(web.Params(c.Req)[":userId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "userId is invalid", err)
}
return hs.updateOrgUserHelper(c, cmd)
}
// swagger:route PATCH /orgs/{org_id}/users/{user_id} orgs updateOrgUser
//
// Update Users in Organization.
//
// If you are running Grafana Enterprise and have Fine-grained access control enabled
// you need to have a permission with action: `org.users.role:update` with scope `users:*`.
//
// Responses:
// 200: okResponse
// 400: badRequestError
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) UpdateOrgUser(c *contextmodel.ReqContext) response.Response {
cmd := org.UpdateOrgUserCommand{}
var err error
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
cmd.OrgID, err = strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "orgId is invalid", err)
}
cmd.UserID, err = strconv.ParseInt(web.Params(c.Req)[":userId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "userId is invalid", err)
}
return hs.updateOrgUserHelper(c, cmd)
}
func (hs *HTTPServer) updateOrgUserHelper(c *contextmodel.ReqContext, cmd org.UpdateOrgUserCommand) response.Response {
if !cmd.Role.IsValid() {
return response.Error(http.StatusBadRequest, "Invalid role specified", nil)
}
if !c.SignedInUser.GetOrgRole().Includes(cmd.Role) && !c.SignedInUser.GetIsGrafanaAdmin() {
return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil)
}
// we do not allow to change role for external synced users
qAuth := login.GetAuthInfoQuery{UserId: cmd.UserID}
authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &qAuth)
if err != nil {
if errors.Is(err, user.ErrUserNotFound) {
hs.log.Debug("Failed to get user auth info for basic auth user", cmd.UserID, nil)
} else {
hs.log.Error("Failed to get user auth info for external sync check", cmd.UserID, err)
return response.Error(http.StatusInternalServerError, "Failed to get user auth info", nil)
}
}
if authInfo != nil && authInfo.AuthModule != "" && login.IsExternallySynced(hs.Cfg, authInfo.AuthModule) {
// A GCom specific feature toggle for role locking has been introduced, as the previous implementation had a bug with locking down external users synced through GCom (https://github.com/grafana/grafana/pull/72044)
// Remove this conditional once FlagGcomOnlyExternalOrgRoleSync feature toggle has been removed
if authInfo.AuthModule != login.GrafanaComAuthModule || hs.Features.IsEnabled(featuremgmt.FlagGcomOnlyExternalOrgRoleSync) {
return response.Err(org.ErrCannotChangeRoleForExternallySyncedUser.Errorf("Cannot change role for externally synced user"))
}
}
if err := hs.orgService.UpdateOrgUser(c.Req.Context(), &cmd); err != nil {
if errors.Is(err, org.ErrLastOrgAdmin) {
return response.Error(http.StatusBadRequest, "Cannot change role so that there is no organization admin left", nil)
}
return response.Error(http.StatusInternalServerError, "Failed update org user", err)
}
hs.accesscontrolService.ClearUserPermissionCache(&user.SignedInUser{
UserID: cmd.UserID,
OrgID: cmd.OrgID,
})
return response.Success("Organization user updated")
}
// swagger:route DELETE /org/users/{user_id} org removeOrgUserForCurrentOrg
//
// Delete user in current organization.
//
// If you are running Grafana Enterprise and have Fine-grained access control enabled
// you need to have a permission with action: `org.users:remove` with scope `users:*`.
//
// Responses:
// 200: okResponse
// 400: badRequestError
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) RemoveOrgUserForCurrentOrg(c *contextmodel.ReqContext) response.Response {
userId, err := strconv.ParseInt(web.Params(c.Req)[":userId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "userId is invalid", err)
}
return hs.removeOrgUserHelper(c.Req.Context(), &org.RemoveOrgUserCommand{
UserID: userId,
OrgID: c.SignedInUser.GetOrgID(),
ShouldDeleteOrphanedUser: true,
})
}
// swagger:route DELETE /orgs/{org_id}/users/{user_id} orgs removeOrgUser
//
// Delete user in current organization.
//
// If you are running Grafana Enterprise and have Fine-grained access control enabled
// you need to have a permission with action: `org.users:remove` with scope `users:*`.
//
// Responses:
// 200: okResponse
// 400: badRequestError
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) RemoveOrgUser(c *contextmodel.ReqContext) response.Response {
userId, err := strconv.ParseInt(web.Params(c.Req)[":userId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "userId is invalid", err)
}
orgId, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "orgId is invalid", err)
}
return hs.removeOrgUserHelper(c.Req.Context(), &org.RemoveOrgUserCommand{
UserID: userId,
OrgID: orgId,
})
}
func (hs *HTTPServer) removeOrgUserHelper(ctx context.Context, cmd *org.RemoveOrgUserCommand) response.Response {
if err := hs.orgService.RemoveOrgUser(ctx, cmd); err != nil {
if errors.Is(err, org.ErrLastOrgAdmin) {
return response.Error(400, "Cannot remove last organization admin", nil)
}
return response.Error(500, "Failed to remove user from organization", err)
}
if cmd.UserWasDeleted {
// This should be called from appropriate service when moved
if err := hs.accesscontrolService.DeleteUserPermissions(ctx, accesscontrol.GlobalOrgID, cmd.UserID); err != nil {
hs.log.Warn("failed to delete permissions for user", "userID", cmd.UserID, "orgID", accesscontrol.GlobalOrgID, "err", err)
}
return response.Success("User deleted")
}
// This should be called from appropriate service when moved
if err := hs.accesscontrolService.DeleteUserPermissions(ctx, cmd.OrgID, cmd.UserID); err != nil {
hs.log.Warn("failed to delete permissions for user", "userID", cmd.UserID, "orgID", cmd.OrgID, "err", err)
}
return response.Success("User removed from organization")
}
// swagger:parameters addOrgUserToCurrentOrg
type AddOrgUserToCurrentOrgParams struct {
// in:body
// required:true
Body org.AddOrgUserCommand `json:"body"`
}
// swagger:parameters addOrgUser
type AddOrgUserParams struct {
// in:body
// required:true
Body org.AddOrgUserCommand `json:"body"`
// in:path
// required:true
OrgID int64 `json:"org_id"`
}
// swagger:parameters getOrgUsersForCurrentOrgLookup
type LookupOrgUsersParams struct {
// in:query
// required:false
Query string `json:"query"`
// in:query
// required:false
Limit int `json:"limit"`
}
// swagger:parameters getOrgUsers
type GetOrgUsersParams struct {
// in:path
// required:true
OrgID int64 `json:"org_id"`
}
// swagger:parameters updateOrgUserForCurrentOrg
type UpdateOrgUserForCurrentOrgParams struct {
// in:body
// required:true
Body org.UpdateOrgUserCommand `json:"body"`
// in:path
// required:true
UserID int64 `json:"user_id"`
}
// swagger:parameters updateOrgUser
type UpdateOrgUserParams struct {
// in:body
// required:true
Body org.UpdateOrgUserCommand `json:"body"`
// in:path
// required:true
OrgID int64 `json:"org_id"`
// in:path
// required:true
UserID int64 `json:"user_id"`
}
// swagger:parameters removeOrgUserForCurrentOrg
type RemoveOrgUserForCurrentOrgParams struct {
// in:path
// required:true
UserID int64 `json:"user_id"`
}
// swagger:parameters removeOrgUser
type RemoveOrgUserParams struct {
// in:path
// required:true
OrgID int64 `json:"org_id"`
// in:path
// required:true
UserID int64 `json:"user_id"`
}
// swagger:parameters searchOrgUsers
type SearchOrgUsersParams struct {
// in:path
// required:true
OrgID int64 `json:"org_id"`
}
// swagger:response getOrgUsersForCurrentOrgLookupResponse
type GetOrgUsersForCurrentOrgLookupResponse struct {
// The response message
// in: body
Body []*dtos.UserLookupDTO `json:"body"`
}
// swagger:response getOrgUsersForCurrentOrgResponse
type GetOrgUsersForCurrentOrgResponse struct {
// The response message
// in: body
Body []*org.OrgUserDTO `json:"body"`
}
// swagger:response getOrgUsersResponse
type GetOrgUsersResponse struct {
// The response message/
// in: body
Body []*org.OrgUserDTO `json:"body"`
}
// swagger:response searchOrgUsersResponse
type SearchOrgUsersResponse struct {
// The response message
// in: body
Body *org.SearchOrgUsersQueryResult `json:"body"`
}