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"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -25,7 +26,12 @@ const (
|
||||
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.
|
||||
type ClientParams struct {
|
||||
// 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 PostLoginHookFn func(ctx context.Context, identity *Identity, r *Request, err error)
|
||||
|
||||
type Service interface {
|
||||
// Authenticate authenticates a request using the specified client.
|
||||
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(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 {
|
||||
@ -59,7 +68,7 @@ type Client 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 {
|
||||
@ -71,6 +80,23 @@ type Request struct {
|
||||
// Resp is the response writer to use for the request
|
||||
// Used to set cookies and headers
|
||||
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 (
|
||||
@ -201,6 +227,23 @@ func (i *Identity) SignedInUser() *user.SignedInUser {
|
||||
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.
|
||||
func IdentityFromSignedInUser(id string, usr *user.SignedInUser, params ClientParams) *Identity {
|
||||
return &Identity{
|
||||
|
@ -96,6 +96,8 @@ type Service struct {
|
||||
|
||||
// postAuthHooks are called after a successful authentication. They can modify the identity.
|
||||
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) {
|
||||
@ -130,12 +132,23 @@ func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Requ
|
||||
return identity, true, nil
|
||||
}
|
||||
|
||||
func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (*authn.Identity, error) {
|
||||
identity, ok, err := s.Authenticate(ctx, client, r)
|
||||
func (s *Service) RegisterPostAuthHook(hook authn.PostAuthHookFn) {
|
||||
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 {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@ -143,7 +156,7 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (*
|
||||
namespace, id := identity.NamespacedID()
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@ -158,14 +171,12 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (*
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// FIXME: add login hooks to replace the one used in HookService
|
||||
|
||||
identity.SessionToken = sessionToken
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
func (s *Service) RegisterPostAuthHook(hook authn.PostAuthHookFn) {
|
||||
s.postAuthHooks = append(s.postAuthHooks, hook)
|
||||
func (s *Service) RegisterPostLoginHook(hook authn.PostLoginHookFn) {
|
||||
s.postLoginHooks = append(s.postLoginHooks, hook)
|
||||
}
|
||||
|
||||
func orgIDFromRequest(r *authn.Request) int64 {
|
||||
|
@ -33,6 +33,6 @@ type FakePasswordClient struct {
|
||||
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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
r.SetMeta(authn.MetaKeyUsername, username)
|
||||
|
||||
ok, err := c.loginAttempts.Validate(ctx, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -47,7 +49,7 @@ func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
|
||||
}
|
||||
|
||||
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 errors.Is(err, errIdentityNotFound) {
|
||||
// continue to next password client if identity could not be found
|
||||
|
@ -20,7 +20,7 @@ type Grafana struct {
|
||||
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})
|
||||
if err != nil {
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
@ -29,11 +29,14 @@ func (c Grafana) AuthenticatePassword(ctx context.Context, orgID int64, username
|
||||
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 {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ func TestGrafana_AuthenticatePassword(t *testing.T) {
|
||||
}
|
||||
|
||||
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.EqualValues(t, tt.expectedIdentity, identity)
|
||||
})
|
||||
|
@ -21,26 +21,30 @@ type LDAP struct {
|
||||
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{
|
||||
Username: username,
|
||||
Password: password,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, multildap.ErrInvalidCredentials) {
|
||||
return nil, errInvalidPassword.Errorf("invalid password: %w", err)
|
||||
}
|
||||
if errors.Is(err, multildap.ErrCouldNotFindUser) {
|
||||
return nil, errIdentityNotFound.Errorf("no user found: %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
|
||||
if errors.Is(err, multildap.ErrCouldNotFindUser) {
|
||||
return nil, errIdentityNotFound.Errorf("no user found: %w", err)
|
||||
}
|
||||
return nil, errInvalidPassword.Errorf("invalid password: %w", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &authn.Identity{
|
||||
OrgID: orgID,
|
||||
OrgID: r.OrgID,
|
||||
OrgRoles: info.OrgRoles,
|
||||
Login: info.Login,
|
||||
Name: info.Name,
|
||||
|
@ -79,7 +79,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
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.EqualValues(t, tt.expectedIdentity, identity)
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user