From 22be025284445f7d4e9b1df15f33f38416e75daf Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Fri, 2 Dec 2022 15:10:03 +0100 Subject: [PATCH] Auth: Add anonymous authn client (#59637) * Authn: Add Client interface and Reqeust and Identity structures * Authn: Implement Authenticate method in service * Authn: Add tracing * Authn: Add logger * AuthN: Implement Anonymous client --- pkg/api/common_test.go | 3 +- pkg/middleware/middleware_test.go | 3 +- pkg/server/wire.go | 4 ++ pkg/services/authn/authn.go | 41 ++++++++++++ pkg/services/authn/authnimpl/service.go | 58 +++++++++++++++- pkg/services/authn/authnimpl/service_test.go | 62 +++++++++++++++++ pkg/services/authn/authntest/fake.go | 17 ++++- pkg/services/authn/clients/anonymous.go | 41 ++++++++++++ pkg/services/authn/clients/anonymous_test.go | 66 +++++++++++++++++++ pkg/services/authn/error.go | 5 ++ .../contexthandler/auth_proxy_test.go | 3 +- pkg/services/contexthandler/contexthandler.go | 20 +++++- 12 files changed, 314 insertions(+), 9 deletions(-) create mode 100644 pkg/services/authn/authnimpl/service_test.go create mode 100644 pkg/services/authn/clients/anonymous.go create mode 100644 pkg/services/authn/clients/anonymous_test.go create mode 100644 pkg/services/authn/error.go diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 3f07d38d494..b9497b4d16e 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -30,6 +30,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" "github.com/grafana/grafana/pkg/services/auth/authtest" + "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" @@ -217,7 +218,7 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHa authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginservice.LoginServiceMock{}, &usertest.FakeUserService{}, sqlStore) loginService := &logintest.LoginServiceFake{} authenticator := &logintest.AuthenticatorFake{} - ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator, usertest.NewUserServiceFake(), orgtest.NewOrgServiceFake(), nil, featuremgmt.WithFeatures()) + ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator, usertest.NewUserServiceFake(), orgtest.NewOrgServiceFake(), nil, featuremgmt.WithFeatures(), &authntest.FakeService{}) return ctxHdlr } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 1c45065c374..173c295b45d 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -29,6 +29,7 @@ import ( "github.com/grafana/grafana/pkg/services/apikey/apikeytest" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" + "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -887,7 +888,7 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg, mockSQLStore *dbtest.Fake tracer := tracing.InitializeTracerForTest() authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginService, userService, mockSQLStore) authenticator := &logintest.AuthenticatorFake{ExpectedUser: &user.User{}} - return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, mockSQLStore, tracer, authProxy, loginService, apiKeyService, authenticator, userService, orgService, oauthTokenService, featuremgmt.WithFeatures(featuremgmt.FlagAccessTokenExpirationCheck)) + return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, mockSQLStore, tracer, authProxy, loginService, apiKeyService, authenticator, userService, orgService, oauthTokenService, featuremgmt.WithFeatures(featuremgmt.FlagAccessTokenExpirationCheck), &authntest.FakeService{}) } type fakeRenderService struct { diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 5a0bc6f3c72..b2fde668d5e 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -38,6 +38,8 @@ import ( "github.com/grafana/grafana/pkg/services/annotations/annotationsimpl" "github.com/grafana/grafana/pkg/services/apikey/apikeyimpl" "github.com/grafana/grafana/pkg/services/auth/jwt" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/authnimpl" "github.com/grafana/grafana/pkg/services/cleanup" "github.com/grafana/grafana/pkg/services/comments" "github.com/grafana/grafana/pkg/services/contexthandler" @@ -348,6 +350,8 @@ var wireBasicSet = wire.NewSet( wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), + authnimpl.ProvideService, + wire.Bind(new(authn.Service), new(*authnimpl.Service)), ) var wireSet = wire.NewSet( diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index ac9b9b64a63..22e8f491a5e 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -1,4 +1,45 @@ package authn +import ( + "context" + "net/http" + + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" +) + +const ( + ClientAnonymous = "auth.anonymous" +) + type Service interface { + Authenticate(ctx context.Context, client string, r *Request) (*Identity, error) +} + +type Client interface { + Authenticate(ctx context.Context, r *Request) (*Identity, error) +} + +type Request struct { + HTTPRequest *http.Request +} + +type Identity struct { + OrgID int64 + OrgName string + IsAnonymous bool + OrgRoles map[int64]org.RoleType +} + +func (i *Identity) Role() org.RoleType { + return i.OrgRoles[i.OrgID] +} + +func (i *Identity) SignedInUser() *user.SignedInUser { + return &user.SignedInUser{ + OrgID: i.OrgID, + OrgName: i.OrgName, + OrgRole: i.Role(), + IsAnonymous: i.IsAnonymous, + } } diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index cb6e846aad8..5ce6cc91b14 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -1,8 +1,62 @@ package authnimpl -import "github.com/grafana/grafana/pkg/services/authn" +import ( + "context" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/clients" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/setting" + "go.opentelemetry.io/otel/attribute" +) var _ authn.Service = new(Service) -type Service struct { +func ProvideService(cfg *setting.Cfg, tracer tracing.Tracer, orgService org.Service) *Service { + s := &Service{ + log: log.New("authn.service"), + cfg: cfg, + clients: make(map[string]authn.Client), + tracer: tracer, + } + + if s.cfg.AnonymousEnabled { + s.clients[authn.ClientAnonymous] = clients.ProvideAnonymous(cfg, orgService) + } + + return s +} + +type Service struct { + log log.Logger + cfg *setting.Cfg + clients map[string]authn.Client + + tracer tracing.Tracer +} + +func (s *Service) Authenticate(ctx context.Context, clientName string, r *authn.Request) (*authn.Identity, error) { + ctx, span := s.tracer.Start(ctx, "authn.Authenticate") + defer span.End() + + span.SetAttributes("authn.client", clientName, attribute.Key("authn.client").String(clientName)) + + client, ok := s.clients[clientName] + if !ok { + s.log.FromContext(ctx).Warn("auth client not found", "client", clientName) + span.AddEvents([]string{"message"}, []tracing.EventValue{{Str: "auth client is not configured"}}) + return nil, authn.ErrClientNotFound + } + + // FIXME: We want to perform common authentication operations here. + // We will add them as we start to implement clients that requires them. + // Those operations can be Syncing user, syncing teams, create a session etc. + // We would need to check what operations a client support and also if they are requested + // because for e.g. basic auth we want to create a session if the call is coming from the + // login handler, but if we want to perform basic auth during a request (called from contexthandler) we don't + // want a session to be created. + + return client.Authenticate(ctx, r) } diff --git a/pkg/services/authn/authnimpl/service_test.go b/pkg/services/authn/authnimpl/service_test.go new file mode 100644 index 00000000000..5b1a6e418d6 --- /dev/null +++ b/pkg/services/authn/authnimpl/service_test.go @@ -0,0 +1,62 @@ +package authnimpl + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/authntest" + "github.com/grafana/grafana/pkg/setting" +) + +func TestService_Authenticate(t *testing.T) { + type TestCase struct { + desc string + clientName string + expectedErr error + } + + tests := []TestCase{ + { + desc: "should succeed with authentication for configured client", + clientName: "fake", + }, + { + desc: "should fail when client is not configured", + clientName: "gitlab", + expectedErr: authn.ErrClientNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + svc := setupTests(t, func(svc *Service) { + svc.clients["fake"] = &authntest.FakeClient{} + }) + + _, err := svc.Authenticate(context.Background(), tt.clientName, &authn.Request{}) + assert.ErrorIs(t, tt.expectedErr, err) + }) + } +} + +func setupTests(t *testing.T, opts ...func(svc *Service)) *Service { + t.Helper() + + s := &Service{ + log: log.NewNopLogger(), + cfg: setting.NewCfg(), + clients: map[string]authn.Client{}, + tracer: tracing.InitializeTracerForTest(), + } + + for _, o := range opts { + o(s) + } + + return s +} diff --git a/pkg/services/authn/authntest/fake.go b/pkg/services/authn/authntest/fake.go index c1e306ac15d..0c6c85deb71 100644 --- a/pkg/services/authn/authntest/fake.go +++ b/pkg/services/authn/authntest/fake.go @@ -1,7 +1,22 @@ package authntest -import "github.com/grafana/grafana/pkg/services/authn" +import ( + "context" + + "github.com/grafana/grafana/pkg/services/authn" +) type FakeService struct { authn.Service } + +var _ authn.Client = new(FakeClient) + +type FakeClient struct { + ExpectedErr error + ExpectedIdentity *authn.Identity +} + +func (f *FakeClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + return f.ExpectedIdentity, f.ExpectedErr +} diff --git a/pkg/services/authn/clients/anonymous.go b/pkg/services/authn/clients/anonymous.go new file mode 100644 index 00000000000..3f7e55952a8 --- /dev/null +++ b/pkg/services/authn/clients/anonymous.go @@ -0,0 +1,41 @@ +package clients + +import ( + "context" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/setting" +) + +var _ authn.Client = new(Anonymous) + +func ProvideAnonymous(cfg *setting.Cfg, orgService org.Service) *Anonymous { + return &Anonymous{ + cfg: cfg, + log: log.New("authn.anonymous"), + orgService: orgService, + } +} + +type Anonymous struct { + cfg *setting.Cfg + log log.Logger + orgService org.Service +} + +func (a *Anonymous) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + o, err := a.orgService.GetByName(ctx, &org.GetOrgByNameQuery{Name: a.cfg.AnonymousOrgName}) + if err != nil { + a.log.FromContext(ctx).Error("failed to find organization", "name", a.cfg.AnonymousOrgName, "error", err) + return nil, err + } + + return &authn.Identity{ + OrgID: o.ID, + OrgName: o.Name, + OrgRoles: map[int64]org.RoleType{o.ID: org.RoleType(a.cfg.AnonymousOrgRole)}, + IsAnonymous: true, + }, nil +} diff --git a/pkg/services/authn/clients/anonymous_test.go b/pkg/services/authn/clients/anonymous_test.go new file mode 100644 index 00000000000..a33a6fdb868 --- /dev/null +++ b/pkg/services/authn/clients/anonymous_test.go @@ -0,0 +1,66 @@ +package clients + +import ( + "context" + "fmt" + "testing" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgtest" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAnonymous_Authenticate(t *testing.T) { + type TestCase struct { + desc string + org *org.Org + cfg *setting.Cfg + err error + } + + tests := []TestCase{ + { + desc: "should success with valid org configured", + org: &org.Org{ID: 1, Name: "some org"}, + cfg: &setting.Cfg{ + AnonymousOrgName: "some org", + AnonymousOrgRole: "Viewer", + }, + }, + { + desc: "should return error if any error occurs during org lookup", + err: fmt.Errorf("some error"), + cfg: &setting.Cfg{ + AnonymousOrgName: "some org", + AnonymousOrgRole: "Viewer", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + c := Anonymous{ + cfg: tt.cfg, + log: log.NewNopLogger(), + orgService: &orgtest.FakeOrgService{ExpectedOrg: tt.org, ExpectedError: tt.err}, + } + + identity, err := c.Authenticate(context.Background(), &authn.Request{}) + if err != nil { + require.Error(t, err) + require.Nil(t, identity) + } else { + require.Nil(t, err) + + assert.Equal(t, true, identity.IsAnonymous) + assert.Equal(t, tt.org.ID, identity.OrgID) + assert.Equal(t, tt.org.Name, identity.OrgName) + assert.Equal(t, tt.cfg.AnonymousOrgRole, string(identity.Role())) + } + }) + } +} diff --git a/pkg/services/authn/error.go b/pkg/services/authn/error.go new file mode 100644 index 00000000000..70258baadbb --- /dev/null +++ b/pkg/services/authn/error.go @@ -0,0 +1,5 @@ +package authn + +import "github.com/grafana/grafana/pkg/util/errutil" + +var ErrClientNotFound = errutil.NewBase(errutil.StatusNotFound, "auth.client.notConfigured") diff --git a/pkg/services/contexthandler/auth_proxy_test.go b/pkg/services/contexthandler/auth_proxy_test.go index 3958f57e249..55737a44720 100644 --- a/pkg/services/contexthandler/auth_proxy_test.go +++ b/pkg/services/contexthandler/auth_proxy_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth/authtest" + "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/org/orgtest" @@ -104,7 +105,7 @@ func getContextHandler(t *testing.T) *ContextHandler { return ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator, - &userService, orgService, nil, nil) + &userService, orgService, nil, nil, &authntest.FakeService{}) } type FakeGetSignUserStore struct { diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 0b2c3de850e..a894eee743b 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -23,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -49,6 +50,7 @@ func ProvideService(cfg *setting.Cfg, tokenService auth.UserTokenService, jwtSer tracer tracing.Tracer, authProxy *authproxy.AuthProxy, loginService login.Service, apiKeyService apikey.Service, authenticator loginpkg.Authenticator, userService user.Service, orgService org.Service, oauthTokenService oauthtoken.OAuthTokenService, features *featuremgmt.FeatureManager, + authnService authn.Service, ) *ContextHandler { return &ContextHandler{ Cfg: cfg, @@ -86,6 +88,7 @@ type ContextHandler struct { orgService org.Service oauthTokenService oauthtoken.OAuthTokenService features *featuremgmt.FeatureManager + authnService authn.Service // GetTime returns the current time. // Stubbable by tests. GetTime func() time.Time @@ -188,13 +191,24 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { } func (h *ContextHandler) initContextWithAnonymousUser(reqContext *models.ReqContext) bool { + ctx, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithAnonymousUser") + defer span.End() + + if h.features.IsEnabled(featuremgmt.FlagAuthnService) { + identity, err := h.authnService.Authenticate(ctx, authn.ClientAnonymous, &authn.Request{HTTPRequest: reqContext.Req}) + if err != nil { + return false + } + reqContext.SignedInUser = identity.SignedInUser() + reqContext.IsSignedIn = false + reqContext.AllowAnonymous = true + return true + } + if !h.Cfg.AnonymousEnabled { return false } - _, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithAnonymousUser") - defer span.End() - getOrg := org.GetOrgByNameQuery{Name: h.Cfg.AnonymousOrgName} orga, err := h.orgService.GetByName(reqContext.Req.Context(), &getOrg)