diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index 22e8f491a5e..64622a28ea3 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -3,43 +3,115 @@ package authn import ( "context" "net/http" + "strconv" + "strings" + "time" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" ) const ( - ClientAnonymous = "auth.anonymous" + ClientAPIKey = "auth.client.api-key" // #nosec G101 + ClientAnonymous = "auth.client.anonymous" ) type Service interface { - Authenticate(ctx context.Context, client string, r *Request) (*Identity, error) + // Authenticate is used to authenticate using a specific client + Authenticate(ctx context.Context, client string, r *Request) (*Identity, bool, error) } type Client interface { + // Authenticate performs the authentication for the request Authenticate(ctx context.Context, r *Request) (*Identity, error) + // Test should return true if client can be used to authenticate request + Test(ctx context.Context, r *Request) bool } type Request struct { HTTPRequest *http.Request } +const ( + APIKeyIDPrefix = "api-key:" + ServiceAccountIDPrefix = "service-account:" +) + type Identity struct { - OrgID int64 - OrgName string - IsAnonymous bool - OrgRoles map[int64]org.RoleType + ID string + OrgID int64 + OrgCount int + OrgName string + OrgRoles map[int64]org.RoleType + Login string + Name string + Email string + AuthID string + AuthModule string + IsGrafanaAdmin bool + IsDisabled bool + HelpFlags1 user.HelpFlags1 + LastSeenAt time.Time + Teams []int64 } func (i *Identity) Role() org.RoleType { return i.OrgRoles[i.OrgID] } +// IsAnonymous will return true if no ID is set on the identity +func (i *Identity) IsAnonymous() bool { + return i.ID == "" +} + +// SignedInUser is used to translate Identity into SignedInUser struct func (i *Identity) SignedInUser() *user.SignedInUser { - return &user.SignedInUser{ - OrgID: i.OrgID, - OrgName: i.OrgName, - OrgRole: i.Role(), - IsAnonymous: i.IsAnonymous, + u := &user.SignedInUser{ + UserID: 0, + OrgID: i.OrgID, + OrgName: i.OrgName, + OrgRole: i.Role(), + ExternalAuthModule: i.AuthModule, + ExternalAuthID: i.AuthID, + Login: i.Login, + Name: i.Name, + Email: i.Email, + OrgCount: i.OrgCount, + IsGrafanaAdmin: i.IsGrafanaAdmin, + IsAnonymous: i.IsAnonymous(), + IsDisabled: i.IsDisabled, + HelpFlags1: i.HelpFlags1, + LastSeenAt: i.LastSeenAt, + Teams: i.Teams, + } + + // For now, we need to set different fields of the signed-in user based on the identity "type" + if strings.HasPrefix(i.ID, APIKeyIDPrefix) { + id, _ := strconv.ParseInt(strings.TrimPrefix(i.ID, APIKeyIDPrefix), 10, 64) + u.ApiKeyID = id + } else if strings.HasPrefix(i.ID, ServiceAccountIDPrefix) { + id, _ := strconv.ParseInt(strings.TrimPrefix(i.ID, ServiceAccountIDPrefix), 10, 64) + u.UserID = id + u.IsServiceAccount = true + } + + return u +} + +func IdentityFromSignedInUser(id string, usr *user.SignedInUser) *Identity { + return &Identity{ + ID: id, + OrgID: usr.OrgID, + OrgName: usr.OrgName, + OrgRoles: map[int64]org.RoleType{usr.OrgID: usr.OrgRole}, + Login: usr.Login, + Name: usr.Name, + Email: usr.Email, + OrgCount: usr.OrgCount, + IsGrafanaAdmin: usr.IsGrafanaAdmin, + IsDisabled: usr.IsDisabled, + HelpFlags1: usr.HelpFlags1, + LastSeenAt: usr.LastSeenAt, + Teams: usr.Teams, } } diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 5ce6cc91b14..f631eb19dc9 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -5,23 +5,28 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/apikey" "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/services/user" "github.com/grafana/grafana/pkg/setting" "go.opentelemetry.io/otel/attribute" ) var _ authn.Service = new(Service) -func ProvideService(cfg *setting.Cfg, tracer tracing.Tracer, orgService org.Service) *Service { +func ProvideService(cfg *setting.Cfg, tracer tracing.Tracer, orgService org.Service, apikeyService apikey.Service, userService user.Service) *Service { s := &Service{ - log: log.New("authn.service"), - cfg: cfg, - clients: make(map[string]authn.Client), - tracer: tracer, + log: log.New("authn.service"), + cfg: cfg, + clients: make(map[string]authn.Client), + tracer: tracer, + userService: userService, } + s.clients[authn.ClientAPIKey] = clients.ProvideAPIKey(apikeyService, userService) + if s.cfg.AnonymousEnabled { s.clients[authn.ClientAnonymous] = clients.ProvideAnonymous(cfg, orgService) } @@ -34,20 +39,35 @@ type Service struct { cfg *setting.Cfg clients map[string]authn.Client - tracer tracing.Tracer + tracer tracing.Tracer + userService user.Service } -func (s *Service) Authenticate(ctx context.Context, clientName string, r *authn.Request) (*authn.Identity, error) { +func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Request) (*authn.Identity, bool, error) { ctx, span := s.tracer.Start(ctx, "authn.Authenticate") defer span.End() - span.SetAttributes("authn.client", clientName, attribute.Key("authn.client").String(clientName)) + span.SetAttributes("authn.client", client, attribute.Key("authn.client").String(client)) + logger := s.log.FromContext(ctx) - client, ok := s.clients[clientName] + c, ok := s.clients[client] if !ok { - s.log.FromContext(ctx).Warn("auth client not found", "client", clientName) + logger.Debug("auth client not found", "client", client) span.AddEvents([]string{"message"}, []tracing.EventValue{{Str: "auth client is not configured"}}) - return nil, authn.ErrClientNotFound + return nil, false, nil + } + + if !c.Test(ctx, r) { + logger.Debug("auth client cannot handle request", "client", client) + span.AddEvents([]string{"message"}, []tracing.EventValue{{Str: "auth client cannot handle request"}}) + return nil, false, nil + } + + identity, err := c.Authenticate(ctx, r) + if err != nil { + logger.Warn("auth client could not authenticate request", "client", client, "error", err) + span.AddEvents([]string{"message"}, []tracing.EventValue{{Str: "auth client could not authenticate request"}}) + return nil, true, err } // FIXME: We want to perform common authentication operations here. @@ -58,5 +78,6 @@ func (s *Service) Authenticate(ctx context.Context, clientName string, r *authn. // 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) + logger.Debug("auth client successfully authenticated request", "client", client, "identity", identity) + return identity, true, nil } diff --git a/pkg/services/authn/authnimpl/service_test.go b/pkg/services/authn/authnimpl/service_test.go index 5b1a6e418d6..1255813a358 100644 --- a/pkg/services/authn/authnimpl/service_test.go +++ b/pkg/services/authn/authnimpl/service_test.go @@ -2,6 +2,7 @@ package authnimpl import ( "context" + "errors" "testing" "github.com/stretchr/testify/assert" @@ -17,6 +18,7 @@ func TestService_Authenticate(t *testing.T) { type TestCase struct { desc string clientName string + expectedOK bool expectedErr error } @@ -24,22 +26,35 @@ func TestService_Authenticate(t *testing.T) { { desc: "should succeed with authentication for configured client", clientName: "fake", + expectedOK: true, }, { - desc: "should fail when client is not configured", - clientName: "gitlab", - expectedErr: authn.ErrClientNotFound, + desc: "should return false when client is not configured", + clientName: "gitlab", + expectedOK: false, + }, + { + desc: "should return true and error when client could be used but failed to authenticate", + clientName: "fake", + expectedOK: true, + expectedErr: errors.New("some error"), }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { svc := setupTests(t, func(svc *Service) { - svc.clients["fake"] = &authntest.FakeClient{} + svc.clients["fake"] = &authntest.FakeClient{ + ExpectedErr: tt.expectedErr, + ExpectedTest: tt.expectedOK, + } }) - _, err := svc.Authenticate(context.Background(), tt.clientName, &authn.Request{}) - assert.ErrorIs(t, tt.expectedErr, err) + _, ok, err := svc.Authenticate(context.Background(), tt.clientName, &authn.Request{}) + assert.Equal(t, tt.expectedOK, ok) + if tt.expectedErr != nil { + assert.Error(t, err) + } }) } } diff --git a/pkg/services/authn/authntest/fake.go b/pkg/services/authn/authntest/fake.go index 0c6c85deb71..a9851aad899 100644 --- a/pkg/services/authn/authntest/fake.go +++ b/pkg/services/authn/authntest/fake.go @@ -14,9 +14,14 @@ var _ authn.Client = new(FakeClient) type FakeClient struct { ExpectedErr error + ExpectedTest bool ExpectedIdentity *authn.Identity } func (f *FakeClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { return f.ExpectedIdentity, f.ExpectedErr } + +func (f *FakeClient) Test(ctx context.Context, r *authn.Request) bool { + return f.ExpectedTest +} diff --git a/pkg/services/authn/clients/anonymous.go b/pkg/services/authn/clients/anonymous.go index 3f7e55952a8..401a5bbfcca 100644 --- a/pkg/services/authn/clients/anonymous.go +++ b/pkg/services/authn/clients/anonymous.go @@ -33,9 +33,13 @@ func (a *Anonymous) Authenticate(ctx context.Context, r *authn.Request) (*authn. } return &authn.Identity{ - OrgID: o.ID, - OrgName: o.Name, - OrgRoles: map[int64]org.RoleType{o.ID: org.RoleType(a.cfg.AnonymousOrgRole)}, - IsAnonymous: true, + OrgID: o.ID, + OrgName: o.Name, + OrgRoles: map[int64]org.RoleType{o.ID: org.RoleType(a.cfg.AnonymousOrgRole)}, }, nil } + +func (a *Anonymous) Test(ctx context.Context, r *authn.Request) bool { + // If anonymous client is register it can always be used for authentication + return true +} diff --git a/pkg/services/authn/clients/anonymous_test.go b/pkg/services/authn/clients/anonymous_test.go index a33a6fdb868..7ecca2bc583 100644 --- a/pkg/services/authn/clients/anonymous_test.go +++ b/pkg/services/authn/clients/anonymous_test.go @@ -56,7 +56,7 @@ func TestAnonymous_Authenticate(t *testing.T) { } else { require.Nil(t, err) - assert.Equal(t, true, identity.IsAnonymous) + assert.Equal(t, true, identity.ID == "") 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/clients/api_key.go b/pkg/services/authn/clients/api_key.go new file mode 100644 index 00000000000..4615e6e266a --- /dev/null +++ b/pkg/services/authn/clients/api_key.go @@ -0,0 +1,180 @@ +package clients + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/grafana/grafana/pkg/components/apikeygen" + apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/apikey" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/util/errutil" +) + +const ( + basicPrefix = "Basic " + bearerPrefix = "Bearer " +) + +var ( + ErrAPIKeyInvalid = errutil.NewBase(errutil.StatusUnauthorized, "api-key.invalid", errutil.WithPublicMessage("Invalid API key")) + ErrAPIKeyExpired = errutil.NewBase(errutil.StatusUnauthorized, "api-key.expired", errutil.WithPublicMessage("Expired API key")) + ErrAPIKeyRevoked = errutil.NewBase(errutil.StatusUnauthorized, "api-key.revoked", errutil.WithPublicMessage("Revoked API key")) + ErrServiceAccountDisabled = errutil.NewBase(errutil.StatusUnauthorized, "service-account.disabled", errutil.WithPublicMessage("Disabled service account")) +) + +var _ authn.Client = new(APIKey) + +func ProvideAPIKey(apiKeyService apikey.Service, userService user.Service) *APIKey { + return &APIKey{ + log: log.New(authn.ClientAPIKey), + userService: userService, + apiKeyService: apiKeyService, + } +} + +type APIKey struct { + log log.Logger + userService user.Service + apiKeyService apikey.Service +} + +func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + apiKey, err := s.getAPIKey(ctx, getTokenFromRequest(r)) + if err != nil { + if errors.Is(err, apikeygen.ErrInvalidApiKey) { + return nil, ErrAPIKeyInvalid.Errorf("API key is invalid") + } + return nil, err + } + + if apiKey.Expires != nil && *apiKey.Expires <= time.Now().Unix() { + return nil, ErrAPIKeyExpired.Errorf("API key has expired") + } + + if apiKey.IsRevoked != nil && *apiKey.IsRevoked { + return nil, ErrAPIKeyRevoked.Errorf("Api key is revoked") + } + + go func(id int64) { + defer func() { + if err := recover(); err != nil { + s.log.Error("api key authentication panic", "err", err) + } + }() + if err := s.apiKeyService.UpdateAPIKeyLastUsedDate(context.Background(), id); err != nil { + s.log.Warn("failed to update last use date for api key", "id", id) + } + }(apiKey.Id) + + // if the api key don't belong to a service account construct the identity and return it + if apiKey.ServiceAccountId == nil || *apiKey.ServiceAccountId < 1 { + return &authn.Identity{ + ID: fmt.Sprintf("%s%d", authn.APIKeyIDPrefix, apiKey.Id), + OrgID: apiKey.OrgId, + OrgRoles: map[int64]org.RoleType{apiKey.OrgId: apiKey.Role}, + }, nil + } + + usr, err := s.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{ + UserID: *apiKey.ServiceAccountId, + OrgID: apiKey.OrgId, + }) + + if err != nil { + return nil, err + } + + if usr.IsDisabled { + return nil, ErrServiceAccountDisabled.Errorf("Disabled service account") + } + + return authn.IdentityFromSignedInUser(fmt.Sprintf("%s%d", authn.ServiceAccountIDPrefix, *apiKey.ServiceAccountId), usr), nil +} + +func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, error) { + fn := s.getFromToken + if !strings.HasPrefix(token, apikeygenprefix.GrafanaPrefix) { + fn = s.getFromTokenLegacy + } + + apiKey, err := fn(ctx, token) + if err != nil { + return nil, err + } + + return apiKey, nil +} + +func (s *APIKey) getFromToken(ctx context.Context, token string) (*apikey.APIKey, error) { + decoded, err := apikeygenprefix.Decode(token) + if err != nil { + return nil, err + } + + hash, err := decoded.Hash() + if err != nil { + return nil, err + } + + return s.apiKeyService.GetAPIKeyByHash(ctx, hash) +} + +func (s *APIKey) getFromTokenLegacy(ctx context.Context, token string) (*apikey.APIKey, error) { + decoded, err := apikeygen.Decode(token) + if err != nil { + return nil, err + } + + // fetch key + keyQuery := apikey.GetByNameQuery{KeyName: decoded.Name, OrgId: decoded.OrgId} + if err := s.apiKeyService.GetApiKeyByName(ctx, &keyQuery); err != nil { + return nil, err + } + + // validate api key + isValid, err := apikeygen.IsValid(decoded, keyQuery.Result.Key) + if err != nil { + return nil, err + } + if !isValid { + return nil, apikeygen.ErrInvalidApiKey + } + + return keyQuery.Result, nil +} + +func (s *APIKey) Test(ctx context.Context, r *authn.Request) bool { + return looksLikeApiKey(getTokenFromRequest(r)) +} + +func looksLikeApiKey(token string) bool { + return token != "" +} + +func getTokenFromRequest(r *authn.Request) string { + // api keys are only supported through http requests + if r.HTTPRequest == nil { + return "" + } + + header := r.HTTPRequest.Header.Get("Authorization") + + if strings.HasPrefix(header, bearerPrefix) { + return strings.TrimPrefix(header, bearerPrefix) + } + if strings.HasPrefix(header, basicPrefix) { + username, password, err := util.DecodeBasicAuthHeader(header) + if err == nil && username == "api_key" { + return password + } + } + return "" +} diff --git a/pkg/services/authn/clients/api_key_test.go b/pkg/services/authn/clients/api_key_test.go new file mode 100644 index 00000000000..c644f2951c2 --- /dev/null +++ b/pkg/services/authn/clients/api_key_test.go @@ -0,0 +1,207 @@ +package clients + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "testing" + + "github.com/grafana/grafana/pkg/components/apikeygen" + apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" + "github.com/grafana/grafana/pkg/services/apikey" + "github.com/grafana/grafana/pkg/services/apikey/apikeytest" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" + "github.com/stretchr/testify/assert" +) + +var ( + revoked = true + secret, hash = genApiKey(false) +) + +func TestAPIKey_Authenticate(t *testing.T) { + type TestCase struct { + desc string + req *authn.Request + expectedKey *apikey.APIKey + expectedUser *user.SignedInUser + expectedErr error + expectedIdentity *authn.Identity + } + + tests := []TestCase{ + { + desc: "should success for valid token that is not connected to a service account", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{ + "Authorization": {"Bearer " + secret}, + }, + }}, + expectedKey: &apikey.APIKey{ + Id: 1, + OrgId: 1, + Key: hash, + Role: org.RoleAdmin, + }, + expectedIdentity: &authn.Identity{ + ID: "api-key:1", + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, + }, + }, + { + desc: "should success for valid token that is connected to service account", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{ + "Authorization": {"Bearer " + secret}, + }, + }}, + expectedKey: &apikey.APIKey{ + Id: 1, + OrgId: 1, + Key: hash, + ServiceAccountId: intPtr(1), + }, + expectedUser: &user.SignedInUser{ + UserID: 1, + OrgID: 1, + IsServiceAccount: true, + OrgCount: 1, + OrgRole: org.RoleViewer, + Name: "test", + }, + expectedIdentity: &authn.Identity{ + ID: "service-account:1", + OrgID: 1, + OrgCount: 1, + Name: "test", + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + }, + }, + { + desc: "should fail for expired api key", + req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{"Authorization": {"Bearer " + secret}}}}, + expectedKey: &apikey.APIKey{ + Key: hash, + Expires: intPtr(0), + }, + expectedErr: ErrAPIKeyExpired, + }, + { + desc: "should fail for revoked api key", + req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{"Authorization": {"Bearer " + secret}}}}, + expectedKey: &apikey.APIKey{ + Key: hash, + IsRevoked: &revoked, + }, + expectedErr: ErrAPIKeyRevoked, + }, + { + desc: "should fail if service account is disabled", + req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{"Authorization": {"Bearer " + secret}}}}, + expectedKey: &apikey.APIKey{ + Key: hash, + ServiceAccountId: intPtr(1), + }, + expectedUser: &user.SignedInUser{IsDisabled: true}, + expectedErr: ErrServiceAccountDisabled, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + c := ProvideAPIKey(&apikeytest.Service{ + ExpectedAPIKey: tt.expectedKey, + }, &usertest.FakeUserService{ + ExpectedSignedInUser: tt.expectedUser, + }) + + identity, err := c.Authenticate(context.Background(), tt.req) + if tt.expectedErr != nil { + assert.Nil(t, identity) + assert.ErrorIs(t, err, tt.expectedErr) + } else { + assert.NoError(t, err) + assert.Equal(t, *tt.expectedIdentity, *identity) + } + }) + } +} + +func TestAPIKey_Test(t *testing.T) { + type TestCase struct { + desc string + req *authn.Request + expected bool + } + + tests := []TestCase{ + { + desc: "should succeed when api key is provided in Authorization header as bearer token", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{ + "Authorization": {"Bearer 123123"}, + }, + }}, + expected: true, + }, + { + desc: "should succeed when api key is provided in Authorization header as basic auth and api_key as username", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{ + "Authorization": {encodeBasicAuth("api_key", "test")}, + }, + }}, + expected: true, + }, + { + desc: "should fail when no http request is passed", + req: &authn.Request{}, + expected: false, + }, + { + desc: "should fail when no there is no Authorization header", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{}, + }}, + expected: false, + }, + { + desc: "should fail when Authorization header is not prefixed with Basic or Bearer", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{ + "Authorization": {"test"}, + }, + }}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + c := ProvideAPIKey(&apikeytest.Service{}, usertest.NewUserServiceFake()) + assert.Equal(t, tt.expected, c.Test(context.Background(), tt.req)) + }) + } +} + +func intPtr(n int64) *int64 { + return &n +} + +func genApiKey(legacy bool) (string, string) { + if legacy { + res, _ := apikeygen.New(1, "test") + return res.ClientSecret, res.HashedKey + } + res, _ := apikeygenprefix.New("test") + return res.ClientSecret, res.HashedKey +} + +func encodeBasicAuth(username, password string) string { + return "Basic " + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))) +} diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index ed12223d8ab..d814b028756 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -34,6 +34,7 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/web" ) @@ -68,6 +69,7 @@ func ProvideService(cfg *setting.Cfg, tokenService auth.UserTokenService, jwtSer orgService: orgService, oauthTokenService: oauthTokenService, features: features, + authnService: authnService, } } @@ -200,8 +202,8 @@ func (h *ContextHandler) initContextWithAnonymousUser(reqContext *models.ReqCont 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 { + identity, ok, err := h.authnService.Authenticate(ctx, authn.ClientAnonymous, &authn.Request{HTTPRequest: reqContext.Req}) + if !ok || err != nil { return false } reqContext.SignedInUser = identity.SignedInUser() @@ -271,6 +273,26 @@ func (h *ContextHandler) getAPIKey(ctx context.Context, keyString string) (*apik } func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bool { + if h.features.IsEnabled(featuremgmt.FlagAuthnService) { + identity, ok, err := h.authnService.Authenticate(reqContext.Req.Context(), authn.ClientAPIKey, &authn.Request{HTTPRequest: reqContext.Req}) + if !ok { + return false + } + + // include auth header in context + ctx := WithAuthHTTPHeader(reqContext.Req.Context(), "Authorization") + *reqContext.Req = *reqContext.Req.WithContext(ctx) + + if err != nil { + writeErr(reqContext, err) + return true + } + + reqContext.IsSignedIn = true + reqContext.SignedInUser = identity.SignedInUser() + return true + } + header := reqContext.Req.Header.Get("Authorization") parts := strings.SplitN(header, " ", 2) var keyString string @@ -709,6 +731,16 @@ func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext, return true } +// writeErr will write error response based on errutil.Error. +func writeErr(c *models.ReqContext, err error) { + grfErr := &errutil.Error{} + if !errors.As(err, grfErr) { + c.JsonApiErr(http.StatusInternalServerError, "", err) + return + } + c.JsonApiErr(grfErr.Reason.Status().HTTPStatus(), grfErr.Public().Message, err) +} + type authHTTPHeaderListContextKey struct{} var authHTTPHeaderListKey = authHTTPHeaderListContextKey{}