diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index b82becb9684..bcfa9f496f0 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -248,8 +248,9 @@ func (s *fakeRenderService) Init() error { return nil } +// FIXME: This user should not be anonymous func userWithPermissions(orgID int64, permissions []accesscontrol.Permission) *user.SignedInUser { - return &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)}} + return &user.SignedInUser{IsAnonymous: true, OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)}} } func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer { diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 53a94e80cdb..22b97801444 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/metrics" ac "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/notifications" "github.com/grafana/grafana/pkg/services/org" @@ -33,11 +34,11 @@ import ( // 403: forbiddenError // 500: internalServerError func (hs *HTTPServer) GetPendingOrgInvites(c *contextmodel.ReqContext) response.Response { - query := tempuser.GetTempUsersQuery{OrgID: c.OrgID, Status: tempuser.TmpUserInvitePending} + query := tempuser.GetTempUsersQuery{OrgID: c.SignedInUser.GetOrgID(), Status: tempuser.TmpUserInvitePending} queryResult, err := hs.tempUserService.GetTempUsersQuery(c.Req.Context(), &query) if err != nil { - return response.Error(500, "Failed to get invites from db", err) + return response.Error(http.StatusInternalServerError, "Failed to get invites from db", err) } for _, invite := range queryResult { @@ -64,9 +65,9 @@ func (hs *HTTPServer) AddOrgInvite(c *contextmodel.ReqContext) response.Response return response.Error(http.StatusBadRequest, "bad request data", err) } if !inviteDto.Role.IsValid() { - return response.Error(400, "Invalid role specified", nil) + return response.Error(http.StatusBadRequest, "Invalid role specified", nil) } - if !c.OrgRole.Includes(inviteDto.Role) && !c.IsGrafanaAdmin { + if !c.SignedInUser.GetOrgRole().Includes(inviteDto.Role) && !c.SignedInUser.GetIsGrafanaAdmin() { return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil) } @@ -75,7 +76,7 @@ func (hs *HTTPServer) AddOrgInvite(c *contextmodel.ReqContext) response.Response usr, err := hs.userService.GetByLogin(c.Req.Context(), &userQuery) if err != nil { if !errors.Is(err, user.ErrUserNotFound) { - return response.Error(500, "Failed to query db for existing user check", err) + return response.Error(http.StatusInternalServerError, "Failed to query db for existing user check", err) } } else { // Evaluate permissions for adding an existing user to the organization @@ -91,25 +92,37 @@ func (hs *HTTPServer) AddOrgInvite(c *contextmodel.ReqContext) response.Response } if hs.Cfg.DisableLoginForm { - return response.Error(400, "Cannot invite external user when login is disabled.", nil) + return response.Error(http.StatusBadRequest, "Cannot invite external user when login is disabled.", nil) } cmd := tempuser.CreateTempUserCommand{} - cmd.OrgID = c.OrgID + cmd.OrgID = c.SignedInUser.GetOrgID() cmd.Email = inviteDto.LoginOrEmail cmd.Name = inviteDto.Name cmd.Status = tempuser.TmpUserInvitePending - cmd.InvitedByUserID = c.UserID + + namespace, identifier := c.SignedInUser.GetNamespacedID() + var userID int64 + switch namespace { + case identity.NamespaceUser, identity.NamespaceServiceAccount: + var err error + userID, err = strconv.ParseInt(identifier, 10, 64) + if err != nil { + return response.Error(http.StatusInternalServerError, "Unrecognized user", err) + } + } + + cmd.InvitedByUserID = userID cmd.Code, err = util.GetRandomString(30) if err != nil { - return response.Error(500, "Could not generate random string", err) + return response.Error(http.StatusInternalServerError, "Could not generate random string", err) } cmd.Role = inviteDto.Role cmd.RemoteAddr = c.RemoteAddr() cmdResult, err := hs.tempUserService.CreateTempUser(c.Req.Context(), &cmd) if err != nil { - return response.Error(500, "Failed to save invite to database", err) + return response.Error(http.StatusInternalServerError, "Failed to save invite to database", err) } // send invite email @@ -119,24 +132,24 @@ func (hs *HTTPServer) AddOrgInvite(c *contextmodel.ReqContext) response.Response Template: "new_user_invite", Data: map[string]interface{}{ "Name": util.StringsFallback2(cmd.Name, cmd.Email), - "OrgName": c.OrgName, - "Email": c.Email, + "OrgName": c.SignedInUser.GetOrgName(), + "Email": c.SignedInUser.GetEmail(), "LinkUrl": setting.ToAbsUrl("invite/" + cmd.Code), - "InvitedBy": util.StringsFallback3(c.Name, c.Email, c.Login), + "InvitedBy": c.SignedInUser.GetDisplayName(), }, } if err := hs.AlertNG.NotificationService.SendEmailCommandHandler(c.Req.Context(), &emailCmd); err != nil { if errors.Is(err, notifications.ErrSmtpNotEnabled) { - return response.Error(412, err.Error(), err) + return response.Error(http.StatusPreconditionFailed, err.Error(), err) } - return response.Error(500, "Failed to send email invite", err) + return response.Error(http.StatusInternalServerError, "Failed to send email invite", err) } emailSentCmd := tempuser.UpdateTempUserWithEmailSentCommand{Code: cmdResult.Code} if err := hs.tempUserService.UpdateTempUserWithEmailSent(c.Req.Context(), &emailSentCmd); err != nil { - return response.Error(500, "Failed to update invite with email sent info", err) + return response.Error(http.StatusInternalServerError, "Failed to update invite with email sent info", err) } return response.Success(fmt.Sprintf("Sent invite to %s", inviteDto.LoginOrEmail)) @@ -147,12 +160,12 @@ func (hs *HTTPServer) AddOrgInvite(c *contextmodel.ReqContext) response.Response func (hs *HTTPServer) inviteExistingUserToOrg(c *contextmodel.ReqContext, user *user.User, inviteDto *dtos.AddInviteForm) response.Response { // user exists, add org role - createOrgUserCmd := org.AddOrgUserCommand{OrgID: c.OrgID, UserID: user.ID, Role: inviteDto.Role} + createOrgUserCmd := org.AddOrgUserCommand{OrgID: c.SignedInUser.GetOrgID(), UserID: user.ID, Role: inviteDto.Role} if err := hs.orgService.AddOrgUser(c.Req.Context(), &createOrgUserCmd); err != nil { if errors.Is(err, org.ErrOrgUserAlreadyAdded) { - return response.Error(412, fmt.Sprintf("User %s is already added to organization", inviteDto.LoginOrEmail), err) + return response.Error(http.StatusPreconditionFailed, fmt.Sprintf("User %s is already added to organization", inviteDto.LoginOrEmail), err) } - return response.Error(500, "Error while trying to create org user", err) + return response.Error(http.StatusInternalServerError, "Error while trying to create org user", err) } if inviteDto.SendEmail && util.IsEmail(user.Email) { @@ -161,18 +174,18 @@ func (hs *HTTPServer) inviteExistingUserToOrg(c *contextmodel.ReqContext, user * Template: "invited_to_org", Data: map[string]interface{}{ "Name": user.NameOrFallback(), - "OrgName": c.OrgName, - "InvitedBy": util.StringsFallback3(c.Name, c.Email, c.Login), + "OrgName": c.SignedInUser.GetOrgName(), + "InvitedBy": c.SignedInUser.GetDisplayName(), }, } if err := hs.AlertNG.NotificationService.SendEmailCommandHandler(c.Req.Context(), &emailCmd); err != nil { - return response.Error(500, "Failed to send email invited_to_org", err) + return response.Error(http.StatusInternalServerError, "Failed to send email invited_to_org", err) } } return response.JSON(http.StatusOK, util.DynMap{ - "message": fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName), + "message": fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.SignedInUser.GetOrgName()), "userId": user.ID, }) } diff --git a/pkg/api/team.go b/pkg/api/team.go index 18944eca554..fbee7f8fb39 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -7,6 +7,7 @@ import ( "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" @@ -30,12 +31,12 @@ func (hs *HTTPServer) CreateTeam(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "bad request data", err) } - t, err := hs.teamService.CreateTeam(cmd.Name, cmd.Email, c.OrgID) + t, err := hs.teamService.CreateTeam(cmd.Name, cmd.Email, c.SignedInUser.GetOrgID()) if err != nil { if errors.Is(err, team.ErrTeamNameTaken) { - return response.Error(409, "Team name taken", err) + return response.Error(http.StatusConflict, "Team name taken", err) } - return response.Error(500, "Failed to create Team", 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 @@ -45,13 +46,22 @@ func (hs *HTTPServer) CreateTeam(c *contextmodel.ReqContext) response.Response { // 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 - if c.SignedInUser.IsRealUser() { - if err := addOrUpdateTeamMember(c.Req.Context(), hs.teamPermissionsService, c.SignedInUser.UserID, c.OrgID, t.ID, dashboards.PERMISSION_ADMIN.String()); err != nil { + 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) } - } else { + 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", @@ -75,7 +85,7 @@ func (hs *HTTPServer) UpdateTeam(c *contextmodel.ReqContext) response.Response { if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - cmd.OrgID = c.OrgID + 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) @@ -83,9 +93,9 @@ func (hs *HTTPServer) UpdateTeam(c *contextmodel.ReqContext) response.Response { if err := hs.teamService.UpdateTeam(c.Req.Context(), &cmd); err != nil { if errors.Is(err, team.ErrTeamNameTaken) { - return response.Error(400, "Team name taken", err) + return response.Error(http.StatusBadRequest, "Team name taken", err) } - return response.Error(500, "Failed to update Team", err) + return response.Error(http.StatusInternalServerError, "Failed to update Team", err) } return response.Success("Team updated") @@ -102,7 +112,7 @@ func (hs *HTTPServer) UpdateTeam(c *contextmodel.ReqContext) response.Response { // 404: notFoundError // 500: internalServerError func (hs *HTTPServer) DeleteTeamByID(c *contextmodel.ReqContext) response.Response { - orgID := c.OrgID + 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) @@ -110,9 +120,9 @@ func (hs *HTTPServer) DeleteTeamByID(c *contextmodel.ReqContext) response.Respon 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(404, "Failed to delete Team. ID not found", nil) + return response.Error(http.StatusNotFound, "Failed to delete Team. ID not found", nil) } - return response.Error(500, "Failed to delete Team", err) + return response.Error(http.StatusInternalServerError, "Failed to delete Team", err) } return response.Success("Team deleted") } @@ -137,7 +147,7 @@ func (hs *HTTPServer) SearchTeams(c *contextmodel.ReqContext) response.Response } query := team.SearchTeamsQuery{ - OrgID: c.OrgID, + OrgID: c.SignedInUser.GetOrgID(), Query: c.Query("query"), Name: c.Query("name"), Page: page, @@ -148,7 +158,7 @@ func (hs *HTTPServer) SearchTeams(c *contextmodel.ReqContext) response.Response queryResult, err := hs.teamService.SearchTeams(c.Req.Context(), &query) if err != nil { - return response.Error(500, "Failed to search Teams", err) + return response.Error(http.StatusInternalServerError, "Failed to search Teams", err) } teamIDs := map[string]bool{} @@ -157,7 +167,7 @@ func (hs *HTTPServer) SearchTeams(c *contextmodel.ReqContext) response.Response teamIDs[strconv.FormatInt(team.ID, 10)] = true } - metadata := hs.getMultiAccessControlMetadata(c, c.OrgID, "teams:id:", teamIDs) + metadata := hs.getMultiAccessControlMetadata(c, c.SignedInUser.GetOrgID(), "teams:id:", teamIDs) if len(metadata) > 0 { for _, team := range queryResult.Teams { team.AccessControl = metadata[strconv.FormatInt(team.ID, 10)] @@ -187,7 +197,7 @@ func (hs *HTTPServer) GetTeamByID(c *contextmodel.ReqContext) response.Response } query := team.GetTeamByIDQuery{ - OrgID: c.OrgID, + OrgID: c.SignedInUser.GetOrgID(), ID: teamId, SignedInUser: c.SignedInUser, HiddenUsers: hs.Cfg.HiddenUsers, @@ -196,14 +206,14 @@ func (hs *HTTPServer) GetTeamByID(c *contextmodel.ReqContext) response.Response queryResult, err := hs.teamService.GetTeamByID(c.Req.Context(), &query) if err != nil { if errors.Is(err, team.ErrTeamNotFound) { - return response.Error(404, "Team not found", err) + return response.Error(http.StatusNotFound, "Team not found", err) } - return response.Error(500, "Failed to get Team", err) + return response.Error(http.StatusInternalServerError, "Failed to get Team", err) } // Add accesscontrol metadata - queryResult.AccessControl = hs.getAccessControlMetadata(c, c.OrgID, "teams:id:", strconv.FormatInt(queryResult.ID, 10)) + 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) @@ -223,7 +233,7 @@ func (hs *HTTPServer) GetTeamPreferences(c *contextmodel.ReqContext) response.Re return response.Error(http.StatusBadRequest, "teamId is invalid", err) } - return hs.getPreferencesFor(c.Req.Context(), c.OrgID, 0, teamId) + return hs.getPreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, teamId) } // swagger:route PUT /teams/{team_id}/preferences teams updateTeamPreferences @@ -246,7 +256,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.OrgID, 0, teamId, &dtoCmd) + return hs.updatePreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, teamId, &dtoCmd) } // swagger:parameters updateTeamPreferences diff --git a/pkg/services/auth/identity/requester.go b/pkg/services/auth/identity/requester.go index f8031e7ab89..a384e8152d6 100644 --- a/pkg/services/auth/identity/requester.go +++ b/pkg/services/auth/identity/requester.go @@ -11,16 +11,42 @@ const ( ) type Requester interface { + // GetDisplayName returns the display name of the active entity. + // The display name is the name if it is set, otherwise the login or email. + GetDisplayName() string + // GetEmail returns the email of the active entity. + // Can be empty. + GetEmail() string + // GetIsGrafanaAdmin returns true if the user is a server admin GetIsGrafanaAdmin() bool + // GetLogin returns the login of the active entity + // Can be empty. GetLogin() string - GetOrgID() int64 - GetPermissions() map[string][]string - GetTeams() []int64 - GetOrgRole() roletype.RoleType + // GetNamespacedID returns the namespace and ID of the active entity. + // The namespace is one of the constants defined in pkg/services/auth/identity. GetNamespacedID() (string, string) + // GetOrgID returns the ID of the active organization + GetOrgID() int64 + // GetOrgRole returns the role of the active entity in the active organization. + GetOrgRole() roletype.RoleType + // GetPermissions returns the permissions of the active entity. + GetPermissions() map[string][]string + // DEPRECATED: GetTeams returns the teams the entity is a member of. + // Retrieve the teams from the team service instead of using this method. + GetTeams() []int64 + // DEPRECATED: GetOrgName returns the name of the active organization. + // Retrieve the organization name from the organization service instead of using this method. + GetOrgName() string + + // IsNil returns true if the identity is nil + // FIXME: remove this method once all services are using an interface IsNil() bool // Legacy + + // GetCacheKey returns a unique key for the entity. + // Add an extra prefix to avoid collisions with other caches GetCacheKey() (string, error) + // HasUniqueId returns true if the entity has a unique id HasUniqueId() bool } diff --git a/pkg/services/contexthandler/model/model.go b/pkg/services/contexthandler/model/model.go index 07c8a43dc70..d1dd30844fd 100644 --- a/pkg/services/contexthandler/model/model.go +++ b/pkg/services/contexthandler/model/model.go @@ -155,7 +155,7 @@ func (ctx *ReqContext) writeErrOrFallback(status int, message string, err error) } func (ctx *ReqContext) HasUserRole(role org.RoleType) bool { - return ctx.OrgRole.Includes(role) + return ctx.SignedInUser.GetOrgRole().Includes(role) } func (ctx *ReqContext) HasHelpFlag(flag user.HelpFlags1) bool { diff --git a/pkg/services/user/identity.go b/pkg/services/user/identity.go index a92fbe083f8..671a97eb67a 100644 --- a/pkg/services/user/identity.go +++ b/pkg/services/user/identity.go @@ -59,7 +59,7 @@ func (u *SignedInUser) HasRole(role roletype.RoleType) bool { return u.OrgRole.Includes(role) } -// IsRealUser returns true if the user is a real user and not a service account +// IsRealUser returns true if the entity is a real user and not a service account func (u *SignedInUser) IsRealUser() bool { // backwards compatibility // checking if userId the user is a real user @@ -72,15 +72,18 @@ func (u *SignedInUser) IsApiKeyUser() bool { return u.ApiKeyID > 0 } -// IsServiceAccountUser returns true if the user is a service account +// IsServiceAccountUser returns true if the entity is a service account func (u *SignedInUser) IsServiceAccountUser() bool { return u.IsServiceAccount } +// HasUniqueId returns true if the entity has a unique id func (u *SignedInUser) HasUniqueId() bool { return u.IsRealUser() || u.IsApiKeyUser() || u.IsServiceAccountUser() } +// GetCacheKey returns a unique key for the entity. +// Add an extra prefix to avoid collisions with other caches func (u *SignedInUser) GetCacheKey() (string, error) { if u.IsRealUser() { return fmt.Sprintf("%d-user-%d", u.OrgID, u.UserID), nil @@ -94,18 +97,29 @@ func (u *SignedInUser) GetCacheKey() (string, error) { return "", ErrNoUniqueID } +// GetIsGrafanaAdmin returns true if the user is a server admin func (u *SignedInUser) GetIsGrafanaAdmin() bool { return u.IsGrafanaAdmin } +// GetLogin returns the login of the active entity +// Can be empty if the user is anonymous func (u *SignedInUser) GetLogin() string { return u.Login } +// GetOrgID returns the ID of the active organization func (u *SignedInUser) GetOrgID() int64 { return u.OrgID } +// DEPRECATED: GetOrgName returns the name of the active organization +// Retrieve the organization name from the organization service instead of using this method. +func (u *SignedInUser) GetOrgName() string { + return u.OrgName +} + +// GetPermissions returns the permissions of the active entity func (u *SignedInUser) GetPermissions() map[string][]string { if u.Permissions == nil { return make(map[string][]string) @@ -118,21 +132,26 @@ func (u *SignedInUser) GetPermissions() map[string][]string { return u.Permissions[u.GetOrgID()] } +// DEPRECATED: GetTeams returns the teams the entity is a member of +// Retrieve the teams from the team service instead of using this method. func (u *SignedInUser) GetTeams() []int64 { return u.Teams } +// GetOrgRole returns the role of the active entity in the active organization func (u *SignedInUser) GetOrgRole() roletype.RoleType { return u.OrgRole } +// GetNamespacedID returns the namespace and ID of the active entity +// The namespace is one of the constants defined in pkg/services/auth/identity func (u *SignedInUser) GetNamespacedID() (string, string) { switch { case u.ApiKeyID != 0: return identity.NamespaceAPIKey, fmt.Sprintf("%d", u.ApiKeyID) case u.IsServiceAccount: return identity.NamespaceServiceAccount, fmt.Sprintf("%d", u.UserID) - case u.UserID != 0: + case u.UserID > 0: return identity.NamespaceUser, fmt.Sprintf("%d", u.UserID) case u.IsAnonymous: return identity.NamespaceAnonymous, "" @@ -148,3 +167,15 @@ func (u *SignedInUser) GetNamespacedID() (string, string) { func (u *SignedInUser) IsNil() bool { return u == nil } + +// GetEmail returns the email of the active entity +// Can be empty. +func (u *SignedInUser) GetEmail() string { + return u.Email +} + +// GetDisplayName returns the display name of the active entity +// The display name is the name if it is set, otherwise the login or email +func (u *SignedInUser) GetDisplayName() string { + return u.NameOrFallback() +}