mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AuthN: Post login hooks (#61287)
* AuthN: add the ability to register post login hooks * AuthN: add a guard for the user id * AuthN: Add helper to create external user info from identity * AuthN: Pass auth request to password clients * AuthN: set auth module and username in metadata
This commit is contained in:
parent
4b13a5a9ab
commit
3e8857acb8
@ -8,12 +8,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/auth"
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -25,7 +26,12 @@ const (
|
|||||||
ClientSession = "auth.client.session"
|
ClientSession = "auth.client.session"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClientParams are hints to the auth service about how to handle the identity management
|
const (
|
||||||
|
MetaKeyUsername = "username"
|
||||||
|
MetaKeyAuthModule = "authModule"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClientParams are hints to the auth serviAuthN: Post login hooksce about how to handle the identity management
|
||||||
// from the authenticating client.
|
// from the authenticating client.
|
||||||
type ClientParams struct {
|
type ClientParams struct {
|
||||||
// Update the internal representation of the entity from the identity provided
|
// Update the internal representation of the entity from the identity provided
|
||||||
@ -41,14 +47,17 @@ type ClientParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) error
|
type PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) error
|
||||||
|
type PostLoginHookFn func(ctx context.Context, identity *Identity, r *Request, err error)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
// Authenticate authenticates a request using the specified client.
|
// Authenticate authenticates a request using the specified client.
|
||||||
Authenticate(ctx context.Context, client string, r *Request) (*Identity, bool, error)
|
Authenticate(ctx context.Context, client string, r *Request) (*Identity, bool, error)
|
||||||
// Login authenticates a request and creates a session on successful authentication.
|
|
||||||
Login(ctx context.Context, client string, r *Request) (*Identity, error)
|
|
||||||
// RegisterPostAuthHook registers a hook that is called after a successful authentication.
|
// RegisterPostAuthHook registers a hook that is called after a successful authentication.
|
||||||
RegisterPostAuthHook(hook PostAuthHookFn)
|
RegisterPostAuthHook(hook PostAuthHookFn)
|
||||||
|
// Login authenticates a request and creates a session on successful authentication.
|
||||||
|
Login(ctx context.Context, client string, r *Request) (*Identity, error)
|
||||||
|
// RegisterPostLoginHook registers a hook that that is called after a login request.
|
||||||
|
RegisterPostLoginHook(hook PostLoginHookFn)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
@ -59,7 +68,7 @@ type Client interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PasswordClient interface {
|
type PasswordClient interface {
|
||||||
AuthenticatePassword(ctx context.Context, orgID int64, username, password string) (*Identity, error)
|
AuthenticatePassword(ctx context.Context, r *Request, username, password string) (*Identity, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
@ -71,6 +80,23 @@ type Request struct {
|
|||||||
// Resp is the response writer to use for the request
|
// Resp is the response writer to use for the request
|
||||||
// Used to set cookies and headers
|
// Used to set cookies and headers
|
||||||
Resp web.ResponseWriter
|
Resp web.ResponseWriter
|
||||||
|
|
||||||
|
// metadata is additional information about the auth request
|
||||||
|
metadata map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) SetMeta(k, v string) {
|
||||||
|
if r.metadata == nil {
|
||||||
|
r.metadata = map[string]string{}
|
||||||
|
}
|
||||||
|
r.metadata[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) GetMeta(k string) string {
|
||||||
|
if r.metadata == nil {
|
||||||
|
r.metadata = map[string]string{}
|
||||||
|
}
|
||||||
|
return r.metadata[k]
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -201,6 +227,23 @@ func (i *Identity) SignedInUser() *user.SignedInUser {
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Identity) ExternalUserInfo() models.ExternalUserInfo {
|
||||||
|
_, id := i.NamespacedID()
|
||||||
|
return models.ExternalUserInfo{
|
||||||
|
OAuthToken: i.OAuthToken,
|
||||||
|
AuthModule: i.AuthModule,
|
||||||
|
AuthId: i.AuthID,
|
||||||
|
UserId: id,
|
||||||
|
Email: i.Email,
|
||||||
|
Login: i.Login,
|
||||||
|
Name: i.Name,
|
||||||
|
Groups: i.Groups,
|
||||||
|
OrgRoles: i.OrgRoles,
|
||||||
|
IsGrafanaAdmin: i.IsGrafanaAdmin,
|
||||||
|
IsDisabled: i.IsDisabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// IdentityFromSignedInUser creates an identity from a SignedInUser.
|
// IdentityFromSignedInUser creates an identity from a SignedInUser.
|
||||||
func IdentityFromSignedInUser(id string, usr *user.SignedInUser, params ClientParams) *Identity {
|
func IdentityFromSignedInUser(id string, usr *user.SignedInUser, params ClientParams) *Identity {
|
||||||
return &Identity{
|
return &Identity{
|
||||||
|
@ -96,6 +96,8 @@ type Service struct {
|
|||||||
|
|
||||||
// postAuthHooks are called after a successful authentication. They can modify the identity.
|
// postAuthHooks are called after a successful authentication. They can modify the identity.
|
||||||
postAuthHooks []authn.PostAuthHookFn
|
postAuthHooks []authn.PostAuthHookFn
|
||||||
|
// postLoginHooks are called after a login request is performed, both for failing and successful requests.
|
||||||
|
postLoginHooks []authn.PostLoginHookFn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Request) (*authn.Identity, bool, error) {
|
func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Request) (*authn.Identity, bool, error) {
|
||||||
@ -130,12 +132,23 @@ func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Requ
|
|||||||
return identity, true, nil
|
return identity, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (*authn.Identity, error) {
|
func (s *Service) RegisterPostAuthHook(hook authn.PostAuthHookFn) {
|
||||||
identity, ok, err := s.Authenticate(ctx, client, r)
|
s.postAuthHooks = append(s.postAuthHooks, hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (identity *authn.Identity, err error) {
|
||||||
|
var ok bool
|
||||||
|
identity, ok, err = s.Authenticate(ctx, client, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, authn.ErrClientNotConfigured.Errorf("client not configured: %s", client)
|
return nil, authn.ErrClientNotConfigured.Errorf("client not configured: %s", client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
for _, hook := range s.postLoginHooks {
|
||||||
|
hook(ctx, identity, r, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -143,7 +156,7 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (*
|
|||||||
namespace, id := identity.NamespacedID()
|
namespace, id := identity.NamespacedID()
|
||||||
|
|
||||||
// Login is only supported for users
|
// Login is only supported for users
|
||||||
if namespace != authn.NamespaceUser {
|
if namespace != authn.NamespaceUser || id <= 0 {
|
||||||
return nil, authn.ErrUnsupportedIdentity.Errorf("expected identity of type user but got: %s", namespace)
|
return nil, authn.ErrUnsupportedIdentity.Errorf("expected identity of type user but got: %s", namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,14 +171,12 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (*
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: add login hooks to replace the one used in HookService
|
|
||||||
|
|
||||||
identity.SessionToken = sessionToken
|
identity.SessionToken = sessionToken
|
||||||
return identity, nil
|
return identity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) RegisterPostAuthHook(hook authn.PostAuthHookFn) {
|
func (s *Service) RegisterPostLoginHook(hook authn.PostLoginHookFn) {
|
||||||
s.postAuthHooks = append(s.postAuthHooks, hook)
|
s.postLoginHooks = append(s.postLoginHooks, hook)
|
||||||
}
|
}
|
||||||
|
|
||||||
func orgIDFromRequest(r *authn.Request) int64 {
|
func orgIDFromRequest(r *authn.Request) int64 {
|
||||||
|
@ -33,6 +33,6 @@ type FakePasswordClient struct {
|
|||||||
ExpectedIdentity *authn.Identity
|
ExpectedIdentity *authn.Identity
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f FakePasswordClient) AuthenticatePassword(ctx context.Context, orgID int64, username, password string) (*authn.Identity, error) {
|
func (f FakePasswordClient) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) {
|
||||||
return f.ExpectedIdentity, f.ExpectedErr
|
return f.ExpectedIdentity, f.ExpectedErr
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,8 @@ func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
|
|||||||
return nil, errDecodingBasicAuthHeader.Errorf("failed to decode basic auth header: %w", err)
|
return nil, errDecodingBasicAuthHeader.Errorf("failed to decode basic auth header: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.SetMeta(authn.MetaKeyUsername, username)
|
||||||
|
|
||||||
ok, err := c.loginAttempts.Validate(ctx, username)
|
ok, err := c.loginAttempts.Validate(ctx, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -47,7 +49,7 @@ func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, pwClient := range c.clients {
|
for _, pwClient := range c.clients {
|
||||||
identity, err := pwClient.AuthenticatePassword(ctx, r.OrgID, username, password)
|
identity, err := pwClient.AuthenticatePassword(ctx, r, username, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errIdentityNotFound) {
|
if errors.Is(err, errIdentityNotFound) {
|
||||||
// continue to next password client if identity could not be found
|
// continue to next password client if identity could not be found
|
||||||
|
@ -20,7 +20,7 @@ type Grafana struct {
|
|||||||
userService user.Service
|
userService user.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Grafana) AuthenticatePassword(ctx context.Context, orgID int64, username, password string) (*authn.Identity, error) {
|
func (c Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) {
|
||||||
usr, err := c.userService.GetByLogin(ctx, &user.GetUserByLoginQuery{LoginOrEmail: username})
|
usr, err := c.userService.GetByLogin(ctx, &user.GetUserByLoginQuery{LoginOrEmail: username})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, user.ErrUserNotFound) {
|
if errors.Is(err, user.ErrUserNotFound) {
|
||||||
@ -29,11 +29,14 @@ func (c Grafana) AuthenticatePassword(ctx context.Context, orgID int64, username
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// user was found so set auth module in req metadata
|
||||||
|
r.SetMeta(authn.MetaKeyAuthModule, "grafana")
|
||||||
|
|
||||||
if ok := comparePassword(password, usr.Salt, usr.Password); !ok {
|
if ok := comparePassword(password, usr.Salt, usr.Password); !ok {
|
||||||
return nil, errInvalidPassword.Errorf("invalid password")
|
return nil, errInvalidPassword.Errorf("invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
signedInUser, err := c.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{OrgID: orgID, UserID: usr.ID})
|
signedInUser, err := c.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{OrgID: r.OrgID, UserID: usr.ID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ func TestGrafana_AuthenticatePassword(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c := ProvideGrafana(userService)
|
c := ProvideGrafana(userService)
|
||||||
identity, err := c.AuthenticatePassword(context.Background(), 1, tt.username, tt.password)
|
identity, err := c.AuthenticatePassword(context.Background(), &authn.Request{OrgID: 1}, tt.username, tt.password)
|
||||||
assert.ErrorIs(t, err, tt.expectedErr)
|
assert.ErrorIs(t, err, tt.expectedErr)
|
||||||
assert.EqualValues(t, tt.expectedIdentity, identity)
|
assert.EqualValues(t, tt.expectedIdentity, identity)
|
||||||
})
|
})
|
||||||
|
@ -21,26 +21,30 @@ type LDAP struct {
|
|||||||
service ldapService
|
service ldapService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LDAP) AuthenticatePassword(ctx context.Context, orgID int64, username, password string) (*authn.Identity, error) {
|
func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) {
|
||||||
info, err := c.service.Login(&models.LoginUserQuery{
|
info, err := c.service.Login(&models.LoginUserQuery{
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: password,
|
Password: password,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if errors.Is(err, multildap.ErrCouldNotFindUser) {
|
||||||
if errors.Is(err, multildap.ErrInvalidCredentials) {
|
return nil, errIdentityNotFound.Errorf("no user found: %w", err)
|
||||||
return nil, errInvalidPassword.Errorf("invalid password: %w", err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// user was found so set auth module in req metadata
|
||||||
|
r.SetMeta(authn.MetaKeyAuthModule, "ldap")
|
||||||
|
|
||||||
|
if errors.Is(err, multildap.ErrInvalidCredentials) {
|
||||||
// FIXME: disable user in grafana if not found
|
// FIXME: disable user in grafana if not found
|
||||||
if errors.Is(err, multildap.ErrCouldNotFindUser) {
|
return nil, errInvalidPassword.Errorf("invalid password: %w", err)
|
||||||
return nil, errIdentityNotFound.Errorf("no user found: %w", err)
|
}
|
||||||
}
|
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &authn.Identity{
|
return &authn.Identity{
|
||||||
OrgID: orgID,
|
OrgID: r.OrgID,
|
||||||
OrgRoles: info.OrgRoles,
|
OrgRoles: info.OrgRoles,
|
||||||
Login: info.Login,
|
Login: info.Login,
|
||||||
Name: info.Name,
|
Name: info.Name,
|
||||||
|
@ -79,7 +79,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) {
|
|||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
c := &LDAP{cfg: setting.NewCfg(), service: fakeLDAPService{ExpectedInfo: tt.expectedInfo, ExpectedErr: tt.expectedLDAPErr}}
|
c := &LDAP{cfg: setting.NewCfg(), service: fakeLDAPService{ExpectedInfo: tt.expectedInfo, ExpectedErr: tt.expectedLDAPErr}}
|
||||||
|
|
||||||
identity, err := c.AuthenticatePassword(context.Background(), 1, tt.username, tt.password)
|
identity, err := c.AuthenticatePassword(context.Background(), &authn.Request{OrgID: 1}, tt.username, tt.password)
|
||||||
assert.ErrorIs(t, err, tt.expectedErr)
|
assert.ErrorIs(t, err, tt.expectedErr)
|
||||||
assert.EqualValues(t, tt.expectedIdentity, identity)
|
assert.EqualValues(t, tt.expectedIdentity, identity)
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user