diff --git a/pkg/services/auth/identity/requester.go b/pkg/services/auth/identity/requester.go index 1e257792081..1995769f81b 100644 --- a/pkg/services/auth/identity/requester.go +++ b/pkg/services/auth/identity/requester.go @@ -12,12 +12,10 @@ 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. + // Deprecated: use GetID instead GetNamespacedID() (namespace Namespace, identifier string) - // GetID returns namespaced id for the entity + // GetUID returns namespaced uid 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 diff --git a/pkg/services/authn/authnimpl/sync/user_sync.go b/pkg/services/authn/authnimpl/sync/user_sync.go index 66dacaa85ca..49bf41ab00d 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync.go +++ b/pkg/services/authn/authnimpl/sync/user_sync.go @@ -364,7 +364,7 @@ func (s *UserSync) lookupByOneOf(ctx context.Context, params login.UserLookupPar var err error // If not found, try to find the user by email address - if usr == nil && params.Email != nil && *params.Email != "" { + if params.Email != nil && *params.Email != "" { usr, err = s.userService.GetByEmail(ctx, &user.GetUserByEmailQuery{Email: *params.Email}) if err != nil && !errors.Is(err, user.ErrUserNotFound) { return nil, err @@ -390,6 +390,7 @@ func (s *UserSync) lookupByOneOf(ctx context.Context, params login.UserLookupPar // This is used to update the identity with the latest user information. func syncUserToIdentity(usr *user.User, id *authn.Identity) { id.ID = authn.NewNamespaceID(authn.NamespaceUser, usr.ID) + id.UID = authn.NewNamespaceIDString(authn.NamespaceUser, usr.UID) id.Login = usr.Login id.Email = usr.Email id.Name = usr.Name @@ -399,6 +400,14 @@ func syncUserToIdentity(usr *user.User, id *authn.Identity) { // syncSignedInUserToIdentity syncs a user to an identity. func syncSignedInUserToIdentity(usr *user.SignedInUser, identity *authn.Identity) { + var ns authn.Namespace + if identity.ID.IsNamespace(authn.NamespaceServiceAccount) { + ns = authn.NamespaceServiceAccount + } else { + ns = authn.NamespaceUser + } + identity.UID = authn.NewNamespaceIDString(ns, usr.UserUID) + identity.Name = usr.Name identity.Login = usr.Login identity.Email = usr.Email diff --git a/pkg/services/authn/authnimpl/sync/user_sync_test.go b/pkg/services/authn/authnimpl/sync/user_sync_test.go index d9470adaa8b..eb0e7fafd35 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/user_sync_test.go @@ -47,6 +47,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { userService := &usertest.FakeUserService{ExpectedUser: &user.User{ ID: 1, + UID: "1", Login: "test", Name: "test", Email: "test", @@ -54,6 +55,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { userServiceMod := &usertest.FakeUserService{ExpectedUser: &user.User{ ID: 3, + UID: "3", Login: "test", Name: "test", Email: "test", @@ -63,6 +65,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { userServiceEmailMod := &usertest.FakeUserService{ExpectedUser: &user.User{ ID: 3, + UID: "3", Login: "test", Name: "test", Email: "test@test.com", @@ -76,6 +79,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { CreateFn: func(ctx context.Context, cmd *user.CreateUserCommand) (*user.User, error) { return &user.User{ ID: 2, + UID: "2", Login: cmd.Login, Name: cmd.Name, Email: cmd.Email, @@ -159,6 +163,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { wantErr: false, wantID: &authn.Identity{ ID: authn.MustParseNamespaceID("user:1"), + UID: authn.MustParseNamespaceID("user:1"), Login: "test", Name: "test", Email: "test", @@ -197,6 +202,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { wantErr: false, wantID: &authn.Identity{ ID: authn.MustParseNamespaceID("user:1"), + UID: authn.MustParseNamespaceID("user:1"), Login: "test", Name: "test", Email: "test", @@ -237,6 +243,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { wantErr: false, wantID: &authn.Identity{ ID: authn.MustParseNamespaceID("user:1"), + UID: authn.MustParseNamespaceID("user:1"), AuthID: "2032", AuthenticatedBy: "oauth", Login: "test", @@ -308,6 +315,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { wantErr: false, wantID: &authn.Identity{ ID: authn.MustParseNamespaceID("user:2"), + UID: authn.MustParseNamespaceID("user:2"), Login: "test_create", Name: "test_create", Email: "test_create", @@ -353,6 +361,7 @@ func TestUserSync_SyncUserHook(t *testing.T) { wantErr: false, wantID: &authn.Identity{ ID: authn.MustParseNamespaceID("user:3"), + UID: authn.MustParseNamespaceID("user:3"), Login: "test_mod", Name: "test_mod", Email: "test_mod", @@ -397,8 +406,9 @@ func TestUserSync_SyncUserHook(t *testing.T) { wantErr: false, wantID: &authn.Identity{ ID: authn.MustParseNamespaceID("user:3"), - Login: "test", + UID: authn.MustParseNamespaceID("user:3"), Name: "test", + Login: "test", Email: "test_mod@test.com", IsDisabled: false, EmailVerified: false, diff --git a/pkg/services/authn/clients/ext_jwt.go b/pkg/services/authn/clients/ext_jwt.go index 97ea29fdd8d..84f82fdb26a 100644 --- a/pkg/services/authn/clients/ext_jwt.go +++ b/pkg/services/authn/clients/ext_jwt.go @@ -149,6 +149,7 @@ func (s *ExtendedJWT) authenticateAsService(claims *authlib.Claims[authlib.Acces return &authn.Identity{ ID: id, + UID: id, OrgID: s.getDefaultOrgID(), AuthenticatedBy: login.ExtendedJWTModule, AuthID: claims.Subject, diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index a023aefdfdb..e0210deeaed 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -11,7 +11,6 @@ import ( "github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v3/jwt" - "golang.org/x/oauth2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,7 +18,6 @@ import ( authlib "github.com/grafana/authlib/authn" "github.com/grafana/grafana/pkg/models/roletype" - "github.com/grafana/grafana/pkg/models/usertoken" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/signingkeys" "github.com/grafana/grafana/pkg/services/signingkeys/signingkeystest" @@ -209,20 +207,16 @@ func TestExtendedJWT_Authenticate(t *testing.T) { name: "successful authentication as service", payload: &validPayload, orgID: 1, - want: &authn.Identity{OrgID: 1, OrgName: "", - OrgRoles: map[int64]roletype.RoleType(nil), - ID: authn.MustParseNamespaceID("access-policy:this-uid"), Login: "", Name: "", Email: "", - IsGrafanaAdmin: (*bool)(nil), AuthenticatedBy: "extendedjwt", - AuthID: "access-policy:this-uid", IsDisabled: false, HelpFlags1: 0x0, - LastSeenAt: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), - Teams: []int64(nil), Groups: []string(nil), - OAuthToken: (*oauth2.Token)(nil), SessionToken: (*usertoken.UserToken)(nil), - ClientParams: authn.ClientParams{SyncUser: false, - AllowSignUp: false, EnableUser: false, FetchSyncedUser: false, - SyncTeams: false, SyncOrgRoles: false, CacheAuthProxyKey: "", + want: &authn.Identity{ + ID: authn.MustParseNamespaceID("access-policy:this-uid"), + UID: authn.MustParseNamespaceID("access-policy:this-uid"), + OrgID: 1, + AuthenticatedBy: "extendedjwt", + AuthID: "access-policy:this-uid", + ClientParams: authn.ClientParams{ SyncPermissions: true, - FetchPermissionsParams: authn.FetchPermissionsParams{ActionsLookup: []string(nil), Roles: []string{"fixed:folders:reader"}}}, - Permissions: map[int64]map[string][]string(nil), IDToken: ""}, + FetchPermissionsParams: authn.FetchPermissionsParams{Roles: []string{"fixed:folders:reader"}}}, + }, wantErr: nil, }, { @@ -240,21 +234,19 @@ func TestExtendedJWT_Authenticate(t *testing.T) { Login: "johndoe", } }, - want: &authn.Identity{OrgID: 1, OrgName: "", - OrgRoles: map[int64]roletype.RoleType(nil), ID: authn.MustParseNamespaceID("user:2"), - Login: "", Name: "", Email: "", - IsGrafanaAdmin: (*bool)(nil), AuthenticatedBy: "extendedjwt", - AuthID: "access-policy:this-uid", IsDisabled: false, HelpFlags1: 0x0, - LastSeenAt: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), - Teams: []int64(nil), Groups: []string(nil), - OAuthToken: (*oauth2.Token)(nil), SessionToken: (*usertoken.UserToken)(nil), - ClientParams: authn.ClientParams{SyncUser: false, AllowSignUp: false, - EnableUser: false, FetchSyncedUser: true, SyncTeams: false, - SyncOrgRoles: false, CacheAuthProxyKey: "", + want: &authn.Identity{ + ID: authn.MustParseNamespaceID("user:2"), + OrgID: 1, + AuthenticatedBy: "extendedjwt", + AuthID: "access-policy:this-uid", + ClientParams: authn.ClientParams{ + FetchSyncedUser: true, SyncPermissions: true, - FetchPermissionsParams: authn.FetchPermissionsParams{ActionsLookup: []string{"dashboards:create", - "folders:read", "datasources:explore", "datasources.insights:read"}, - Roles: []string(nil)}}, Permissions: map[int64]map[string][]string(nil), IDToken: ""}, + FetchPermissionsParams: authn.FetchPermissionsParams{ + ActionsLookup: []string{"dashboards:create", "folders:read", "datasources:explore", "datasources.insights:read"}, + }, + }, + }, wantErr: nil, }, { diff --git a/pkg/services/authn/identity.go b/pkg/services/authn/identity.go index 70381f02742..ef63f5f0de1 100644 --- a/pkg/services/authn/identity.go +++ b/pkg/services/authn/identity.go @@ -2,7 +2,6 @@ package authn import ( "fmt" - "strconv" "time" "golang.org/x/oauth2" @@ -22,10 +21,10 @@ var _ Requester = (*Identity)(nil) type Identity struct { // ID is the unique identifier for the entity in the Grafana database. - // It is in the format : where namespace is one of the - // Namespace* constants. For example, "user:1" or "api-key:1". // If the entity is not found in the DB or this entity is non-persistent, this field will be empty. ID NamespaceID + // UID is a unique identifier stored for the entity in Grafana database. Not all entities support uid so it can be empty. + UID NamespaceID // OrgID is the active organization for the entity. OrgID int64 // OrgName is the name of the active organization. @@ -71,8 +70,6 @@ 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 { @@ -84,12 +81,7 @@ func (i *Identity) GetNamespacedID() (namespace identity.Namespace, identifier s } 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 + return i.UID } func (i *Identity) GetAuthID() string { @@ -212,8 +204,6 @@ func (i *Identity) IsNil() bool { // SignedInUser returns a SignedInUser from the identity. func (i *Identity) SignedInUser() *user.SignedInUser { - namespace, id := i.GetNamespacedID() - u := &user.SignedInUser{ OrgID: i.OrgID, OrgName: i.OrgName, @@ -224,7 +214,7 @@ func (i *Identity) SignedInUser() *user.SignedInUser { AuthID: i.AuthID, AuthenticatedBy: i.AuthenticatedBy, IsGrafanaAdmin: i.GetIsGrafanaAdmin(), - IsAnonymous: namespace == NamespaceAnonymous, + IsAnonymous: i.ID.IsNamespace(NamespaceAnonymous), IsDisabled: i.IsDisabled, HelpFlags1: i.HelpFlags1, LastSeenAt: i.LastSeenAt, @@ -234,33 +224,26 @@ func (i *Identity) SignedInUser() *user.SignedInUser { NamespacedID: i.ID, } - if namespace == NamespaceAPIKey { - u.ApiKeyID = intIdentifier(id) + if i.ID.IsNamespace(NamespaceAPIKey) { + id, _ := i.ID.ParseInt() + u.ApiKeyID = id } else { - u.UserID = intIdentifier(id) - u.IsServiceAccount = namespace == NamespaceServiceAccount + id, _ := i.ID.UserID() + u.UserID = id + u.UserUID = i.UID.ID() + u.IsServiceAccount = i.ID.IsNamespace(NamespaceServiceAccount) } return u } -func intIdentifier(identifier string) int64 { - id, err := strconv.ParseInt(identifier, 10, 64) - if err != nil { - // FIXME (kalleep): Improve error handling - return -1 - } - - return id -} - func (i *Identity) ExternalUserInfo() login.ExternalUserInfo { - _, id := i.GetNamespacedID() + id, _ := i.ID.UserID() return login.ExternalUserInfo{ OAuthToken: i.OAuthToken, AuthModule: i.AuthenticatedBy, AuthId: i.AuthID, - UserId: intIdentifier(id), + UserId: id, Email: i.Email, Login: i.Login, Name: i.Name, diff --git a/pkg/services/authn/namespace.go b/pkg/services/authn/namespace.go index 6c4c1220270..48e8717933e 100644 --- a/pkg/services/authn/namespace.go +++ b/pkg/services/authn/namespace.go @@ -15,11 +15,13 @@ const ( var AnonymousNamespaceID = NewNamespaceID(NamespaceAnonymous, 0) +type Namespace = identity.Namespace type NamespaceID = identity.NamespaceID var ( ParseNamespaceID = identity.ParseNamespaceID MustParseNamespaceID = identity.MustParseNamespaceID NewNamespaceID = identity.NewNamespaceID + NewNamespaceIDString = identity.NewNamespaceIDString ErrInvalidNamespaceID = identity.ErrInvalidNamespaceID ) diff --git a/pkg/services/live/features/dashboard.go b/pkg/services/live/features/dashboard.go index e5d178a9895..f6e3b8df26e 100644 --- a/pkg/services/live/features/dashboard.go +++ b/pkg/services/live/features/dashboard.go @@ -37,16 +37,12 @@ type userDisplayDTO struct { // 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 = "" + uid := "" + if requester.GetUID().IsNamespace(identity.NamespaceUser, identity.NamespaceServiceAccount) { + uid = requester.GetUID().ID() } + userID, _ := requester.GetID().UserID() return &userDisplayDTO{ ID: userID, UID: uid, diff --git a/pkg/services/user/identity.go b/pkg/services/user/identity.go index 45f8ed31b5a..5ceee009a78 100644 --- a/pkg/services/user/identity.go +++ b/pkg/services/user/identity.go @@ -192,27 +192,20 @@ func (u *SignedInUser) GetNamespacedID() (identity.Namespace, string) { // 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) + return identity.NewNamespaceIDString(identity.NamespaceAPIKey, strconv.FormatInt(u.ApiKeyID, 10)) case u.IsServiceAccount: - return identity.NamespaceServiceAccount, u.UserUID + return identity.NewNamespaceIDString(identity.NamespaceServiceAccount, u.UserUID) case u.UserID > 0: - return identity.NamespaceUser, u.UserUID + return identity.NewNamespaceIDString(identity.NamespaceUser, u.UserUID) case u.IsAnonymous: - return identity.NamespaceAnonymous, "" + return identity.NewNamespaceIDString(identity.NamespaceAnonymous, "0") case u.AuthenticatedBy == "render" && u.UserID == 0: - return identity.NamespaceRenderService, "" + return identity.NewNamespaceIDString(identity.NamespaceRenderService, "0") } - return identity.NamespaceEmpty, "" + return identity.NewNamespaceIDString(identity.NamespaceEmpty, "0") } func (u *SignedInUser) GetAuthID() string {