From 3e8857acb85278b7b638434d13e8d45828db174e Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Thu, 12 Jan 2023 15:02:04 +0100 Subject: [PATCH] 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 --- pkg/services/authn/authn.go | 53 ++++++++++++++++++++-- pkg/services/authn/authnimpl/service.go | 25 +++++++--- pkg/services/authn/authntest/fake.go | 2 +- pkg/services/authn/clients/basic.go | 4 +- pkg/services/authn/clients/grafana.go | 7 ++- pkg/services/authn/clients/grafana_test.go | 2 +- pkg/services/authn/clients/ldap.go | 22 +++++---- pkg/services/authn/clients/ldap_test.go | 2 +- 8 files changed, 90 insertions(+), 27 deletions(-) diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index 20c3749c50f..a6394b77308 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -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{ diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 8d707623fb6..3718375b3c4 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -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 { diff --git a/pkg/services/authn/authntest/fake.go b/pkg/services/authn/authntest/fake.go index 6b5f2021577..e34dd38fafe 100644 --- a/pkg/services/authn/authntest/fake.go +++ b/pkg/services/authn/authntest/fake.go @@ -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 } diff --git a/pkg/services/authn/clients/basic.go b/pkg/services/authn/clients/basic.go index 4c043b07311..e3fcf608908 100644 --- a/pkg/services/authn/clients/basic.go +++ b/pkg/services/authn/clients/basic.go @@ -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 diff --git a/pkg/services/authn/clients/grafana.go b/pkg/services/authn/clients/grafana.go index 55032194f59..a14be60f163 100644 --- a/pkg/services/authn/clients/grafana.go +++ b/pkg/services/authn/clients/grafana.go @@ -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 } diff --git a/pkg/services/authn/clients/grafana_test.go b/pkg/services/authn/clients/grafana_test.go index bea7c87c509..4d17bfa8795 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -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) }) diff --git a/pkg/services/authn/clients/ldap.go b/pkg/services/authn/clients/ldap.go index 8c2ba538a8d..364be4bb65b 100644 --- a/pkg/services/authn/clients/ldap.go +++ b/pkg/services/authn/clients/ldap.go @@ -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, diff --git a/pkg/services/authn/clients/ldap_test.go b/pkg/services/authn/clients/ldap_test.go index 305d5d407c8..91971225519 100644 --- a/pkg/services/authn/clients/ldap_test.go +++ b/pkg/services/authn/clients/ldap_test.go @@ -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) })