Identity: Unfurl UserID and Email in pkg/api to user identity.Requester (#76112)

* Unfurl OrgRole in pkg/api to allow using identity.Requester interface

* Unfurl Email in pkg/api to allow using identity.Requester interface

* Update UserID in pkg/api to allow using identity.Requester interface

* fix authed test

* fix datasource tests

* guard login

* fix preferences anon testing

* fix anonymous index rendering

* do not error with user id 0
This commit is contained in:
Jo 2023-10-09 16:07:28 +02:00 committed by GitHub
parent 8bf914ac0b
commit 8919cafcb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 145 additions and 25 deletions

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations"
"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/guardian"
@ -132,9 +133,14 @@ func (hs *HTTPServer) PostAnnotation(c *contextmodel.ReqContext) response.Respon
return response.Error(400, "Failed to save annotation", err)
}
userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to save annotation", err)
}
item := annotations.Item{
OrgID: c.SignedInUser.GetOrgID(),
UserID: c.UserID,
UserID: userID,
DashboardID: cmd.DashboardId,
PanelID: cmd.PanelId,
Epoch: cmd.Time,
@ -214,9 +220,14 @@ func (hs *HTTPServer) PostGraphiteAnnotation(c *contextmodel.ReqContext) respons
return response.Error(400, "Failed to save Graphite annotation", err)
}
userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to save Graphite annotation", err)
}
item := annotations.Item{
OrgID: c.SignedInUser.GetOrgID(),
UserID: c.UserID,
UserID: userID,
Epoch: cmd.When * 1000,
Text: text,
Tags: tagsArray,
@ -264,9 +275,15 @@ func (hs *HTTPServer) UpdateAnnotation(c *contextmodel.ReqContext) response.Resp
return dashboardGuardianResponse(err)
}
userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if err != nil {
return response.Error(http.StatusInternalServerError,
"Failed to update annotation", err)
}
item := annotations.Item{
OrgID: c.SignedInUser.GetOrgID(),
UserID: c.UserID,
UserID: userID,
ID: annotationID,
Epoch: cmd.Time,
EpochEnd: cmd.TimeEnd,
@ -319,9 +336,15 @@ func (hs *HTTPServer) PatchAnnotation(c *contextmodel.ReqContext) response.Respo
return dashboardGuardianResponse(err)
}
userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if err != nil {
return response.Error(http.StatusInternalServerError,
"Failed to update annotation", err)
}
existing := annotations.Item{
OrgID: c.SignedInUser.GetOrgID(),
UserID: c.UserID,
UserID: userID,
ID: annotationID,
Epoch: annotation.Time,
EpochEnd: annotation.TimeEnd,

View File

@ -238,7 +238,7 @@ func TestAPI_Annotations(t *testing.T) {
body = strings.NewReader(tt.body)
}
req := webtest.RequestWithSignedInUser(server.NewRequest(tt.method, tt.path, body), userWithPermissions(1, tt.permissions))
req := webtest.RequestWithSignedInUser(server.NewRequest(tt.method, tt.path, body), authedUserWithPermissions(1, 1, tt.permissions))
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, tt.expectedCode, res.StatusCode)

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/metrics"
"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/dashboardsnapshots"
@ -119,10 +120,16 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *contextmodel.ReqContext) respon
cmd.Name = "Unnamed snapshot"
}
userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if err != nil {
return response.Error(http.StatusInternalServerError,
"Failed to create external snapshot", err)
}
var snapshotUrl string
cmd.ExternalURL = ""
cmd.OrgID = c.SignedInUser.GetOrgID()
cmd.UserID = c.UserID
cmd.UserID = userID
originalDashboardURL, err := createOriginalDashboardURL(&cmd)
if err != nil {
return response.Error(http.StatusInternalServerError, "Invalid app URL", err)

View File

@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/setting"
@ -372,9 +373,15 @@ func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Respons
return response.Error(http.StatusBadRequest, "bad request data", err)
}
userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if err != nil {
return response.Error(http.StatusInternalServerError,
"Failed to add datasource", err)
}
datasourcesLogger.Debug("Received command to add data source", "url", cmd.URL)
cmd.OrgID = c.SignedInUser.GetOrgID()
cmd.UserID = c.UserID
cmd.UserID = userID
if cmd.URL != "" {
if resp := validateURL(cmd.Type, cmd.URL); resp != nil {
return resp

View File

@ -94,6 +94,7 @@ func TestAddDataSource_InvalidURL(t *testing.T) {
Access: "direct",
Type: "test",
})
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{})
return hs.AddDataSource(c)
}))
@ -125,6 +126,7 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) {
Access: "direct",
Type: "test",
})
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{})
return hs.AddDataSource(c)
}))
@ -156,6 +158,7 @@ func TestAddDataSource_InvalidJSONData(t *testing.T) {
Type: "test",
JsonData: jsonData,
})
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{})
return hs.AddDataSource(c)
}))
@ -179,6 +182,7 @@ func TestUpdateDataSource_InvalidURL(t *testing.T) {
Access: "direct",
Type: "test",
})
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{})
return hs.AddDataSource(c)
}))
@ -208,6 +212,7 @@ func TestUpdateDataSource_InvalidJSONData(t *testing.T) {
Type: "test",
JsonData: jsonData,
})
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{})
return hs.AddDataSource(c)
}))
@ -239,6 +244,8 @@ func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) {
Access: "direct",
Type: "test",
})
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{})
return hs.AddDataSource(c)
}))
@ -375,7 +382,7 @@ func TestAPI_datasources_AccessControl(t *testing.T) {
body = strings.NewReader(tt.body)
}
res, err := server.SendJSON(webtest.RequestWithSignedInUser(server.NewRequest(tt.method, url, body), userWithPermissions(1, tt.permission)))
res, err := server.SendJSON(webtest.RequestWithSignedInUser(server.NewRequest(tt.method, url, body), authedUserWithPermissions(1, 1, tt.permission)))
require.NoError(t, err)
assert.Equal(t, tt.expectedCode, res.StatusCode)
require.NoError(t, res.Body.Close())

View File

@ -30,7 +30,9 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
return nil, err
}
prefsQuery := pref.GetPreferenceWithDefaultsQuery{UserID: c.UserID, OrgID: c.SignedInUser.GetOrgID(), Teams: c.Teams}
userID, _ := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
prefsQuery := pref.GetPreferenceWithDefaultsQuery{UserID: userID, OrgID: c.SignedInUser.GetOrgID(), Teams: c.Teams}
prefs, err := hs.preferenceService.GetWithDefaults(c.Req.Context(), &prefsQuery)
if err != nil {
return nil, err
@ -81,7 +83,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
theme := hs.getThemeForIndexData(prefs.Theme, c.Query("theme"))
userOrgCount := 1
userOrgs, err := hs.orgService.GetUserOrgList(c.Req.Context(), &org.GetUserOrgListQuery{UserID: c.UserID})
userOrgs, err := hs.orgService.GetUserOrgList(c.Req.Context(), &org.GetUserOrgListQuery{UserID: userID})
if err != nil {
hs.log.Error("Failed to count user orgs", "error", err)
}
@ -95,16 +97,16 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
data := dtos.IndexViewData{
User: &dtos.CurrentUser{
Id: c.UserID,
Id: userID,
IsSignedIn: c.IsSignedIn,
Login: c.Login,
Email: c.Email,
Email: c.SignedInUser.GetEmail(),
Name: c.Name,
OrgId: c.SignedInUser.GetOrgID(),
OrgName: c.OrgName,
OrgRole: c.OrgRole,
OrgRole: c.SignedInUser.GetOrgRole(),
OrgCount: userOrgCount,
GravatarUrl: dtos.GetGravatarUrl(c.Email),
GravatarUrl: dtos.GetGravatarUrl(c.SignedInUser.GetEmail()),
IsGrafanaAdmin: c.IsGrafanaAdmin,
Theme: theme.ID,
LightTheme: theme.Type == "light",

View File

@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/infra/network"
"github.com/grafana/grafana/pkg/middleware/cookies"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/authn"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@ -240,9 +241,14 @@ func (hs *HTTPServer) loginUserWithUser(user *user.User, c *contextmodel.ReqCont
}
func (hs *HTTPServer) Logout(c *contextmodel.ReqContext) {
userID, errID := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if errID != nil {
hs.log.Error("failed to retrieve user ID", "error", errID)
}
// If SAML is enabled and this is a SAML user use saml logout
if hs.samlSingleLogoutEnabled() {
getAuthQuery := loginservice.GetAuthInfoQuery{UserId: c.UserID}
getAuthQuery := loginservice.GetAuthInfoQuery{UserId: userID}
if authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil {
if authInfo.AuthModule == loginservice.SAMLAuthModule {
c.Redirect(hs.Cfg.AppSubURL + "/logout/saml")
@ -266,7 +272,7 @@ func (hs *HTTPServer) Logout(c *contextmodel.ReqContext) {
}
if err := hs.oauthTokenService.InvalidateOAuthTokens(c.Req.Context(), entry); err != nil {
hs.log.Warn("failed to invalidate oauth tokens for user", "userId", c.UserID, "error", err)
hs.log.Warn("failed to invalidate oauth tokens for user", "userId", userID, "error", err)
}
}
@ -284,7 +290,7 @@ func (hs *HTTPServer) Logout(c *contextmodel.ReqContext) {
}
c.Redirect(rdUrl)
} else {
hs.log.Info("Successful Logout", "User", c.Email)
hs.log.Info("Successful Logout", "User", c.SignedInUser.GetEmail())
c.Redirect(hs.Cfg.AppSubURL + "/login")
}
}
@ -329,7 +335,16 @@ func (hs *HTTPServer) RedirectResponseWithError(c *contextmodel.ReqContext, err
func (hs *HTTPServer) redirectURLWithErrorCookie(c *contextmodel.ReqContext, err error) string {
setCookie := true
if hs.Features.IsEnabled(featuremgmt.FlagIndividualCookiePreferences) {
prefsQuery := pref.GetPreferenceWithDefaultsQuery{UserID: c.UserID, OrgID: c.SignedInUser.GetOrgID(), Teams: c.Teams}
var userID int64
if c.SignedInUser != nil && !c.SignedInUser.IsNil() {
var errID error
userID, errID = identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if errID != nil {
hs.log.Error("failed to retrieve user ID", "error", errID)
}
}
prefsQuery := pref.GetPreferenceWithDefaultsQuery{UserID: userID, OrgID: c.SignedInUser.GetOrgID(), Teams: c.Teams}
prefs, err := hs.preferenceService.GetWithDefaults(c.Req.Context(), &prefsQuery)
if err != nil {
c.Redirect(hs.Cfg.AppSubURL + "/login")

View File

@ -7,6 +7,7 @@ import (
"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/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
pref "github.com/grafana/grafana/pkg/services/preference"
@ -19,7 +20,13 @@ func (hs *HTTPServer) SetHomeDashboard(c *contextmodel.ReqContext) response.Resp
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
cmd.UserID = c.UserID
userID, errID := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if errID != nil {
return response.Error(http.StatusInternalServerError, "Failed to set home dashboard", errID)
}
cmd.UserID = userID
cmd.OrgID = c.SignedInUser.GetOrgID()
// the default value of HomeDashboardID is taken from input, when HomeDashboardID is set also,
@ -56,7 +63,12 @@ func (hs *HTTPServer) SetHomeDashboard(c *contextmodel.ReqContext) response.Resp
// 401: unauthorisedError
// 500: internalServerError
func (hs *HTTPServer) GetUserPreferences(c *contextmodel.ReqContext) response.Response {
return hs.getPreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), c.UserID, 0)
userID, errID := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if errID != nil {
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 {
@ -124,7 +136,13 @@ func (hs *HTTPServer) UpdateUserPreferences(c *contextmodel.ReqContext) response
if err := web.Bind(c.Req, &dtoCmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return hs.updatePreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), c.UserID, 0, &dtoCmd)
userID, errID := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if errID != nil {
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 {
@ -182,7 +200,13 @@ func (hs *HTTPServer) PatchUserPreferences(c *contextmodel.ReqContext) response.
if err := web.Bind(c.Req, &dtoCmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return hs.patchPreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), c.UserID, 0, &dtoCmd)
userID, errID := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if errID != nil {
return response.Error(http.StatusInternalServerError, "Failed to update user preferences", errID)
}
return hs.patchPreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), userID, 0, &dtoCmd)
}
func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, teamId int64, dtoCmd *dtos.PatchPrefsCmd) response.Response {

View File

@ -129,6 +129,7 @@ func TestAPIEndpoint_PatchUserPreferences(t *testing.T) {
input := strings.NewReader(testPatchUserPreferencesCmd)
t.Run("Returns 200 on success", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPatch, patchUserPreferencesUrl, input), &user.SignedInUser{
UserID: 1,
OrgID: 1,
OrgRole: org.RoleAdmin,
})
@ -141,6 +142,7 @@ func TestAPIEndpoint_PatchUserPreferences(t *testing.T) {
input = strings.NewReader(testPatchUserPreferencesCmdBad)
t.Run("Returns 400 with bad data", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPatch, patchUserPreferencesUrl, input), &user.SignedInUser{
UserID: 1,
OrgID: 1,
OrgRole: org.RoleAdmin,
})
@ -153,6 +155,7 @@ func TestAPIEndpoint_PatchUserPreferences(t *testing.T) {
input = strings.NewReader(testUpdateOrgPreferencesWithHomeDashboardUIDCmd)
t.Run("Returns 200 on success", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPatch, patchUserPreferencesUrl, input), &user.SignedInUser{
UserID: 1,
OrgID: 1,
OrgRole: org.RoleAdmin,
})
@ -174,6 +177,7 @@ func TestAPIEndpoint_PatchOrgPreferences(t *testing.T) {
input := strings.NewReader(testPatchOrgPreferencesCmd)
t.Run("Returns 200 on success", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPatch, patchOrgPreferencesUrl, input), &user.SignedInUser{
UserID: 1,
OrgID: 1,
OrgRole: org.RoleAdmin,
Permissions: map[int64]map[string][]string{1: {accesscontrol.ActionOrgsPreferencesWrite: {}}},
@ -187,6 +191,7 @@ func TestAPIEndpoint_PatchOrgPreferences(t *testing.T) {
input = strings.NewReader(testPatchOrgPreferencesCmdBad)
t.Run("Returns 400 with bad data", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPatch, patchOrgPreferencesUrl, input), &user.SignedInUser{
UserID: 1,
OrgID: 1,
OrgRole: org.RoleAdmin,
Permissions: map[int64]map[string][]string{1: {accesscontrol.ActionOrgsPreferencesWrite: {}}},

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/util"
@ -53,14 +54,19 @@ func (hs *HTTPServer) RenderToPng(c *contextmodel.ReqContext) {
headers["Accept-Language"] = acceptLanguageHeader
}
userID, errID := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if errID != nil {
hs.log.Error("Failed to parse user id", "err", errID)
}
result, err := hs.RenderService.Render(c.Req.Context(), rendering.Opts{
TimeoutOpts: rendering.TimeoutOpts{
Timeout: time.Duration(timeout) * time.Second,
},
AuthOpts: rendering.AuthOpts{
OrgID: c.SignedInUser.GetOrgID(),
UserID: c.UserID,
OrgRole: c.OrgRole,
UserID: userID,
OrgRole: c.SignedInUser.GetOrgRole(),
},
Width: width,
Height: height,

View File

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
tempuser "github.com/grafana/grafana/pkg/services/temp_user"
"github.com/grafana/grafana/pkg/services/user"
@ -48,11 +49,16 @@ func (hs *HTTPServer) SignUp(c *contextmodel.ReqContext) response.Response {
return response.Error(422, "User with same email address already exists", nil)
}
userID, errID := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if errID != nil {
hs.log.Error("Failed to parse user id", "err", errID)
}
cmd := tempuser.CreateTempUserCommand{}
cmd.OrgID = -1
cmd.Email = form.Email
cmd.Status = tempuser.TmpUserSignUpStarted
cmd.InvitedByUserID = c.UserID
cmd.InvitedByUserID = userID
cmd.Code, err = util.GetRandomString(20)
if err != nil {
return response.Error(500, "Failed to generate random string", err)

View File

@ -87,3 +87,21 @@ func IntIdentifier(namespace, identifier string) (int64, error) {
return 0, ErrNotIntIdentifier
}
// UserIdentifier converts a string identifier to an int64.
// Errors if the identifier is not initialized or if namespace is not recognized.
// Returns 0 if the namespace is not user or service account
func UserIdentifier(namespace, identifier string) (int64, error) {
userID, err := IntIdentifier(namespace, identifier)
if err != nil {
// FIXME: return this error once entity namespaces are handled by stores
return 0, nil
}
switch namespace {
case NamespaceUser, NamespaceServiceAccount:
return userID, nil
}
return 0, nil
}