From 2de72c1c39c7a39b1fde8bb08bd65bb54b990ecb Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Tue, 10 Jan 2023 14:55:27 +0100 Subject: [PATCH] AuthN: Login (#61225) * AuthN: Add function to login auth request --- pkg/services/authn/authn.go | 6 +- pkg/services/authn/authnimpl/service.go | 55 +++++++++++++-- pkg/services/authn/authnimpl/service_test.go | 74 ++++++++++++++++++++ pkg/services/authn/clients/basic.go | 3 +- pkg/services/authn/error.go | 5 +- 5 files changed, 132 insertions(+), 11 deletions(-) diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index 29d6637ba2a..7b497c9d024 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -34,10 +34,12 @@ type ClientParams struct { type PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) error type Service interface { - // RegisterPostAuthHook registers a hook that is called after a successful authentication. - RegisterPostAuthHook(hook PostAuthHookFn) // 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) } type Client interface { diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 89b3cc6a46d..b708d89f349 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -5,7 +5,10 @@ import ( "net/http" "strconv" + "go.opentelemetry.io/otel/attribute" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/network" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey" @@ -20,7 +23,7 @@ import ( "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "go.opentelemetry.io/otel/attribute" + "github.com/grafana/grafana/pkg/web" ) // make sure service implements authn.Service interface @@ -35,11 +38,12 @@ func ProvideService( authInfoService login.AuthInfoService, renderService rendering.Service, ) *Service { s := &Service{ - log: log.New("authn.service"), - cfg: cfg, - clients: make(map[string]authn.Client), - tracer: tracer, - postAuthHooks: []authn.PostAuthHookFn{}, + log: log.New("authn.service"), + cfg: cfg, + clients: make(map[string]authn.Client), + tracer: tracer, + sessionService: sessionService, + postAuthHooks: []authn.PostAuthHookFn{}, } s.clients[authn.ClientRender] = clients.ProvideRender(userService, renderService) @@ -81,9 +85,12 @@ type Service struct { log log.Logger cfg *setting.Cfg clients map[string]authn.Client + + tracer tracing.Tracer + sessionService auth.UserTokenService + // postAuthHooks are called after a successful authentication. They can modify the identity. postAuthHooks []authn.PostAuthHookFn - tracer tracing.Tracer } func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Request) (*authn.Identity, bool, error) { @@ -117,6 +124,40 @@ 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) + if !ok { + return nil, authn.ErrClientNotConfigured.Errorf("client not configured: %s", client) + } + + if err != nil { + return nil, err + } + + namespace, id := identity.NamespacedID() + + // Login is only supported for users + if namespace != authn.NamespaceUser { + return nil, authn.ErrUnsupportedIdentity.Errorf("expected identity of type user but got: %s", namespace) + } + + addr := web.RemoteAddr(r.HTTPRequest) + ip, err := network.GetIPFromAddress(addr) + if err != nil { + s.log.Debug("failed to parse ip from address", "addr", addr) + } + + sessionToken, err := s.sessionService.CreateToken(ctx, &user.User{ID: id}, ip, r.HTTPRequest.UserAgent()) + if err != nil { + 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) } diff --git a/pkg/services/authn/authnimpl/service_test.go b/pkg/services/authn/authnimpl/service_test.go index b12d49d739d..51d84481374 100644 --- a/pkg/services/authn/authnimpl/service_test.go +++ b/pkg/services/authn/authnimpl/service_test.go @@ -3,10 +3,14 @@ package authnimpl import ( "context" "errors" + "net" "net/http" "net/url" "testing" + "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtest" + "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/assert" "github.com/grafana/grafana/pkg/infra/log" @@ -124,6 +128,76 @@ func TestService_AuthenticateOrgID(t *testing.T) { } } +func TestService_Login(t *testing.T) { + type TestCase struct { + desc string + client string + + expectedClientOK bool + expectedClientErr error + expectedClientIdentity *authn.Identity + + expectedSessionErr error + + expectedErr error + expectedIdentity *authn.Identity + } + + tests := []TestCase{ + { + desc: "should authenticate and create session for valid request", + client: "fake", + expectedClientOK: true, + expectedClientIdentity: &authn.Identity{ + ID: "user:1", + }, + expectedIdentity: &authn.Identity{ + ID: "user:1", + SessionToken: &auth.UserToken{UserId: 1}, + }, + }, + { + desc: "should not authenticate with invalid client", + client: "invalid", + expectedErr: authn.ErrClientNotConfigured, + }, + { + desc: "should not authenticate non user identity", + client: "fake", + expectedClientOK: true, + expectedClientIdentity: &authn.Identity{ID: "apikey:1"}, + expectedErr: authn.ErrUnsupportedIdentity, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + s := setupTests(t, func(svc *Service) { + svc.clients["fake"] = &authntest.FakeClient{ + ExpectedErr: tt.expectedClientErr, + ExpectedTest: tt.expectedClientOK, + ExpectedIdentity: tt.expectedClientIdentity, + } + svc.sessionService = &authtest.FakeUserAuthTokenService{ + CreateTokenProvider: func(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) { + if tt.expectedSessionErr != nil { + return nil, tt.expectedSessionErr + } + return &auth.UserToken{UserId: user.ID}, nil + }, + } + }) + + identity, err := s.Login(context.Background(), tt.client, &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{}, + URL: &url.URL{}, + }}) + assert.ErrorIs(t, err, tt.expectedErr) + assert.EqualValues(t, tt.expectedIdentity, identity) + }) + } +} + func mustParseURL(s string) *url.URL { u, err := url.Parse(s) if err != nil { diff --git a/pkg/services/authn/clients/basic.go b/pkg/services/authn/clients/basic.go index 71279db4c8d..4c043b07311 100644 --- a/pkg/services/authn/clients/basic.go +++ b/pkg/services/authn/clients/basic.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/services/loginattempt" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" + "github.com/grafana/grafana/pkg/web" ) var ( @@ -54,7 +55,7 @@ func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden } if errors.Is(err, errInvalidPassword) { // only add login attempt if identity was found but the provided password was invalid - _ = c.loginAttempts.Add(ctx, username, r.HTTPRequest.RemoteAddr) + _ = c.loginAttempts.Add(ctx, username, web.RemoteAddr(r.HTTPRequest)) } return nil, errBasicAuthCredentials.Errorf("failed to authenticate identity: %w", err) } diff --git a/pkg/services/authn/error.go b/pkg/services/authn/error.go index 70258baadbb..c1c18cec460 100644 --- a/pkg/services/authn/error.go +++ b/pkg/services/authn/error.go @@ -2,4 +2,7 @@ package authn import "github.com/grafana/grafana/pkg/util/errutil" -var ErrClientNotFound = errutil.NewBase(errutil.StatusNotFound, "auth.client.notConfigured") +var ( + ErrClientNotConfigured = errutil.NewBase(errutil.StatusBadRequest, "auth.client.notConfigured") + ErrUnsupportedIdentity = errutil.NewBase(errutil.StatusNotImplemented, "auth.identity.unsupported") +)