From 50608db59afd2401d2bf5947b3c85c72e284a257 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Mon, 23 Jan 2023 11:54:38 +0100 Subject: [PATCH] AuthN: Add interface and function to operate on clients that supports redirects (#61905) --- pkg/services/authn/authn.go | 7 ++++ pkg/services/authn/authnimpl/service.go | 22 ++++++++++ pkg/services/authn/authnimpl/service_test.go | 43 ++++++++++++++++++++ pkg/services/authn/authntest/fake.go | 21 ++++++++++ pkg/services/authn/error.go | 1 + 5 files changed, 94 insertions(+) diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index ce6e9f20a47..4ebfd517ba3 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -60,6 +60,8 @@ type Service interface { 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) + // RedirectURL will generate url that we can use to initiate auth flow for supported clients. + RedirectURL(ctx context.Context, client string, r *Request) (string, error) } type Client interface { @@ -69,6 +71,11 @@ type Client interface { Test(ctx context.Context, r *Request) bool } +type RedirectClient interface { + Client + RedirectURL(ctx context.Context, r *Request) (string, error) +} + type PasswordClient interface { AuthenticatePassword(ctx context.Context, r *Request, username, password string) (*Identity, error) } diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 1f65c50c65e..937624a1f16 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -29,6 +29,10 @@ import ( "github.com/grafana/grafana/pkg/web" ) +const ( + attributeKeyClient = "authn.client" +) + var ( errDisabledIdentity = errutil.NewBase(errutil.StatusUnauthorized, "identity.disabled") ) @@ -220,6 +224,24 @@ func (s *Service) RegisterPostLoginHook(hook authn.PostLoginHookFn) { s.postLoginHooks = append(s.postLoginHooks, hook) } +func (s *Service) RedirectURL(ctx context.Context, client string, r *authn.Request) (string, error) { + ctx, span := s.tracer.Start(ctx, "authn.RedirectURL") + defer span.End() + span.SetAttributes(attributeKeyClient, client, attribute.Key(attributeKeyClient).String(client)) + + c, ok := s.clients[client] + if !ok { + return "", authn.ErrClientNotConfigured.Errorf("client not configured: %s", client) + } + + redirectClient, ok := c.(authn.RedirectClient) + if !ok { + return "", authn.ErrUnsupportedClient.Errorf("client does not support generating redirect url: %s", client) + } + + return redirectClient.RedirectURL(ctx, r) +} + func orgIDFromRequest(r *authn.Request) int64 { if r.HTTPRequest == nil { return 0 diff --git a/pkg/services/authn/authnimpl/service_test.go b/pkg/services/authn/authnimpl/service_test.go index af7d04e9c37..20c95c40b78 100644 --- a/pkg/services/authn/authnimpl/service_test.go +++ b/pkg/services/authn/authnimpl/service_test.go @@ -210,6 +210,49 @@ func TestService_Login(t *testing.T) { } } +func TestService_RedirectURL(t *testing.T) { + type testCase struct { + desc string + client string + expectedURL string + expectedErr error + } + + tests := []testCase{ + { + desc: "should generate url for valid redirect client", + client: "redirect", + expectedURL: "https://localhost/redirect", + expectedErr: nil, + }, + { + desc: "should return error on non existing client", + client: "non-existing", + expectedErr: authn.ErrClientNotConfigured, + }, + { + desc: "should return error when client don't support the redirect interface", + client: "non-redirect", + expectedErr: authn.ErrUnsupportedClient, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + service := setupTests(t, func(svc *Service) { + svc.clients["redirect"] = authntest.FakeRedirectClient{ + ExpectedURL: tt.expectedURL, + } + svc.clients["non-redirect"] = &authntest.FakeClient{} + }) + + u, err := service.RedirectURL(context.Background(), tt.client, nil) + assert.ErrorIs(t, err, tt.expectedErr) + assert.Equal(t, tt.expectedURL, u) + }) + } +} + func mustParseURL(s string) *url.URL { u, err := url.Parse(s) if err != nil { diff --git a/pkg/services/authn/authntest/fake.go b/pkg/services/authn/authntest/fake.go index e34dd38fafe..7f64b016294 100644 --- a/pkg/services/authn/authntest/fake.go +++ b/pkg/services/authn/authntest/fake.go @@ -36,3 +36,24 @@ type FakePasswordClient struct { func (f FakePasswordClient) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { return f.ExpectedIdentity, f.ExpectedErr } + +var _ authn.RedirectClient = new(FakeRedirectClient) + +type FakeRedirectClient struct { + ExpectedErr error + ExpectedURL string + ExpectedOK bool + ExpectedIdentity *authn.Identity +} + +func (f FakeRedirectClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + return f.ExpectedIdentity, f.ExpectedErr +} + +func (f FakeRedirectClient) Test(ctx context.Context, r *authn.Request) bool { + return f.ExpectedOK +} + +func (f FakeRedirectClient) RedirectURL(ctx context.Context, r *authn.Request) (string, error) { + return f.ExpectedURL, f.ExpectedErr +} diff --git a/pkg/services/authn/error.go b/pkg/services/authn/error.go index c1c18cec460..54fb5e27321 100644 --- a/pkg/services/authn/error.go +++ b/pkg/services/authn/error.go @@ -3,6 +3,7 @@ package authn import "github.com/grafana/grafana/pkg/util/errutil" var ( + ErrUnsupportedClient = errutil.NewBase(errutil.StatusBadRequest, "auth.client.unsupported") ErrClientNotConfigured = errutil.NewBase(errutil.StatusBadRequest, "auth.client.notConfigured") ErrUnsupportedIdentity = errutil.NewBase(errutil.StatusNotImplemented, "auth.identity.unsupported") )