Chore: Fix error handling in postDashboard, remove UserDisplayDTO, fix live redis client initialization (#87206)

* clean up error handling in postDashboard and remove UserDisplayDTO

* replace GetUserUID with GetUID and GetNamespacedUID, enforce namespace constant type

* lint fix

* lint fix

* more lint fixes
This commit is contained in:
Dan Cech 2024-05-06 14:17:34 -04:00 committed by GitHub
parent ba8b4bde3a
commit 41bee274fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 206 additions and 183 deletions

View File

@ -336,17 +336,13 @@ func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Respo
return response.Error(http.StatusInternalServerError, "Failed to delete dashboard", err)
}
userDTODisplay, err := user.NewUserDisplayDTOFromRequester(c.SignedInUser)
if err != nil {
return response.Error(http.StatusInternalServerError, "Error while parsing the user DTO model", err)
}
if hs.Live != nil {
err := hs.Live.GrafanaScope.Dashboards.DashboardDeleted(c.SignedInUser.GetOrgID(), userDTODisplay, dash.UID)
err := hs.Live.GrafanaScope.Dashboards.DashboardDeleted(c.SignedInUser.GetOrgID(), c.SignedInUser, dash.UID)
if err != nil {
hs.log.Error("Failed to broadcast delete info", "dashboard", dash.UID, "error", err)
}
}
return response.JSON(http.StatusOK, util.DynMap{
"title": dash.Title,
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
@ -440,7 +436,7 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S
Overwrite: cmd.Overwrite,
}
dashboard, err := hs.DashboardService.SaveDashboard(ctx, dashItem, allowUiUpdate)
dashboard, saveErr := hs.DashboardService.SaveDashboard(ctx, dashItem, allowUiUpdate)
if hs.Live != nil {
// Tell everyone listening that the dashboard changed
@ -448,18 +444,13 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S
dashboard = dash // the original request
}
userDTODisplay, err := user.NewUserDisplayDTOFromRequester(c.SignedInUser)
if err != nil {
return response.Error(http.StatusInternalServerError, "Error while parsing the user DTO model", err)
}
// This will broadcast all save requests only if a `gitops` observer exists.
// gitops is useful when trying to save dashboards in an environment where the user can not save
channel := hs.Live.GrafanaScope.Dashboards
liveerr := channel.DashboardSaved(c.SignedInUser.GetOrgID(), userDTODisplay, cmd.Message, dashboard, err)
liveerr := channel.DashboardSaved(c.SignedInUser.GetOrgID(), c.SignedInUser, cmd.Message, dashboard, saveErr)
// When an error exists, but the value broadcast to a gitops listener return 202
if liveerr == nil && err != nil && channel.HasGitOpsObserver(c.SignedInUser.GetOrgID()) {
if liveerr == nil && saveErr != nil && channel.HasGitOpsObserver(c.SignedInUser.GetOrgID()) {
return response.JSON(http.StatusAccepted, util.DynMap{
"status": "pending",
"message": "changes were broadcast to the gitops listener",
@ -471,8 +462,8 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S
}
}
if err != nil {
return apierrors.ToDashboardErrorResponse(ctx, hs.pluginStore, err)
if saveErr != nil {
return apierrors.ToDashboardErrorResponse(ctx, hs.pluginStore, saveErr)
}
// Clear permission cache for the user who's created the dashboard, so that new permissions are fetched for their next call

View File

@ -103,8 +103,7 @@ func (hs *HTTPServer) AddOrgInvite(c *contextmodel.ReqContext) response.Response
namespace, identifier := c.SignedInUser.GetNamespacedID()
var userID int64
switch namespace {
case identity.NamespaceUser, identity.NamespaceServiceAccount:
if namespace == identity.NamespaceUser || namespace == identity.NamespaceServiceAccount {
var err error
userID, err = strconv.ParseInt(identifier, 10, 64)
if err != nil {

View File

@ -36,7 +36,7 @@ func (hs *HTTPServer) GetSignedInUser(c *contextmodel.ReqContext) response.Respo
return response.JSON(http.StatusOK, user.UserProfileDTO{
IsGrafanaAdmin: c.SignedInUser.GetIsGrafanaAdmin(),
OrgID: c.SignedInUser.GetOrgID(),
UID: strings.Join([]string{namespace, identifier}, ":"),
UID: c.SignedInUser.GetID().String(),
Name: c.SignedInUser.NameOrFallback(),
Email: c.SignedInUser.GetEmail(),
Login: c.SignedInUser.GetLogin(),

View File

@ -215,7 +215,7 @@ func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, token
return userInfo, nil
}
func (s *SocialGitlab) extractFromAPI(ctx context.Context, client *http.Client, token *oauth2.Token) (*userData, error) {
func (s *SocialGitlab) extractFromAPI(ctx context.Context, client *http.Client, _ *oauth2.Token) (*userData, error) {
apiResp := &apiData{}
response, err := s.httpGet(ctx, client, s.info.ApiUrl+"/user")
if err != nil {

View File

@ -220,7 +220,7 @@ func (s *SocialGoogle) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption)
return s.SocialBase.Config.AuthCodeURL(state, opts...)
}
func (s *SocialGoogle) extractFromToken(ctx context.Context, client *http.Client, token *oauth2.Token) (*googleUserData, error) {
func (s *SocialGoogle) extractFromToken(_ context.Context, _ *http.Client, token *oauth2.Token) (*googleUserData, error) {
s.log.Debug("Extracting user info from OAuth token")
idToken := token.Extra("id_token")

View File

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"github.com/grafana/grafana/pkg/registry"
@ -100,22 +99,18 @@ func (s *SearchOptions) ComputeUserID() (int64, error) {
if s.NamespacedID == "" {
return 0, errors.New("namespacedID must be set")
}
// Split namespaceID into namespace and ID
parts := strings.Split(s.NamespacedID, ":")
// Validate namespace ID format
if len(parts) != 2 {
return 0, fmt.Errorf("invalid namespaced ID: %s", s.NamespacedID)
}
// Validate namespace type is user or service account
if parts[0] != identity.NamespaceUser && parts[0] != identity.NamespaceServiceAccount {
return 0, fmt.Errorf("invalid namespace: %s", parts[0])
}
// Validate namespace ID is a number
id, err := strconv.ParseInt(parts[1], 10, 64)
id, err := identity.ParseNamespaceID(s.NamespacedID)
if err != nil {
return 0, fmt.Errorf("invalid namespaced ID: %s", s.NamespacedID)
return 0, err
}
return id, nil
// Validate namespace type is user or service account
if id.Namespace() != identity.NamespaceUser && id.Namespace() != identity.NamespaceServiceAccount {
return 0, fmt.Errorf("invalid namespace: %s", id.Namespace())
}
return id.ParseInt()
}
type SyncUserRolesCommand struct {

View File

@ -2,6 +2,7 @@ package acimpl
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
@ -537,7 +538,7 @@ func TestService_SearchUsersPermissions(t *testing.T) {
// only the user's basic roles and the user's stored permissions
name: "check namespacedId filter works correctly",
siuPermissions: listAllPerms,
searchOption: accesscontrol.SearchOptions{NamespacedID: identity.NamespaceServiceAccount + ":1"},
searchOption: accesscontrol.SearchOptions{NamespacedID: fmt.Sprintf("%s:1", identity.NamespaceServiceAccount)},
ramRoles: map[string]*accesscontrol.RoleDTO{
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
@ -607,7 +608,7 @@ func TestService_SearchUserPermissions(t *testing.T) {
name: "ram only",
searchOption: accesscontrol.SearchOptions{
ActionPrefix: "teams",
NamespacedID: identity.NamespaceUser + ":2",
NamespacedID: fmt.Sprintf("%s:2", identity.NamespaceUser),
},
ramRoles: map[string]*accesscontrol.RoleDTO{
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
@ -632,7 +633,7 @@ func TestService_SearchUserPermissions(t *testing.T) {
name: "stored only",
searchOption: accesscontrol.SearchOptions{
ActionPrefix: "teams",
NamespacedID: identity.NamespaceUser + ":2",
NamespacedID: fmt.Sprintf("%s:2", identity.NamespaceUser),
},
storedPerms: map[int64][]accesscontrol.Permission{
1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}},
@ -652,7 +653,7 @@ func TestService_SearchUserPermissions(t *testing.T) {
name: "ram and stored",
searchOption: accesscontrol.SearchOptions{
ActionPrefix: "teams",
NamespacedID: identity.NamespaceUser + ":2",
NamespacedID: fmt.Sprintf("%s:2", identity.NamespaceUser),
},
ramRoles: map[string]*accesscontrol.RoleDTO{
string(roletype.RoleAdmin): {Permissions: []accesscontrol.Permission{
@ -682,7 +683,7 @@ func TestService_SearchUserPermissions(t *testing.T) {
name: "check action prefix filter works correctly",
searchOption: accesscontrol.SearchOptions{
ActionPrefix: "teams",
NamespacedID: identity.NamespaceUser + ":1",
NamespacedID: fmt.Sprintf("%s:1", identity.NamespaceUser),
},
ramRoles: map[string]*accesscontrol.RoleDTO{
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
@ -704,7 +705,7 @@ func TestService_SearchUserPermissions(t *testing.T) {
name: "check action filter works correctly",
searchOption: accesscontrol.SearchOptions{
Action: accesscontrol.ActionTeamsRead,
NamespacedID: identity.NamespaceUser + ":1",
NamespacedID: fmt.Sprintf("%s:1", identity.NamespaceUser),
},
ramRoles: map[string]*accesscontrol.RoleDTO{
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{

View File

@ -549,7 +549,7 @@ func TestIntegrationAccessControlStore_SearchUsersPermissions(t *testing.T) {
},
options: accesscontrol.SearchOptions{
ActionPrefix: "teams:",
NamespacedID: identity.NamespaceUser + ":1",
NamespacedID: fmt.Sprintf("%s:1", identity.NamespaceUser),
},
wantPerm: map[int64][]accesscontrol.Permission{
1: {{Action: "teams:read", Scope: "teams:id:1"}, {Action: "teams:read", Scope: "teams:id:10"},

View File

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
)
func setupTestEnv(b *testing.B, resourceCount, permissionPerResource int) (map[string][]string, map[string]bool) {
func setupTestEnv(resourceCount, permissionPerResource int) (map[string][]string, map[string]bool) {
res := map[string][]string{}
ids := make(map[string]bool, resourceCount)
@ -25,7 +25,7 @@ func setupTestEnv(b *testing.B, resourceCount, permissionPerResource int) (map[s
}
func benchGetMetadata(b *testing.B, resourceCount, permissionPerResource int) {
permissions, ids := setupTestEnv(b, resourceCount, permissionPerResource)
permissions, ids := setupTestEnv(resourceCount, permissionPerResource)
b.ResetTimer()
var metadata map[string]Metadata

View File

@ -6,26 +6,43 @@ import (
"strings"
)
type Namespace string
const (
NamespaceUser = "user"
NamespaceAPIKey = "api-key"
NamespaceServiceAccount = "service-account"
NamespaceAnonymous = "anonymous"
NamespaceRenderService = "render"
NamespaceAccessPolicy = "access-policy"
NamespaceUser Namespace = "user"
NamespaceAPIKey Namespace = "api-key"
NamespaceServiceAccount Namespace = "service-account"
NamespaceAnonymous Namespace = "anonymous"
NamespaceRenderService Namespace = "render"
NamespaceAccessPolicy Namespace = "access-policy"
NamespaceEmpty Namespace = ""
)
var AnonymousNamespaceID = MustNewNamespaceID(NamespaceAnonymous, 0)
var namespaceLookup = map[string]struct{}{
NamespaceUser: {},
NamespaceAPIKey: {},
NamespaceServiceAccount: {},
NamespaceAnonymous: {},
NamespaceRenderService: {},
NamespaceAccessPolicy: {},
func (n Namespace) String() string {
return string(n)
}
func ParseNamespace(str string) (Namespace, error) {
switch str {
case string(NamespaceUser):
return NamespaceUser, nil
case string(NamespaceAPIKey):
return NamespaceAPIKey, nil
case string(NamespaceServiceAccount):
return NamespaceServiceAccount, nil
case string(NamespaceAnonymous):
return NamespaceAnonymous, nil
case string(NamespaceRenderService):
return NamespaceRenderService, nil
case string(NamespaceAccessPolicy):
return NamespaceAccessPolicy, nil
default:
return "", ErrInvalidNamespaceID.Errorf("got invalid namespace %s", str)
}
}
var AnonymousNamespaceID = MustNewNamespaceID(NamespaceAnonymous, 0)
func ParseNamespaceID(str string) (NamespaceID, error) {
var namespaceID NamespaceID
@ -34,13 +51,12 @@ func ParseNamespaceID(str string) (NamespaceID, error) {
return namespaceID, ErrInvalidNamespaceID.Errorf("expected namespace id to have 2 parts")
}
namespace, id := parts[0], parts[1]
if _, ok := namespaceLookup[namespace]; !ok {
return namespaceID, ErrInvalidNamespaceID.Errorf("got invalid namespace %s", namespace)
namespace, err := ParseNamespace(parts[0])
if err != nil {
return namespaceID, err
}
namespaceID.id = id
namespaceID.id = parts[1]
namespaceID.namespace = namespace
return namespaceID, nil
@ -57,19 +73,16 @@ func MustParseNamespaceID(str string) NamespaceID {
}
// NewNamespaceID creates a new NamespaceID, will fail for invalid namespace.
func NewNamespaceID(namespace string, id int64) (NamespaceID, error) {
var namespaceID NamespaceID
if _, ok := namespaceLookup[namespace]; !ok {
return namespaceID, ErrInvalidNamespaceID.Errorf("got invalid namespace %s", namespace)
}
namespaceID.id = strconv.FormatInt(id, 10)
namespaceID.namespace = namespace
return namespaceID, nil
func NewNamespaceID(namespace Namespace, id int64) (NamespaceID, error) {
return NamespaceID{
id: strconv.FormatInt(id, 10),
namespace: namespace,
}, nil
}
// MustNewNamespaceID creates a new NamespaceID, will panic for invalid namespace.
// Suitable to use in tests or when we can guarantee that we pass a correct format.
func MustNewNamespaceID(namespace string, id int64) NamespaceID {
func MustNewNamespaceID(namespace Namespace, id int64) NamespaceID {
namespaceID, err := NewNamespaceID(namespace, id)
if err != nil {
panic(err)
@ -79,17 +92,25 @@ func MustNewNamespaceID(namespace string, id int64) NamespaceID {
// NewNamespaceIDUnchecked creates a new NamespaceID without checking if namespace is valid.
// It us up to the caller to ensure that namespace is valid.
func NewNamespaceIDUnchecked(namespace string, id int64) NamespaceID {
func NewNamespaceIDUnchecked(namespace Namespace, id int64) NamespaceID {
return NamespaceID{
id: strconv.FormatInt(id, 10),
namespace: namespace,
}
}
// NewNamespaceIDString creates a new NamespaceID with a string id
func NewNamespaceIDString(namespace Namespace, id string) NamespaceID {
return NamespaceID{
id: id,
namespace: namespace,
}
}
// FIXME: use this instead of encoded string through the codebase
type NamespaceID struct {
id string
namespace string
namespace Namespace
}
func (ni NamespaceID) ID() string {
@ -100,11 +121,11 @@ func (ni NamespaceID) ParseInt() (int64, error) {
return strconv.ParseInt(ni.id, 10, 64)
}
func (ni NamespaceID) Namespace() string {
func (ni NamespaceID) Namespace() Namespace {
return ni.namespace
}
func (ni NamespaceID) IsNamespace(expected ...string) bool {
func (ni NamespaceID) IsNamespace(expected ...Namespace) bool {
return IsNamespace(ni.namespace, expected...)
}

View File

@ -12,7 +12,12 @@ type Requester interface {
GetID() NamespaceID
// GetNamespacedID returns the namespace and ID of the active entity.
// The namespace is one of the constants defined in pkg/services/auth/identity.
GetNamespacedID() (namespace string, identifier string)
GetNamespacedID() (namespace Namespace, identifier string)
// GetID returns namespaced id for the entity
GetUID() NamespaceID
// GetNamespacedID returns the namespace and ID of the active entity.
// The namespace is one of the constants defined in pkg/services/auth/identity.
GetNamespacedUID() (namespace Namespace, identifier string)
// 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
@ -65,7 +70,7 @@ type Requester interface {
}
// IsNamespace returns true if namespace matches any expected namespace
func IsNamespace(namespace string, expected ...string) bool {
func IsNamespace(namespace Namespace, expected ...Namespace) bool {
for _, e := range expected {
if namespace == e {
return true
@ -78,7 +83,7 @@ func IsNamespace(namespace string, expected ...string) bool {
// IntIdentifier converts a string identifier to an int64.
// Applicable for users, service accounts, api keys and renderer service.
// Errors if the identifier is not initialized or if namespace is not recognized.
func IntIdentifier(namespace, identifier string) (int64, error) {
func IntIdentifier(namespace Namespace, identifier string) (int64, error) {
if IsNamespace(namespace, NamespaceUser, NamespaceAPIKey, NamespaceServiceAccount, NamespaceRenderService) {
id, err := strconv.ParseInt(identifier, 10, 64)
if err != nil {
@ -98,7 +103,7 @@ func IntIdentifier(namespace, identifier string) (int64, error) {
// 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) {
func UserIdentifier(namespace Namespace, identifier string) (int64, error) {
userID, err := IntIdentifier(namespace, identifier)
if err != nil {
// FIXME: return this error once entity namespaces are handled by stores

View File

@ -82,7 +82,7 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri
Claims: jwt.Claims{
Issuer: s.cfg.AppURL,
Audience: getAudience(id.GetOrgID()),
Subject: getSubject(namespace, identifier),
Subject: getSubject(namespace.String(), identifier),
Expiry: jwt.NewNumericDate(now.Add(tokenTTL)),
IssuedAt: jwt.NewNumericDate(now),
},

View File

@ -373,7 +373,7 @@ func (s *Service) resolveIdenity(ctx context.Context, orgID int64, namespaceID a
}}, nil
}
resolver, ok := s.idenityResolverClients[namespaceID.Namespace()]
resolver, ok := s.idenityResolverClients[namespaceID.Namespace().String()]
if !ok {
return nil, authn.ErrUnsupportedIdentity.Errorf("no resolver for : %s", namespaceID.Namespace())
}

View File

@ -514,7 +514,7 @@ func TestService_ResolveIdentity(t *testing.T) {
t.Run("should resolve for valid namespace if client is registered", func(t *testing.T) {
svc := setupTests(t, func(svc *Service) {
svc.RegisterClient(&authntest.MockClient{
NamespaceFunc: func() string { return authn.NamespaceAPIKey },
NamespaceFunc: func() string { return authn.NamespaceAPIKey.String() },
ResolveIdentityFunc: func(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) {
return &authn.Identity{}, nil
},

View File

@ -135,7 +135,7 @@ func (s *APIKey) Priority() uint {
}
func (s *APIKey) Namespace() string {
return authn.NamespaceAPIKey
return authn.NamespaceAPIKey.String()
}
func (s *APIKey) ResolveIdentity(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) {

View File

@ -71,16 +71,27 @@ type Identity struct {
// IDToken is a signed token representing the identity that can be forwarded to plugins and external services.
// Will only be set when featuremgmt.FlagIdForwarding is enabled.
IDToken string
// UserUID is the unique identifier for the entity in the Grafana database.
UserUID string
}
func (i *Identity) GetID() NamespaceID {
return i.ID
}
func (i *Identity) GetNamespacedID() (namespace string, identifier string) {
func (i *Identity) GetNamespacedID() (namespace identity.Namespace, identifier string) {
return i.ID.Namespace(), i.ID.ID()
}
func (i *Identity) GetUID() NamespaceID {
ns, uid := i.GetNamespacedUID()
return identity.NewNamespaceIDString(ns, uid)
}
func (i *Identity) GetNamespacedUID() (namespace identity.Namespace, identifier string) {
return i.ID.Namespace(), i.UserUID
}
func (i *Identity) GetAuthID() string {
return i.AuthID
}

View File

@ -158,7 +158,7 @@ func (h *ContextHandler) addIDHeaderEndOfRequestFunc(ident identity.Requester) w
return
}
if _, ok := h.Cfg.IDResponseHeaderNamespaces[namespace]; !ok {
if _, ok := h.Cfg.IDResponseHeaderNamespaces[namespace.String()]; !ok {
return
}

View File

@ -109,12 +109,11 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb
req.FolderUid = folder.UID
}
namespaceID, identifier := req.User.GetNamespacedID()
namespace, identifier := req.User.GetNamespacedID()
userID := int64(0)
switch namespaceID {
case identity.NamespaceUser, identity.NamespaceServiceAccount:
userID, _ = identity.IntIdentifier(namespaceID, identifier)
if namespace == identity.NamespaceUser || namespace == identity.NamespaceServiceAccount {
userID, _ = identity.IntIdentifier(namespace, identifier)
}
saveCmd := dashboards.SaveDashboardCommand{

View File

@ -137,8 +137,8 @@ func (d *DashboardSnapshotStore) SearchDashboardSnapshots(ctx context.Context, q
namespace, id := query.SignedInUser.GetNamespacedID()
var userID int64
switch namespace {
case identity.NamespaceServiceAccount, identity.NamespaceUser:
if namespace == identity.NamespaceServiceAccount || namespace == identity.NamespaceUser {
var err error
userID, err = identity.IntIdentifier(namespace, id)
if err != nil {

View File

@ -140,8 +140,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn
userID := int64(0)
namespaceID, identifier := signedInUser.GetNamespacedID()
switch namespaceID {
case identity.NamespaceUser, identity.NamespaceServiceAccount:
if namespaceID == identity.NamespaceUser || namespaceID == identity.NamespaceServiceAccount {
userID, err = identity.IntIdentifier(namespaceID, identifier)
if err != nil {
l.log.Warn("Error while parsing userID", "namespaceID", namespaceID, "userID", identifier)
@ -589,8 +588,7 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU
var userID int64
namespaceID, identifier := signedInUser.GetNamespacedID()
switch namespaceID {
case identity.NamespaceUser, identity.NamespaceServiceAccount:
if namespaceID == identity.NamespaceUser || namespaceID == identity.NamespaceServiceAccount {
var errID error
userID, errID = identity.IntIdentifier(namespaceID, identifier)
if errID != nil {
@ -798,8 +796,7 @@ func (l *LibraryElementService) connectElementsToDashboardID(c context.Context,
namespaceID, identifier := signedInUser.GetNamespacedID()
userID := int64(0)
switch namespaceID {
case identity.NamespaceUser, identity.NamespaceServiceAccount:
if namespaceID == identity.NamespaceUser || namespaceID == identity.NamespaceServiceAccount {
userID, err = identity.IntIdentifier(namespaceID, identifier)
if err != nil {
l.log.Warn("Failed to parse user ID from namespace identifier", "namespace", namespaceID, "identifier", identifier, "error", err)

View File

@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/live/model"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
)
type actionType string
@ -28,11 +27,39 @@ const (
GitopsChannel = "grafana/dashboard/gitops"
)
type userDisplayDTO struct {
ID int64 `json:"id,omitempty"`
UID string `json:"uid,omitempty"`
Name string `json:"name,omitempty"`
Login string `json:"login,omitempty"`
AvatarURL string `json:"avatarUrl"`
}
// Static function to parse a requester into a userDisplayDTO
func newUserDisplayDTOFromRequester(requester identity.Requester) *userDisplayDTO {
userID := int64(0)
namespaceID, identifier := requester.GetNamespacedID()
if namespaceID == identity.NamespaceUser || namespaceID == identity.NamespaceServiceAccount {
userID, _ = identity.IntIdentifier(namespaceID, identifier)
}
namespaceID, uid := requester.GetNamespacedUID()
if namespaceID != identity.NamespaceUser && namespaceID != identity.NamespaceServiceAccount {
uid = ""
}
return &userDisplayDTO{
ID: userID,
UID: uid,
Login: requester.GetLogin(),
Name: requester.GetDisplayName(),
}
}
// DashboardEvent events related to dashboards
type dashboardEvent struct {
UID string `json:"uid"`
Action actionType `json:"action"` // saved, editing, deleted
User *user.UserDisplayDTO `json:"user,omitempty"`
User *userDisplayDTO `json:"user,omitempty"`
SessionID string `json:"sessionId,omitempty"`
Message string `json:"message,omitempty"`
Dashboard *dashboards.Dashboard `json:"dashboard,omitempty"`
@ -142,10 +169,7 @@ func (h *DashboardHandler) OnPublish(ctx context.Context, requester identity.Req
}
// Tell everyone who is editing
event.User, err = user.NewUserDisplayDTOFromRequester(requester)
if err != nil {
return model.PublishReply{}, backend.PublishStreamStatusNotFound, err
}
event.User = newUserDisplayDTOFromRequester(requester)
msg, err := json.Marshal(event)
if err != nil {
@ -177,7 +201,7 @@ func (h *DashboardHandler) publish(orgID int64, event dashboardEvent) error {
}
// DashboardSaved will broadcast to all connected dashboards
func (h *DashboardHandler) DashboardSaved(orgID int64, user *user.UserDisplayDTO, message string, dashboard *dashboards.Dashboard, err error) error {
func (h *DashboardHandler) DashboardSaved(orgID int64, requester identity.Requester, message string, dashboard *dashboards.Dashboard, err error) error {
if err != nil && !h.HasGitOpsObserver(orgID) {
return nil // only broadcast if it was OK
}
@ -185,7 +209,7 @@ func (h *DashboardHandler) DashboardSaved(orgID int64, user *user.UserDisplayDTO
msg := dashboardEvent{
UID: dashboard.UID,
Action: ActionSaved,
User: user,
User: newUserDisplayDTOFromRequester(requester),
Message: message,
Dashboard: dashboard,
}
@ -198,11 +222,11 @@ func (h *DashboardHandler) DashboardSaved(orgID int64, user *user.UserDisplayDTO
}
// DashboardDeleted will broadcast to all connected dashboards
func (h *DashboardHandler) DashboardDeleted(orgID int64, user *user.UserDisplayDTO, uid string) error {
func (h *DashboardHandler) DashboardDeleted(orgID int64, requester identity.Requester, uid string) error {
return h.publish(orgID, dashboardEvent{
UID: uid,
Action: ActionDeleted,
User: user,
User: newUserDisplayDTOFromRequester(requester),
})
}

View File

@ -54,7 +54,6 @@ import (
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil"
@ -138,7 +137,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
var managedStreamRunner *managedstream.Runner
var redisClient *redis.Client
if g.IsHA() && redisHealthy {
redisClient := redis.NewClient(&redis.Options{
redisClient = redis.NewClient(&redis.Options{
Addr: g.Cfg.LiveHAEngineAddress,
Password: g.Cfg.LiveHAEnginePassword,
})
@ -414,10 +413,10 @@ type DashboardActivityChannel interface {
// gitops workflow that knows if the value was saved to the local database or not
// in many cases all direct save requests will fail, but the request should be forwarded
// to any gitops observers
DashboardSaved(orgID int64, user *user.UserDisplayDTO, message string, dashboard *dashboards.Dashboard, err error) error
DashboardSaved(orgID int64, requester identity.Requester, message string, dashboard *dashboards.Dashboard, err error) error
// Called when a dashboard is deleted
DashboardDeleted(orgID int64, user *user.UserDisplayDTO, uid string) error
DashboardDeleted(orgID int64, requester identity.Requester, uid string) error
// Experimental! Indicate is GitOps is active. This really means
// someone is subscribed to the `grafana/dashboards/gitops` channel

View File

@ -136,10 +136,9 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
folderWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix)
userID := int64(0)
namespaceID, identifier := f.user.GetNamespacedID()
switch namespaceID {
case identity.NamespaceUser, identity.NamespaceServiceAccount:
userID, _ = identity.IntIdentifier(namespaceID, identifier)
namespace, identifier := f.user.GetNamespacedID()
if namespace == identity.NamespaceUser || namespace == identity.NamespaceServiceAccount {
userID, _ = identity.IntIdentifier(namespace, identifier)
}
orgID := f.user.GetOrgID()

View File

@ -34,10 +34,9 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses()
folderWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix)
userID := int64(0)
namespaceID, identifier := f.user.GetNamespacedID()
switch namespaceID {
case identity.NamespaceUser, identity.NamespaceServiceAccount:
userID, _ = identity.IntIdentifier(namespaceID, identifier)
namespace, identifier := f.user.GetNamespacedID()
if namespace == identity.NamespaceUser || namespace == identity.NamespaceServiceAccount {
userID, _ = identity.IntIdentifier(namespace, identifier)
}
orgID := f.user.GetOrgID()

View File

@ -2,6 +2,7 @@ package user
import (
"fmt"
"strconv"
"time"
"github.com/grafana/grafana/pkg/models/roletype"
@ -56,36 +57,6 @@ func (u *SignedInUser) NameOrFallback() string {
return u.Email
}
// TODO: There's a need to remove this struct since it creates a circular dependency
// DEPRECATED: This function uses `UserDisplayDTO` model which we want to remove
// In order to retrieve the user URL, we need the dto library. However, adding
// the dto library to the user service creates a circular dependency
func (u *SignedInUser) ToUserDisplayDTO() *UserDisplayDTO {
return &UserDisplayDTO{
ID: u.UserID,
UID: u.UserUID,
Login: u.Login,
Name: u.Name,
// AvatarURL: dtos.GetGravatarUrl(u.GetEmail()),
}
}
// Static function to parse a requester into a UserDisplayDTO
func NewUserDisplayDTOFromRequester(requester identity.Requester) (*UserDisplayDTO, error) {
userID := int64(0)
namespaceID, identifier := requester.GetNamespacedID()
if namespaceID == identity.NamespaceUser || namespaceID == identity.NamespaceServiceAccount {
userID, _ = identity.IntIdentifier(namespaceID, identifier)
}
return &UserDisplayDTO{
ID: userID,
Login: requester.GetLogin(),
Name: requester.GetDisplayName(),
}, nil
}
func (u *SignedInUser) HasRole(role roletype.RoleType) bool {
if u.IsGrafanaAdmin {
return true
@ -196,27 +167,52 @@ func (u *SignedInUser) GetOrgRole() roletype.RoleType {
// GetID returns namespaced id for the entity
func (u *SignedInUser) GetID() identity.NamespaceID {
switch {
case u.ApiKeyID != 0:
return namespacedID(identity.NamespaceAPIKey, u.ApiKeyID)
case u.IsServiceAccount:
return namespacedID(identity.NamespaceServiceAccount, u.UserID)
case u.UserID > 0:
return namespacedID(identity.NamespaceUser, u.UserID)
case u.IsAnonymous:
return namespacedID(identity.NamespaceAnonymous, 0)
case u.AuthenticatedBy == "render" && u.UserID == 0:
return namespacedID(identity.NamespaceRenderService, 0)
}
return u.NamespacedID
ns, id := u.GetNamespacedID()
return identity.NewNamespaceIDString(ns, id)
}
// 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) {
id := u.GetID()
return id.Namespace(), id.ID()
func (u *SignedInUser) GetNamespacedID() (identity.Namespace, string) {
switch {
case u.ApiKeyID != 0:
return identity.NamespaceAPIKey, strconv.FormatInt(u.ApiKeyID, 10)
case u.IsServiceAccount:
return identity.NamespaceServiceAccount, strconv.FormatInt(u.UserID, 10)
case u.UserID > 0:
return identity.NamespaceUser, strconv.FormatInt(u.UserID, 10)
case u.IsAnonymous:
return identity.NamespaceAnonymous, "0"
case u.AuthenticatedBy == "render" && u.UserID == 0:
return identity.NamespaceRenderService, "0"
}
return u.NamespacedID.Namespace(), u.NamespacedID.ID()
}
// GetUID returns namespaced uid for the entity
func (u *SignedInUser) GetUID() identity.NamespaceID {
ns, uid := u.GetNamespacedUID()
return identity.NewNamespaceIDString(ns, uid)
}
// GetNamespacedUID returns the namespace and UID of the active entity
// The namespace is one of the constants defined in pkg/services/auth/identity
func (u *SignedInUser) GetNamespacedUID() (identity.Namespace, string) {
switch {
case u.ApiKeyID != 0:
return identity.NamespaceAPIKey, fmt.Sprint(u.ApiKeyID)
case u.IsServiceAccount:
return identity.NamespaceServiceAccount, u.UserUID
case u.UserID > 0:
return identity.NamespaceUser, u.UserUID
case u.IsAnonymous:
return identity.NamespaceAnonymous, ""
case u.AuthenticatedBy == "render" && u.UserID == 0:
return identity.NamespaceRenderService, ""
}
return identity.NamespaceEmpty, ""
}
func (u *SignedInUser) GetAuthID() string {
@ -260,7 +256,3 @@ func (u *SignedInUser) GetDisplayName() string {
func (u *SignedInUser) GetIDToken() string {
return u.IDToken
}
func namespacedID(namespace string, id int64) identity.NamespaceID {
return identity.NewNamespaceIDUnchecked(namespace, id)
}

View File

@ -223,14 +223,6 @@ type ErrCaseInsensitiveLoginConflict struct {
Users []User
}
type UserDisplayDTO struct {
ID int64 `json:"id,omitempty"`
UID string `json:"uid,omitempty"`
Name string `json:"name,omitempty"`
Login string `json:"login,omitempty"`
AvatarURL string `json:"avatarUrl"`
}
func (e *ErrCaseInsensitiveLoginConflict) Unwrap() error {
return ErrCaseInsensitive
}

View File

@ -115,8 +115,7 @@ func ApplyUserHeader(sendUserHeader bool, req *http.Request, user identity.Reque
}
namespace, _ := user.GetNamespacedID()
switch namespace {
case identity.NamespaceUser, identity.NamespaceServiceAccount:
if namespace == identity.NamespaceUser || namespace == identity.NamespaceServiceAccount {
req.Header.Set(UserHeaderName, user.GetLogin())
}
}