AuthN: Set uid during authentication (#87797)

* Identity: Remove GetNamespacedUID and use GetUID instead

* Authn: Set uid for users and service accounts
This commit is contained in:
Karl Persson
2024-05-14 14:13:50 +02:00
committed by GitHub
parent 6836bfe1ea
commit 9977258d04
9 changed files with 70 additions and 86 deletions
+2 -4
View File
@@ -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
+10 -1
View File
@@ -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
@@ -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,
+1
View File
@@ -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,
+21 -29
View File
@@ -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,
},
{
+13 -30
View File
@@ -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 <namespace>:<id> 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,
+2
View File
@@ -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
)
+4 -8
View File
@@ -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,
+6 -13
View File
@@ -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 {